js/Mobilizing/text/RichText.js
import Font from "./Font";
import EventEmitter from "../core/util/EventEmitter";
import StyledTextElement from "./StyledTextElement";
import TextLine from "./TextLine";
import { Raleway } from "../misc/DefaultFonts";
import * as debug from '../core/util/Debug';
/**
* Triggered when the canvas has been redrawn, useful to synchronise Texture update and canvas refresh
* @event drawn
*/
const EVT_DRAWN = "drawn";
const TEXT_ALIGN_LEFT = "left";
const TEXT_ALIGN_RIGHT = "right";
const TEXT_ALIGN_CENTER = "center";
export default class RichText {
/**
* RichText class uses standard HTML Text to format text drawn with a certain style in a Canvas. Supported HTML style tags are ```<b>``` ```<strong>``` ```<i>``` ```<em>``` ```<br>``` ```<p>``` ```<font>``` with attributes color and size. TODO : support of face attribute in ```<font>```. Default font are predefined.
*
* @param {Object} params Parameters object, given by the constructor.
* @param {Number} [params.width=512] width of the inner canvas
* @param {Number} [params.height=512] height of the inner canvas
* @param {Number} [params.lineHeight=1] line height for the text
* @param {Number} [params.margins=10] white space (margin) in pixel on all left, top, right and bottom of the canvas. Text will be drawn within these boundaries with automatic newline management.
* @param {Number} [params.marginLeft=undefined] white space (margin) in pixel on left side.
* @param {Number} [params.marginTop=undefined] white space (margin) in pixel on top side.
* @param {Number} [params.marginRight=undefined] white space (margin) in pixel on right side.
* @param {Number} [params.marginBottom=undefined] white space (margin) in pixel on bottom side.
* @param {Number} [params.textSize=20] size, in pixels, of the text
* @param {Color} [params.textColor=black] color the text default color
* @param {Color} [params.textAlign="left"] text alignment in the canvas (margins included), can be one of "left", "center", "right".
* @param {Color} [params.backgroundColor=white] color the background color
* @param {Boolean} [params.drawBoundingBox=false] tell to draw the bounding boxes of every text blocs or not
* @param {Number} [params.boundingBoxStrokeWidth=1] the lineWidth to use for bounding boxes drawing
* @param {Font} params.font Mobilizing Font for the regular font file
* @param {Font} params.fontItalic Mobilizing Font for the italic font file
* @param {Font} params.fontBold Mobilizing Font for the bold font file
* @param {Font} params.fontBoldItalic Mobilizing Font for the bold-italic font file
*/
constructor({
text = "",
width = 512,
height = 512,
backgroundColor = "white",
lineHeight = 1,
margins = 10,
marginLeft = undefined,
marginTop = undefined,
marginRight = undefined,
marginBottom = undefined,
textSize = 20,
textColor = "black",
textAlign = TEXT_ALIGN_LEFT,
boundingBoxStrokeWidth = 1,
drawBoundingBox = false,
font = undefined,
fontItalic = undefined,
fontBold = undefined,
fontBoldItalic = undefined,
autoRender = true,
maxLines = undefined,
} = {}) {
this.text = text;
this.width = width;
this.height = height;
this.backgroundColor = backgroundColor;
this.lineHeight = lineHeight;
this.margins = margins;
this.marginLeft = marginLeft !== undefined ? marginLeft : margins;
this.marginTop = marginTop !== undefined ? marginTop : margins;
this.marginRight = marginRight !== undefined ? marginRight : margins;
this.marginBottom = marginBottom !== undefined ? marginBottom : margins;
this.textSize = textSize;
this.textColor = textColor;
this.textAlign = textAlign;
this.boundingBoxStrokeWidth = boundingBoxStrokeWidth;
this.drawBoundingBox = drawBoundingBox;
this.font = font;
this.fontItalic = fontItalic;
this.fontBold = fontBold;
this.fontBoldItalic = fontBoldItalic;
this.autoRender = autoRender;
this.maxLines = maxLines;
this.events = new EventEmitter({ scope: this });
if (!this.font && !this.fontItalic && !this.fontBold && !this.fontBoldItalic) {
debug.warn("You shoud provide all font for this class to operates. Here you gave :", this.font, this.fontItalic, this.fontBold, this.fontBoldItalic);
debug.info("Will use embedded default font.");
}
this._canvas = document.createElement("canvas");
this._canvas.width = this.width;
this._canvas.height = this.height;
this._canvasContext = this._canvas.getContext("2d");
this._canvas.events = new EventEmitter({ scope: this._canvas });
/*document.body.appendChild(this._canvas);
this._canvas.style.position = "absolute";*/
this._DOMText = document.createElement("div")
if (this.text) {
this._DOMText.innerHTML = this.text;
this.styledText = this.parseDOMTree(this._DOMText);
}
else {
console.error("no html text provided");
}
this.textDimensions = {};
this.textDimensions.width = 0;
this.textDimensions.height = 0;
//for internal use
this._lines = [];
this.setup();
}
/**
* Setup bloc called after default font loading
* @private
* @method setup
*/
setup() {
this._font = (this.font ? this.font : new Font({ base64String: Raleway.regular }));
this._italicFont = (this.fontItalic ? this.fontItalic : new Font({ base64String: Raleway.italic }));
this._boldFont = (this.fontBold ? this.fontBold : new Font({ base64String: Raleway.bold }));
this._boldItalicFont = (this.fontBoldItalic ? this.fontBoldItalic : new Font({ base64String: Raleway.boldItalic }));
if (this.autoRender) {
this.render();
}
}
/**
* @param {String} text
*/
setText(text) {
this.text = text;
if (!this._DOMText) {
this._DOMText = document.createElement("div")
}
this._DOMText.innerHTML = this.text;
this.styledText = this.parseDOMTree(this._DOMText);
this._lines = [];
this.render();
}
/**
* Get canvas width
* @method getWidth
* @return {Number} canvas width
*/
getWidth() {
return this.width;
}
/**
* Get canvas height
* @method getHeight
* @return {Number} canvas height
*/
getHeight() {
return this.height;
}
/**
* Get the canvas
* @method getCanvas
* @return {Canvas} canvas
*/
getCanvas() {
return this._canvas;
}
/**
* Returns the width and height of the drawn text in pixels.
* Computation uses margins and the actual size of the text.
* @return {Object} {width, heigth}
*/
getTextDimensions() {
return this.textDimensions;
}
/**
* @private
*/
computeTextDimensions() {
if (this._lines) {
for (let l = 0; l < this._lines.length; l++) {
const line = this._lines[l];
for (let el = 0; el < line.styledTextElements.length; el++) {
const element = line.styledTextElements[el];
const newWidth = element.x + element.width;
const newHeight = element.y + element.height;
if (this.textDimensions.width < newWidth) {
this.textDimensions.width = newWidth;
}
if (this.textDimensions.height < newHeight) {
this.textDimensions.height = newHeight;
}
}
}
}
}
/**
* Stater method to process the html tree. Manage internally the array used afterward to contain all StyledTextElement.
* @method parseDOMTree
* @param {DOMNodeObject} baseNode
* @return {Array} the resulting StyledTextElement array
*/
parseDOMTree(baseNode) {
const styledText = [];
this.processDOMTree(baseNode, styledText);
//console.log(styledText);
return styledText;
}
/**
* Recursive process of an html tree from it's main node. This method return a linear list (in the original text strings order) of StyledTextElement in order to cumulate all the tags a piece of string is attach to and the attributes of its immediate parent tag.
* @method processDOMTree
* @param {DOMNodeObject} baseNode
* @param {Array} destArray the array of destination
*/
processDOMTree(baseNode, destArray) {
let i = 0;
let node = null;
//lazy <p> support : <p> has automatic margin, top and bottom, of 1 line-height, which is 2 <br> ;-)
//@TODO : support <p> top and bottom margin ?
if (baseNode.tagName === "P") {
if (baseNode.previousElementSibling && baseNode.previousElementSibling.tagName === "P") {
baseNode.innerHTML = `${baseNode.innerHTML}<br>`;
}
else {
baseNode.innerHTML = `<br><br>${baseNode.innerHTML}<br>`;
}
}
while ((node = baseNode.childNodes[i])) {
const children = node.childNodes.length;
if (children >= 1) {
this.processDOMTree(node, destArray);
}
else {
//split the textContent
const words = node.textContent.split(/\s/);
//StyledTextElement array construction loop
for (let w = 0; w < words.length; w++) {
const el = new StyledTextElement();
//surchage StyledTextElement
el.attributes = [];
el.tags = [];
el.setText(words[w]);
//reconstruct white space, lost after split, take first place, \n and "." is first as conditions
if (w !== 0) {
el.text = ` ${el.text}`;
}
//manage attributes at parent level as it's how it's done
if (node.parentNode.attributes) {
for (let a = 0; a < node.parentNode.attributes.length; a++) {
el.attributes.push({ name: node.parentNode.attributes[a].name, value: node.parentNode.attributes[a].value });
}
if (node.tagName) {
el.tags.push(node.tagName);
}
let tempNode = node;
while (tempNode.parentNode) {
//avoid to take the main enclosing span
if (tempNode.parentNode.tagName !== "DIV") {
el.tags.push(tempNode.parentNode.tagName);
}
tempNode = tempNode.parentNode;
}
destArray.push(el);
}
}
}
i++;
}
return destArray;
}
/**
* Render the input HTML text parsed in an array of StyledTextElement in the current canvas
* @method render
*/
render() {
this._canvasContext.fillStyle = this.backgroundColor;
this._canvasContext.fillRect(0, 0, this.width, this.height);
//x position of drawing (letter pos in x)
let letterXOffset = this.marginLeft;
//y position of drawing (letter pos in y, or baseline)
let lineYOffset = this.marginTop;
//number of lines in the canvas
let lineCount = 0;
let styledTextLength = 0;
if (this.maxLines) {
styledTextLength = this.maxLines;
}
else {
styledTextLength = this.styledText.length;
}
for (let i = 0; i < styledTextLength; i++) {
const el = this.styledText[i];
//select the font to use
let currentFont = this._font;
//define the font based on associated tags
if (el.tags.indexOf("B") >= 0 || el.tags.indexOf("STRONG") >= 0) {
currentFont = this._boldFont;
}
if (el.tags.indexOf("I") >= 0 || el.tags.indexOf("EM") >= 0) {
currentFont = this._italicFont;
}
if (el.tags.indexOf("B") >= 0 && el.tags.indexOf("I") >= 0 ||
el.tags.indexOf("STRONG") >= 0 && el.tags.indexOf("EM") >= 0) {
currentFont = this._boldItalicFont;
}
let size = this.textSize;
//test for new line <br> and add a new line to lineCount
if (el.tags.indexOf("BR") >= 0) {
lineYOffset += size * this.lineHeight;
letterXOffset = this.marginLeft;
//increment the lineNb
lineCount++;
this.events.trigger("newline", lineCount);
}
let color = this.textColor;
//is it the 1st run ? place the baseline to the margin + font size
if (lineYOffset === this.marginTop) {
lineYOffset += size;
}
//test for new line from canvas width limit and reset offsets and add a new line to lineCount
const tempWidth = letterXOffset + currentFont.getTextSize(el.text, size).width;
if (tempWidth > this.width - this.marginRight) {
lineYOffset += size * this.lineHeight;
letterXOffset = this.marginLeft;
//to erase first space and avoid unused whitespace at line start
el.text = el.text.replace(/^\s/, "");
//increment the lineNb
lineCount++;
this.events.trigger("newline", lineCount);
}
//manage font tag attributes
//@TODO : manage font changes!!
if (el.attributes) {
for (let j = 0; j < el.attributes.length; j++) {
if (el.attributes[j].name === "color") {
color = el.attributes[j].value;
//console.log(color);
}
if (el.attributes[j].name === "size") {
size = Number(el.attributes[j].value);
}
}
}
//set the font to use
el.setFont(currentFont);
/*el.setPath(currentFont.getFont().getPath(el.text, letterXOffset, lineYOffset, size));*/
const fontTextSize = currentFont.getTextSize(el.text, size);
//store info in the object
el.line = lineCount;
el.setX(letterXOffset);
el.setY(lineYOffset);
el.setSize(size);
//el.setColor("#" + color.getHexString());
el.setColor(color);
el.update();//refresh
//populate the lines object for later align computation
if (this._lines[lineCount]) {
const lineObj = this._lines[lineCount];
lineObj.styledTextElements.push(el);
lineObj.width += el.width;
}
else {
const lineObj = new TextLine();
lineObj.styledTextElements.push(el);
lineObj.width += el.width;
this._lines.push(lineObj);
}
//update the x offset for next el
letterXOffset += fontTextSize.width;
}
//text-align loop, we adjust x positions of every text bloc
for (let i = 0; i < this._lines.length; i++) {
let xDiff = null;
const line = this._lines[i];
//compute the x difference between current and desired align
switch (this.textAlign) {
case TEXT_ALIGN_CENTER:
xDiff = (this.width / 2) - (this.marginLeft + line.width / 2);
break;
case TEXT_ALIGN_RIGHT:
xDiff = (this.width - this.marginLeft) - (this.marginRight + line.width);
break;
default:
break;
}
if (xDiff !== null) {
for (let j = 0; j < line.styledTextElements.length; j++) {
const el = line.styledTextElements[j];
el.setX(el.x + xDiff);
el.update();//refresh
}
}
}
//draw loop
for (let i = 0; i < styledTextLength; i++) {
const el = this.styledText[i];
el.path.draw(this._canvasContext);
if (this.drawBoundingBox) {
const boundingBox = el.path.getBoundingBox();
this._canvasContext.beginPath();
this._canvasContext.moveTo(boundingBox.x1, boundingBox.y1);
this._canvasContext.lineTo(boundingBox.x2, boundingBox.y1);
this._canvasContext.lineTo(boundingBox.x2, boundingBox.y2);
this._canvasContext.lineTo(boundingBox.x1, boundingBox.y2);
this._canvasContext.lineTo(boundingBox.x1, boundingBox.y1 - this.boundingBoxStrokeWidth / 2);
this._canvasContext.lineWidth = this.boundingBoxStrokeWidth;
this._canvasContext.stroke();
}
}
//get text dimensions
this.computeTextDimensions();
//emit a custom event on the canvas to refresh texture when used
this._canvas.events.trigger(EVT_DRAWN);
}
}