text-to-canvas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/SECURITY.md +37 -0
- package/dist/text-to-canvas.cjs +569 -0
- package/dist/text-to-canvas.esm.min.js +335 -0
- package/dist/text-to-canvas.esm.min.js.map +1 -0
- package/dist/text-to-canvas.min.js +2 -0
- package/dist/text-to-canvas.min.js.map +1 -0
- package/dist/text-to-canvas.mjs +569 -0
- package/dist/text-to-canvas.umd.min.js +2 -0
- package/dist/text-to-canvas.umd.min.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/model.d.ts +217 -0
- package/dist/types/util/height.d.ts +25 -0
- package/dist/types/util/justify.d.ts +22 -0
- package/dist/types/util/split.d.ts +43 -0
- package/dist/types/util/style.d.ts +18 -0
- package/dist/types/util/trim.d.ts +22 -0
- package/dist/types/util/whitespace.d.ts +6 -0
- package/package.json +99 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-to-canvas.umd.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n });\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: config.x || 0,\n y: config.y || 0,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const { width, height, x = 0, y = 0 } = config;\n const xEnd = x + width;\n const yEnd = y + height;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = x;\n } else {\n textAnchor = x + width / 2;\n }\n\n let debugY = y;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = y + height / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(x, y, width, height);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, y);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(x, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_b","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","i","_c","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { specToJson, splitWords, splitText, textToWords, wordsToJson } from './util/split';
|
|
2
|
+
import { getTextHeight, getWordHeight } from './util/height';
|
|
3
|
+
import { getTextStyle, getTextFormat } from './util/style';
|
|
4
|
+
import { CanvasRenderContext, DrawTextConfig, Text } from './model';
|
|
5
|
+
declare const drawText: (ctx: CanvasRenderContext, text: Text, config: DrawTextConfig) => {
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
export { drawText, specToJson, splitText, splitWords, textToWords, wordsToJson, getTextHeight, getWordHeight, getTextStyle, getTextFormat, };
|
|
9
|
+
export * from './model';
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
export type CanvasRenderContext = CanvasRenderingContext2D;
|
|
2
|
+
/**
|
|
3
|
+
* Identifies the minimum Canvas `TextMetrics` properties required by this library. This is
|
|
4
|
+
* important for serialization across the main thread to a Web Worker thread (or vice versa)
|
|
5
|
+
* as the native `TextMetrics` object fails to get serialized by `Worker.postMessage()`,
|
|
6
|
+
* causing an exception.
|
|
7
|
+
*/
|
|
8
|
+
export interface TextMetricsLike {
|
|
9
|
+
readonly fontBoundingBoxAscent: number;
|
|
10
|
+
readonly fontBoundingBoxDescent: number;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
}
|
|
13
|
+
export type CanvasTextMetrics = TextMetrics | TextMetricsLike;
|
|
14
|
+
export interface TextFormat {
|
|
15
|
+
/** Font family (CSS value). */
|
|
16
|
+
fontFamily?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Font size (px).
|
|
19
|
+
*
|
|
20
|
+
* ❗️ Rendering words at different sizes currently does not render well per text baseline.
|
|
21
|
+
* Prefer setting a common size as the base formatting for all text instead of setting
|
|
22
|
+
* a different size for a subset of Words.
|
|
23
|
+
*/
|
|
24
|
+
fontSize?: number;
|
|
25
|
+
/** Font weight (CSS value). */
|
|
26
|
+
fontWeight?: string;
|
|
27
|
+
/** Font style (CSS value) */
|
|
28
|
+
fontStyle?: string;
|
|
29
|
+
/** Font variant (CSS value). */
|
|
30
|
+
fontVariant?: 'normal' | 'small-caps' | '';
|
|
31
|
+
/** CSS color value. */
|
|
32
|
+
fontColor?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface Word {
|
|
35
|
+
/** The word. Can also be whitespace. */
|
|
36
|
+
text: string;
|
|
37
|
+
/** Optional formatting. If unspecified, base format defaults will be used. */
|
|
38
|
+
format?: TextFormat;
|
|
39
|
+
/**
|
|
40
|
+
* Optional metrics for this Word if it has __already been measured__ on canvas.
|
|
41
|
+
*
|
|
42
|
+
* ❗️ This is an optimization to increase performance of subsequent splitting of words.
|
|
43
|
+
* If specified, it's assumed that the `text` and `format` __have not changed__ since
|
|
44
|
+
* the last time the word was measured. Also that other aspects of the canvas related
|
|
45
|
+
* to rendering, such as aspect ratio, that could affect measurements __have not changed__.
|
|
46
|
+
*
|
|
47
|
+
* If not specified, this member __will be added__ by `splitWords()` once the word is measured
|
|
48
|
+
* so that it's easy to feed the same word back into `splitWords()` at a later time if its
|
|
49
|
+
* `text` and `format` remain the same. If they change, simply set this property to `undefined`
|
|
50
|
+
* to force it to be re-measured.
|
|
51
|
+
*/
|
|
52
|
+
metrics?: CanvasTextMetrics;
|
|
53
|
+
}
|
|
54
|
+
export type PlainText = string;
|
|
55
|
+
export type Text = PlainText | Word[];
|
|
56
|
+
export interface DrawTextConfig extends TextFormat {
|
|
57
|
+
/**
|
|
58
|
+
* Width of box (px) at X/Y in 2D context within which text should be rendered. This will affect
|
|
59
|
+
* text wrapping, but will not necessarily constrain the text because, at minimum, one word,
|
|
60
|
+
* regardless of its width, will be rendered per line.
|
|
61
|
+
*/
|
|
62
|
+
width: number;
|
|
63
|
+
/**
|
|
64
|
+
* Height of box (px) at X/Y in 2D context within which text should be rendered. While this
|
|
65
|
+
* __will not constrain how the text is rendered__, it will determine how it's positioned
|
|
66
|
+
* given the alignment specified (`align` and `vAlign`). All the text is rendered, and may
|
|
67
|
+
* be rendered above/below the box defined in part by this dimension if it's too long to
|
|
68
|
+
* fit within the specified `boxWidth`.
|
|
69
|
+
*/
|
|
70
|
+
height: number;
|
|
71
|
+
/** Absolute X coordinate (px) in 2D context where text should be rendered. Defaults to `0`. */
|
|
72
|
+
x?: number;
|
|
73
|
+
/** Absolute Y coordinate (px) in 2D context where text should be rendered. Defaults to `0`. */
|
|
74
|
+
y?: number;
|
|
75
|
+
/** Horizontal alignment. Defaults to 'center'. */
|
|
76
|
+
align?: 'left' | 'center' | 'right';
|
|
77
|
+
/** Vertical alignment. Defaults to 'middle'. */
|
|
78
|
+
vAlign?: 'top' | 'middle' | 'bottom';
|
|
79
|
+
/** True if text should be justified within the `boxWidth` to fill the hole width. */
|
|
80
|
+
justify?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* __NOTE:__ Applies only if `text`, given to `drawText()`, is a `Word[]`. Ignored if it's
|
|
83
|
+
* a `string`.
|
|
84
|
+
*
|
|
85
|
+
* True indicates `text` is a `Word` array that contains _mostly_ visible words and
|
|
86
|
+
* whitespace should be inferred _unless a word is whitespace (e.g. a new line or tab)_, based
|
|
87
|
+
* on the context's general text formatting style (i.e. every space will use the font style set
|
|
88
|
+
* on the context). This makes it easier to provide a `Word[]` because whitespace can be omitted
|
|
89
|
+
* if it's just spaces, and only informative whitespace is necessary (e.g. hard line breaks
|
|
90
|
+
* as Words with `text="\n"`).
|
|
91
|
+
*
|
|
92
|
+
* False indicates that `words` contains its own whitespace and it shouldn't be inferred.
|
|
93
|
+
*/
|
|
94
|
+
inferWhitespace?: boolean;
|
|
95
|
+
/** True if debug lines should be rendered behind the text. */
|
|
96
|
+
debug?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface BaseSplitProps {
|
|
99
|
+
ctx: CanvasRenderContext;
|
|
100
|
+
/** Absolute X coordinate (px) in 2D context where text should be rendered. */
|
|
101
|
+
x: number;
|
|
102
|
+
/** Absolute Y coordinate (px) in 2D context where text should be rendered. */
|
|
103
|
+
y: number;
|
|
104
|
+
/**
|
|
105
|
+
* Width of box (px) at X/Y in 2D context within which text should be rendered. This will affect
|
|
106
|
+
* text wrapping, but will not necessarily constrain the text because, at minimum, one word,
|
|
107
|
+
* regardless of its width, will be rendered per line.
|
|
108
|
+
*/
|
|
109
|
+
width: number;
|
|
110
|
+
/**
|
|
111
|
+
* Height of box (px) at X/Y in 2D context within which text should be rendered. While this
|
|
112
|
+
* __will not constrain how the text is rendered__, it will determine how it's positioned
|
|
113
|
+
* given the alignment specified (`align` and `vAlign`). All the text is rendered, and may
|
|
114
|
+
* be rendered above/below the box defined in part by this dimension if it's too long to
|
|
115
|
+
* fit within the specified `boxWidth`.
|
|
116
|
+
*/
|
|
117
|
+
height: number;
|
|
118
|
+
/** Horizontal alignment. Defaults to 'center'. */
|
|
119
|
+
align?: 'left' | 'center' | 'right';
|
|
120
|
+
/** Vertical alignment. Defaults to 'middle'. */
|
|
121
|
+
vAlign?: 'top' | 'middle' | 'bottom';
|
|
122
|
+
/** True if text should be justified within the `boxWidth` to fill the hole width. */
|
|
123
|
+
justify?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Base/default font styles. These will be used for any word that doesn't have specific
|
|
126
|
+
* formatting overrides. It's basically how "plain text" should be rendered.
|
|
127
|
+
*/
|
|
128
|
+
format?: TextFormat;
|
|
129
|
+
}
|
|
130
|
+
export interface SplitTextProps extends BaseSplitProps {
|
|
131
|
+
/**
|
|
132
|
+
* Text to render. Newlines are interpreted as hard breaks. Whitespace is preserved __only
|
|
133
|
+
* within the string__ (whitespace on either end is trimmed). Text will always wrap at max
|
|
134
|
+
* width regardless of newlines.
|
|
135
|
+
*/
|
|
136
|
+
text: PlainText;
|
|
137
|
+
}
|
|
138
|
+
export interface SplitWordsProps extends BaseSplitProps {
|
|
139
|
+
/** For hard breaks, include words that are newline characters as their `text`. */
|
|
140
|
+
words: Word[];
|
|
141
|
+
/**
|
|
142
|
+
* True (default) indicates `words` contains _mostly_ visible words and whitespace should be
|
|
143
|
+
* inferred _unless a word is whitespace (e.g. a new line or tab)_, based on the context's
|
|
144
|
+
* general text formatting style (i.e. every space will use the font style set on the context).
|
|
145
|
+
*
|
|
146
|
+
* False indicates that `words` contains its own whitespace and it shouldn't be inferred.
|
|
147
|
+
*/
|
|
148
|
+
inferWhitespace?: boolean;
|
|
149
|
+
}
|
|
150
|
+
/** Hash representing a `Word` and its associated `TextFormat`. */
|
|
151
|
+
export type WordHash = string;
|
|
152
|
+
/**
|
|
153
|
+
* Maps a `Word` to its measured `metrics` and the font `format` used to measure it (if the
|
|
154
|
+
* `Word` specified a format to use; undefined means the base formatting, as set on the canvas
|
|
155
|
+
* 2D context, was used).
|
|
156
|
+
*/
|
|
157
|
+
export type WordMap = Map<WordHash, {
|
|
158
|
+
metrics: CanvasTextMetrics;
|
|
159
|
+
format?: Required<TextFormat>;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* A `Word` along with its __relative__ position along the X/Y axis within the bounding box
|
|
163
|
+
* in which it is to be drawn.
|
|
164
|
+
*
|
|
165
|
+
* It's the caller's responsibility to render each Word onto the Canvas, as well as to calculate
|
|
166
|
+
* each Word's location in the Canvas' absolute space.
|
|
167
|
+
*/
|
|
168
|
+
export interface PositionedWord {
|
|
169
|
+
/** Reference to a `Word` given to `splitWords()`. */
|
|
170
|
+
readonly word: Word;
|
|
171
|
+
/**
|
|
172
|
+
* Full formatting used to measure/position the `word`, __if a `word.format` partial
|
|
173
|
+
* was specified.__
|
|
174
|
+
*
|
|
175
|
+
* ❗️ __Use this for actual rendering__ instead of the original `word.format`.
|
|
176
|
+
*/
|
|
177
|
+
readonly format?: Readonly<Required<TextFormat>>;
|
|
178
|
+
/** X position (px) relative to render box within 2D context. */
|
|
179
|
+
readonly x: number;
|
|
180
|
+
/** Y position (px) relative to render box within 2D context. */
|
|
181
|
+
readonly y: number;
|
|
182
|
+
/** Width (px) used to render text. */
|
|
183
|
+
readonly width: number;
|
|
184
|
+
/** Height (px) used to render text. */
|
|
185
|
+
readonly height: number;
|
|
186
|
+
/**
|
|
187
|
+
* True if this `word` is non-visible whitespace (per a Regex `^\s+$` match) and so
|
|
188
|
+
* __could be skipped when rendering__.
|
|
189
|
+
*/
|
|
190
|
+
readonly isWhitespace: boolean;
|
|
191
|
+
}
|
|
192
|
+
export interface RenderSpec {
|
|
193
|
+
/**
|
|
194
|
+
* Words split into lines as they would be visually wrapped on canvas if rendered
|
|
195
|
+
* to their prescribed positions.
|
|
196
|
+
*/
|
|
197
|
+
readonly lines: PositionedWord[][];
|
|
198
|
+
/**
|
|
199
|
+
* Baseline to use when rendering text based on alignment settings.
|
|
200
|
+
*
|
|
201
|
+
* ❗️ Set this on the 2D context __before__ rendering the Words in the `lines`.
|
|
202
|
+
*/
|
|
203
|
+
readonly textBaseline: CanvasTextBaseline;
|
|
204
|
+
/**
|
|
205
|
+
* Alignment to use when rendering text based on alignment settings.
|
|
206
|
+
*
|
|
207
|
+
* ❗️ Set this on the 2D context __before__ rendering the Words in the `lines`.
|
|
208
|
+
*/
|
|
209
|
+
readonly textAlign: CanvasTextAlign;
|
|
210
|
+
/**
|
|
211
|
+
* Total required width (px) to render all the lines as wrapped (i.e. the original
|
|
212
|
+
* `width` used to split the words.
|
|
213
|
+
*/
|
|
214
|
+
readonly width: number;
|
|
215
|
+
/** Total required height (px) to render all lines. */
|
|
216
|
+
readonly height: number;
|
|
217
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CanvasRenderContext, Word } from '../model';
|
|
2
|
+
/**
|
|
3
|
+
* Gets the measured height of a given `Word` using its text style.
|
|
4
|
+
* @returns {number} Height in pixels.
|
|
5
|
+
*/
|
|
6
|
+
export declare const getWordHeight: ({ ctx, word, }: {
|
|
7
|
+
ctx: CanvasRenderContext;
|
|
8
|
+
/**
|
|
9
|
+
* Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.
|
|
10
|
+
*/
|
|
11
|
+
word: Word;
|
|
12
|
+
}) => number;
|
|
13
|
+
/**
|
|
14
|
+
* Gets the measured height of a given `string` using a given text style.
|
|
15
|
+
* @returns {number} Height in pixels.
|
|
16
|
+
*/
|
|
17
|
+
export declare const getTextHeight: ({ ctx, text, style, }: {
|
|
18
|
+
ctx: CanvasRenderContext;
|
|
19
|
+
text: string;
|
|
20
|
+
/**
|
|
21
|
+
* CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font
|
|
22
|
+
* settings/styles are used.
|
|
23
|
+
*/
|
|
24
|
+
style?: string;
|
|
25
|
+
}) => number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Word } from '../model';
|
|
2
|
+
/**
|
|
3
|
+
* Inserts spaces between words in a line in order to raise the line width to the box width.
|
|
4
|
+
* The spaces are evenly spread in the line, and extra spaces (if any) are only inserted
|
|
5
|
+
* between words, not at either end of the `line`.
|
|
6
|
+
*
|
|
7
|
+
* @returns New array containing original words from the `line` with additional whitespace
|
|
8
|
+
* for justification to `boxWidth`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const justifyLine: ({ line, spaceWidth, spaceChar, boxWidth, }: {
|
|
11
|
+
/** Assumed to have already been trimmed on both ends. */
|
|
12
|
+
line: Word[];
|
|
13
|
+
/** Width (px) of `spaceChar`. */
|
|
14
|
+
spaceWidth: number;
|
|
15
|
+
/**
|
|
16
|
+
* Character used as a whitespace in justification. Will be injected in between Words in
|
|
17
|
+
* `line` in order to justify the text on the line within `lineWidth`.
|
|
18
|
+
*/
|
|
19
|
+
spaceChar: string;
|
|
20
|
+
/** Width (px) of the box containing the text (i.e. max `line` width). */
|
|
21
|
+
boxWidth: number;
|
|
22
|
+
}) => Word[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SplitTextProps, SplitWordsProps, RenderSpec, Word } from '../model';
|
|
2
|
+
/**
|
|
3
|
+
* Serializes render specs to JSON for storage or for sending via `postMessage()`
|
|
4
|
+
* between the main thread and a Web Worker thread.
|
|
5
|
+
*
|
|
6
|
+
* This is primarily to help with the fact that `postMessage()` fails if given a native
|
|
7
|
+
* Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.
|
|
8
|
+
*
|
|
9
|
+
* @param specs
|
|
10
|
+
* @returns Specs serialized as JSON.
|
|
11
|
+
*/
|
|
12
|
+
export declare const specToJson: (specs: RenderSpec) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Serializes a list of Words to JSON for storage or for sending via `postMessage()`
|
|
15
|
+
* between the main thread and a Web Worker thread.
|
|
16
|
+
*
|
|
17
|
+
* This is primarily to help with the fact that `postMessage()` fails if given a native
|
|
18
|
+
* Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.
|
|
19
|
+
*
|
|
20
|
+
* @param words
|
|
21
|
+
* @returns Words serialized as JSON.
|
|
22
|
+
*/
|
|
23
|
+
export declare const wordsToJson: (words: Word[]) => string;
|
|
24
|
+
/**
|
|
25
|
+
* Splits Words into positioned lines of Words as they need to be rendred in 2D space,
|
|
26
|
+
* but does not render anything.
|
|
27
|
+
* @param config
|
|
28
|
+
* @returns Lines of positioned words to be rendered, and total height required to
|
|
29
|
+
* render all lines.
|
|
30
|
+
*/
|
|
31
|
+
export declare const splitWords: ({ ctx, words, justify, format: baseFormat, inferWhitespace, ...positioning }: SplitWordsProps) => RenderSpec;
|
|
32
|
+
/**
|
|
33
|
+
* Converts a string of text containing words and whitespace, as well as line breaks (newlines),
|
|
34
|
+
* into a `Word[]` that can be given to `splitWords()`.
|
|
35
|
+
* @param text String to convert into Words.
|
|
36
|
+
* @returns Converted text.
|
|
37
|
+
*/
|
|
38
|
+
export declare const textToWords: (text: string) => Word[];
|
|
39
|
+
/**
|
|
40
|
+
* Splits plain text into lines in the order in which they should be rendered, top-down,
|
|
41
|
+
* preserving whitespace __only within the text__ (whitespace on either end is trimmed).
|
|
42
|
+
*/
|
|
43
|
+
export declare const splitText: ({ text, ...params }: SplitTextProps) => string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TextFormat } from '../model';
|
|
2
|
+
export declare const DEFAULT_FONT_FAMILY = "Arial";
|
|
3
|
+
export declare const DEFAULT_FONT_SIZE = 14;
|
|
4
|
+
export declare const DEFAULT_FONT_COLOR = "black";
|
|
5
|
+
/**
|
|
6
|
+
* Generates a text format based on defaults and any provided overrides.
|
|
7
|
+
* @param format Overrides to `baseFormat` and default format.
|
|
8
|
+
* @param baseFormat Overrides to default format.
|
|
9
|
+
* @returns Full text format (all properties specified).
|
|
10
|
+
*/
|
|
11
|
+
export declare const getTextFormat: (format?: TextFormat, baseFormat?: TextFormat) => Required<TextFormat>;
|
|
12
|
+
/**
|
|
13
|
+
* Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.
|
|
14
|
+
* @param format
|
|
15
|
+
* @returns Style string to set on context's `font` property. Note this __does not include
|
|
16
|
+
* the font color__ as that is not part of the CSS font value. Color must be handled separately.
|
|
17
|
+
*/
|
|
18
|
+
export declare const getTextStyle: ({ fontFamily, fontSize, fontStyle, fontVariant, fontWeight, }: TextFormat) => string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Word } from '../model';
|
|
2
|
+
/**
|
|
3
|
+
* Trims whitespace from the beginning and end of a `line`.
|
|
4
|
+
* @param line
|
|
5
|
+
* @param side Which side to trim.
|
|
6
|
+
* @returns An object containing trimmed characters, and the new trimmed line.
|
|
7
|
+
*/
|
|
8
|
+
export declare const trimLine: (line: Word[], side?: 'left' | 'right' | 'both') => {
|
|
9
|
+
/**
|
|
10
|
+
* New array containing what was trimmed from the left (empty if none).
|
|
11
|
+
*/
|
|
12
|
+
trimmedLeft: Word[];
|
|
13
|
+
/**
|
|
14
|
+
* New array containing what was trimmed from the right (empty if none).
|
|
15
|
+
*/
|
|
16
|
+
trimmedRight: Word[];
|
|
17
|
+
/**
|
|
18
|
+
* New array representing the trimmed line, even if nothing gets trimmed. Empty array if
|
|
19
|
+
* all whitespace.
|
|
20
|
+
*/
|
|
21
|
+
trimmedLine: Word[];
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "text-to-canvas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Render multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/stefcameron/text-to-canvas.git"
|
|
8
|
+
},
|
|
9
|
+
"author": "Stefan Cameron <stefan@stefcameron.com> (https://stefancameron.com/)",
|
|
10
|
+
"contributors": [
|
|
11
|
+
"Geon George <me@geongeorge.com> (https://geongeorge.com/)"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/stefcameron/text-to-canvas/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://stefcameron.github.io/text-to-canvas/",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"javascript",
|
|
20
|
+
"library",
|
|
21
|
+
"html",
|
|
22
|
+
"canvas",
|
|
23
|
+
"richtext",
|
|
24
|
+
"multiline",
|
|
25
|
+
"wrapping",
|
|
26
|
+
"es6",
|
|
27
|
+
"node"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"SECURITY.md"
|
|
36
|
+
],
|
|
37
|
+
"main": "./dist/text-to-canvas.min.js",
|
|
38
|
+
"types": "./dist/types/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
"types": "./dist/types/index.d.ts",
|
|
41
|
+
"node": {
|
|
42
|
+
"import": "./dist/text-to-canvas.mjs",
|
|
43
|
+
"require": "./dist/text-to-canvas.cjs"
|
|
44
|
+
},
|
|
45
|
+
"default": {
|
|
46
|
+
"import": "./dist/text-to-canvas.esm.min.js",
|
|
47
|
+
"require": "./dist/text-to-canvas.min.js",
|
|
48
|
+
"browser": "./dist/text-to-canvas.umd.min.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20.11.1",
|
|
53
|
+
"npm": ">=10"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "rm -rf ./dist && npm run build:browser && npm run build:node && npm run build:types",
|
|
57
|
+
"build:browser": "vite build --config config/vite.config.esm.mts && vite build --config config/vite.config.cjs.mts",
|
|
58
|
+
"build:node": "vite build --config config/vite.config.node.mts",
|
|
59
|
+
"build:types": "tsc src/lib/index.ts --declaration --emitDeclarationOnly --outDir dist/types",
|
|
60
|
+
"ci:build": "npm run build && npm run docs",
|
|
61
|
+
"ci:lint": "npm run lint",
|
|
62
|
+
"ci:test": "npm run test:unit && npm run demo:node",
|
|
63
|
+
"demo:node": "vite-node ./src/demos/node-demo.mts",
|
|
64
|
+
"docs": "vite build --config config/vite.config.docs.mts && prettier --write \"src/docs/*.d.ts\"",
|
|
65
|
+
"fmt": "prettier --write \"{*,config/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
|
|
66
|
+
"fmt:check": "prettier --check \"{*,config/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
|
|
67
|
+
"lint": "npm run lint:code && npm run lint:types && npm run fmt:check",
|
|
68
|
+
"lint:code": "eslint \"{*,config/**/*,src/**/*}.+(js|mjs|ts|mts|vue)\"",
|
|
69
|
+
"lint:types": "tsc",
|
|
70
|
+
"prepare": "npm run build",
|
|
71
|
+
"prepublishOnly": "npm run lint && npm run test:unit && npm run build",
|
|
72
|
+
"start": "vite serve --config config/vite.config.docs.mts",
|
|
73
|
+
"test": "npm run lint && npm run test:unit && npm run build",
|
|
74
|
+
"test:unit": "echo 'TODO: Add unit tests...'"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@types/lodash": "^4.17.0",
|
|
78
|
+
"@types/node": "^20.11.27",
|
|
79
|
+
"@types/offscreencanvas": "^2019.7.3",
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
81
|
+
"@typescript-eslint/parser": "^7.2.0",
|
|
82
|
+
"@vitejs/plugin-vue": "^5.0.4",
|
|
83
|
+
"element-plus": "^2.6.1",
|
|
84
|
+
"eslint": "^8.57.0",
|
|
85
|
+
"eslint-config-prettier": "^9.1.0",
|
|
86
|
+
"eslint-plugin-vue": "^9.23.0",
|
|
87
|
+
"lodash": "^4.17.21",
|
|
88
|
+
"prettier": "^3.2.5",
|
|
89
|
+
"typescript": "^5.4.2",
|
|
90
|
+
"unplugin-auto-import": "^0.17.5",
|
|
91
|
+
"unplugin-vue-components": "^0.26.0",
|
|
92
|
+
"vite": "^5.1.6",
|
|
93
|
+
"vite-node": "^1.3.1",
|
|
94
|
+
"vue": "^3.4.21"
|
|
95
|
+
},
|
|
96
|
+
"optionalDependencies": {
|
|
97
|
+
"canvas": "^2.11.2"
|
|
98
|
+
}
|
|
99
|
+
}
|