UNPKG

canvas-text-block

Version:

Easily render a wrapping block of text in an HTML Canvas Element.

320 lines (300 loc) 13.4 kB
'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * https://developer.mozilla.org/en-US/docs/Web/CSS/font */ var composeFontString = function (fontSize, fontFamily, weight) { return "".concat(weight, " ").concat(fontSize, "px ").concat(fontFamily); }; var calculateNextLineYPos = function (yPos, LineActualBoundingBoxAscent, lineHeight, index) { return yPos + LineActualBoundingBoxAscent / 2 + lineHeight * index; }; var appendEllipsisToLine = function (context, line, lineMaxWidth) { for (var i = 0; i < line.length; i++) { var possibleLine = line.substr(0, line.length - i) + "..."; var possibleLineWidth = context.measureText(possibleLine).width; if (possibleLineWidth < lineMaxWidth) { return possibleLine; } } throw new Error("Could not add ellipsis"); }; var defaultOptions = { color: "#fff", fontFamily: "arial", fontSize: 16, lineHeight: 24, padding: 0, weight: "normal", ellipsis: true, overflow: false, backgroundColor: "transparent", }; var WidthLargerThanCanvasWidthError = /** @class */ (function (_super) { __extends(WidthLargerThanCanvasWidthError, _super); function WidthLargerThanCanvasWidthError() { var _this = _super.call(this) || this; _this.message = "The specified width can not be larger than the canvas width"; _this.name = "WidthLargerThanCanvasWidthError"; return _this; } return WidthLargerThanCanvasWidthError; }(Error)); var HeightLargerThanCanvasHeightError = /** @class */ (function (_super) { __extends(HeightLargerThanCanvasHeightError, _super); function HeightLargerThanCanvasHeightError() { var _this = _super.call(this) || this; _this.message = "The specified height can not be larger than the canvas height"; _this.name = "HeightLargerThanCanvasHeightError"; return _this; } return HeightLargerThanCanvasHeightError; }(Error)); var XPositionOutOfRangeError = /** @class */ (function (_super) { __extends(XPositionOutOfRangeError, _super); function XPositionOutOfRangeError() { var _this = _super.call(this) || this; _this.message = "The x position specified is out of range"; _this.name = "XPositionOutOfRangeError"; return _this; } return XPositionOutOfRangeError; }(Error)); var YPositionOutOfRangeError = /** @class */ (function (_super) { __extends(YPositionOutOfRangeError, _super); function YPositionOutOfRangeError() { var _this = _super.call(this) || this; _this.message = "The y position specified is out of range"; _this.name = "YPositionOutOfRangeError"; return _this; } return YPositionOutOfRangeError; }(Error)); var CanvasContextIsNullError = /** @class */ (function (_super) { __extends(CanvasContextIsNullError, _super); function CanvasContextIsNullError() { var _this = _super.call(this) || this; _this.message = "The canvas needs a 2d context!"; _this.name = "CanvasContextIsNullError"; return _this; } return CanvasContextIsNullError; }(Error)); var CanvasTextBlock = /** @class */ (function () { function CanvasTextBlock(canvas, x, y, width, height, options) { var _this = this; this.getTextBlockMaxWidth = function () { var maxWidth = _this.width; if (_this.options.padding !== 0) { maxWidth = maxWidth - _this.options.padding * 2; } return maxWidth; }; this.getOptions = function () { return _this.options; }; this.getMaxLineCount = function () { var height = _this.height; if (_this.options.padding !== 0) { height = height - _this.options.padding * 2; } return Math.floor(height / _this.options.lineHeight); }; this.setBackgroundColor = function () { if (_this.options.backgroundColor !== "transparent") { _this.context.fillStyle = _this.options.backgroundColor; _this.context.fillRect(_this.x, _this.y, _this.width, _this.height); } }; this.getStartingLineXPos = function () { var xPos = _this.x; if (_this.options.padding !== 0) { xPos = _this.x + _this.options.padding; } return xPos; }; this.getStartingLineYPos = function () { var yPos = _this.y; if (_this.options.padding !== 0) { yPos = _this.y + _this.options.padding; } return yPos; }; this.setText = function (text) { var lines = _this.getLinesFromText(text); // Loop through each line and render then to the canvas. lines.map(function (line, index) { _this.context.fillStyle = _this.options.color; _this.context.font = composeFontString(_this.options.fontSize, _this.options.fontFamily, _this.options.weight); /** * TODO: I'd like to be able to define the text align in settings, * but more work needs to be done here before that can happen. */ _this.context.textAlign = "left"; _this.context.textBaseline = "top"; var textMeasurements = _this.context.measureText(line); /** * Render each line on the x,y cords based on the line index, line height * font size, etc. */ _this.context.fillText(line, _this.getStartingLineXPos(), calculateNextLineYPos(_this.getStartingLineYPos(), textMeasurements.actualBoundingBoxAscent, _this.options.lineHeight, index)); }); }; this.getLinesFromText = function (text) { var words = text.split(" "); // Find all words with new lines and make them their own word. for (var i = 0; i < words.length - 1; i++) { if (words[i].includes("\n")) { var newWords = words[i].split(/(\n)/g); words.splice.apply(words, __spreadArray([i, 1], newWords, false)); i = i + newWords.length - 1; } } var lines = []; var currentLine = words[0]; // Loop through each word and build lines for the text block for (var i = 1; i < words.length; i++) { var word = words[i]; _this.context.font = composeFontString(_this.options.fontSize, _this.options.fontFamily, _this.options.weight); // If the word is a new line, append a new line. if (word === "\n") { lines.push(currentLine); currentLine = ""; continue; } var currentLineWithWordWidth = _this.context.measureText("".concat(currentLine, " ").concat(word)).width; // When the current line with the new word is less width than the max width. if (currentLineWithWordWidth < _this.getTextBlockMaxWidth()) { // Append a space before the previous word if the previous // word is not a new line. if (words[i - 1] && words[i - 1] === "\n") { currentLine += word; } else { currentLine += " ".concat(word); } } else { lines.push(currentLine); // If the word is wider than the block width, break it up into fragments var wordLength = _this.context.measureText(word).width; if (wordLength > _this.width) { var fragments = []; var currentFragment = ""; var splitWord = word.split(""); for (var i_1 = 0; i_1 < splitWord.length; i_1++) { var currentFragmentWidth = _this.context.measureText("".concat(currentFragment).concat(splitWord[i_1], "-")).width; if (currentFragmentWidth > _this.getTextBlockMaxWidth()) { fragments.push("".concat(currentFragment, "-")); currentFragment = splitWord[i_1]; } else { currentFragment += splitWord[i_1]; } } lines.push.apply(lines, fragments); currentLine = currentFragment; } else { currentLine = word; } } } lines.push(currentLine); if (lines.length > _this.getMaxLineCount()) { // Clip the amount of lines to render depending on the overflow value if (!_this.options.overflow) { lines = lines.slice(0, _this.getMaxLineCount()); // Add ellipsis to the last line if needed if (_this.options.ellipsis && lines.length >= _this.getMaxLineCount()) { var lastLine = appendEllipsisToLine(_this.context, lines[lines.length - 1], _this.width); lines[lines.length - 1] = lastLine; } } } return lines; }; this.canvas = canvas; var _context = this.canvas.getContext("2d"); if (!_context) { throw new CanvasContextIsNullError(); } this.context = _context; this.x = x; this.y = y; this.width = width; this.height = height; if (this.width > this.canvas.width) { throw new WidthLargerThanCanvasWidthError(); } if (this.height > this.canvas.height) { throw new HeightLargerThanCanvasHeightError(); } if (this.x < 0 || this.x > this.canvas.width) { throw new XPositionOutOfRangeError(); } if (this.y < 0 || this.y > this.canvas.height) { throw new YPositionOutOfRangeError(); } if ((options === null || options === void 0 ? void 0 : options.fontSize) && !options.lineHeight) { // If the font size is provided without a line height, set it to 1.25 options.lineHeight = options.fontSize * 1.25; } this.options = __assign(__assign({}, defaultOptions), options); if (this.options.backgroundColor !== "transparent") { this.setBackgroundColor(); } } return CanvasTextBlock; }()); module.exports = CanvasTextBlock;