{"id":17229102,"url":"https://github.com/stefcameron/text-to-canvas","last_synced_at":"2025-04-14T02:32:08.857Z","repository":{"id":225900325,"uuid":"767170196","full_name":"stefcameron/text-to-canvas","owner":"stefcameron","description":"Render multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping","archived":false,"fork":false,"pushed_at":"2025-04-09T14:55:31.000Z","size":18209,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-09T15:46:49.307Z","etag":null,"topics":["canvas","es6","html","javascript","library","multiline","node","richtext","wrapping"],"latest_commit_sha":null,"homepage":"https://stefcameron.github.io/text-to-canvas/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stefcameron.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-04T20:32:48.000Z","updated_at":"2025-04-09T14:55:34.000Z","dependencies_parsed_at":"2024-03-11T19:27:28.733Z","dependency_job_id":"4f0bbf21-2222-43c2-896a-3e93f73d0386","html_url":"https://github.com/stefcameron/text-to-canvas","commit_stats":null,"previous_names":["stefcameron/text-to-canvas"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefcameron%2Ftext-to-canvas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefcameron%2Ftext-to-canvas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefcameron%2Ftext-to-canvas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefcameron%2Ftext-to-canvas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stefcameron","download_url":"https://codeload.github.com/stefcameron/text-to-canvas/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248810916,"owners_count":21165200,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["canvas","es6","html","javascript","library","multiline","node","richtext","wrapping"],"created_at":"2024-10-15T04:46:00.491Z","updated_at":"2025-04-14T02:32:08.835Z","avatar_url":"https://github.com/stefcameron.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/stefcameron/text-to-canvas/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stefcameron/text-to-canvas/actions/workflows/ci.yml) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)\n\n# text-to-canvas\n\nRender multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping.\n\n## Origins and Differences\n\n🙌 This library would not exist were it not for all the work done by its original author, [Geon George](https://geongeorge.com/), in his [canvas-txt](https://github.com/geongeorge/Canvas-Txt) library.\n\nThe main feature that sparked `text-to-canvas` is a significant [update](https://github.com/geongeorge/Canvas-Txt/pull/95) to the original code base in order to support rich text formatting, which introduced the concept of a `Word` specifying both `text` and (optional) associated CSS-based `format` styles. A sentence is then simply a `Word[]` with/out whitespace (optionally inferred).\n\nPlain text (i.e. a `string`) is still supported as a convenience via the `drawText()`, `splitText()`, and `textToWords()` [APIs](#api).\n\nThe main differences (at `v1.0.0`) between `canvas-txt` and this library are:\n\n- Formal support for Node by `canvas-txt` vs this library's support solely focused on the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement).\n- This library's concerted effort to [support](#web-worker-and-offscreencanvas) [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and use of an [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), neither of which is formally supported by `canvas-txt`.\n\nThe feature gap may widen with future releases of both libraries.\n\nWhile there is a [Node](#node) [demo](./src/demos/node-demo.mts), it only works because the `node-canvas` library being used supports enough of the `HTMLCanvasElement`'s API, not because this library formally supports Node, or `node-canvas`.\n\n## Features\n\n- ✅ Rich text formatting (with the exception of words with different font _sizes_ not yet working well in terms of text baseline alignment)\n- ✅ Multiline text\n- ✅ Auto line breaks\n- ✅ Horizontal alignment\n- ✅ Vertical alignment\n- ✅ Justification\n- ✅ Optimized performance with support for Web Workers and `OffscreenCanvas`\n\n## Demo\n\nSee demo [here](https://stefcameron.github.io/text-to-canvas/).\n\n# Installation\n\n```bash\n$ npm install text-to-canvas\n# OR\n$ yarn add text-to-canvas\n```\n\n\u003e 💡 If this fails with a `node-pre-gyp` compilation error, please see [Compilation of the canvas package](#compilation-of-canvas-package) for help.\n\n## Compilation of canvas package\n\nThis project __optionally__ depends on the [canvas](https://github.com/Automattic/node-canvas) package which enables it to be used in a Node [demo](#node).\n\n\u003e ❗️ Note this is __optional__ as `text-to-canvas` does not formally support this library. This is purely for casual testing and as an example of how `text-to-canvas` should technically work with any library that supports the `HTMLCanvasElement` API. `text-to-canvas` only officially supports HTML `\u003ccanvas\u003e`.\n\nSince this package needs to be compiled for use on the platform on which you intend to install/use it, the author must either include pre-built binaries specific to your OS when they make a [release](https://github.com/Automattic/node-canvas/releases), or a new binary must be compiled by your package manager (i.e. `npm`) upon installation.\n\nIf you're installing on a newer Apple M1, M2, or M3 computer, or if you're using a version of Node newer than v20 (the latest LTS at time of writing), you may experience a `node-pre-gyp` failure because `canvas` doesn't provide pre-built binaries for the ARM64 architecture, only providing x86-64 (Intel x64) binaries for Node v20.\n\n\u003e ❗️ __Before installing text-to-canvas__, refer to the `canvas` [compilation](https://github.com/Automattic/node-canvas?tab=readme-ov-file#compiling) page for your OS/architecture, especially if you aren't on an Apple computer.\n\nFor Apple M computers (ARM64), this worked for me using [HomeBrew](https://brew.sh/) and [pyenv](https://github.com/pyenv/pyenv) to install additional compiler dependencies:\n\n```bash\n$ brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman\n$ pyenv install 3.12.1  # install Python 3.12 on which `cairo` depends\n$ pyenv local 3.12.1\n$ npm install  # should succeed\n```\n\n# Usage\n\nUse with a bundler (Webpack, Rollup, Vite, etc) or directly in a browser is supported.\n\nUse in Node is only supported to the extent that appropriate bundles are provided. Make sure you use a Node-base Canvas library that supports the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) API.\n\n## Bundler\n\nTwo bundles are provided for this type of target:\n\n- `./dist/text-to-canvas.esm.min.js` (ESM, `import`, ES2020+)\n- `./dist/text-to-canvas.min.js` (CJS, `require()`, ES2019+)\n\nUsed implicitly when using the library in a larger app bundled with a bundler like Webpack, Rollup, or Vite.\n\nDeclare a Canvas in your DOM (directly, via JSX, or other):\n\n```html\n\u003ccanvas id=\"my-canvas\" width=\"500\" height=\"500\"\u003e\u003c/canvas\u003e\n```\n\nCall the `drawText()` [API](#api):\n\n```javascript\nimport { drawText, Word } from 'text-to-canvas';\n\nconst canvas = document.getElementById('my-canvas');\nconst ctx = canvas.getContext('2d');\n\nctx.clearRect(0, 0, 500, 500);\n\n// plain text\nconst text = 'Lorem ipsum dolor sit amet';\n// OR with some formatting\nconst text: Word[] = [\n  { text: 'Lorem' },\n  { text: 'ipsum', format: { fontWeight: 'bold', fontColor: 'red' } },\n  { text: 'dolor', format: { fontStyle: 'italic' } },\n  { text: 'sit' },\n  { text: 'amet' },\n];\n\ndrawText(ctx, text, {\n  x: 100,\n  y: 200,\n  width: 200,\n  height: 200,\n  fontSize: 24,\n});\n```\n\nIf you need to know the total render height, `drawText()` returns it:\n\n```javascript\nconst { height } = drawText(...);\n```\n\n\u003e ⚠️ The library doesn't yet fully support varying font sizes, so you'll get best results by keeping the size consistent (via the [base font size](#drawtext-config)) and changing other formatting options on a per-`Word` basis.\n\n## Browser\n\nOne bundle is provided for this type of target:\n\n- `./dist/text-to-canvas.umd.min.js` (UMD, ES2019+)\n\nUsed implicitly when loading the library directly in a browser:\n\n```html\n\u003cbody\u003e\n  \u003ccanvas id=\"my-canvas\" width=\"500\" height=\"500\"\u003e\u003c/canvas\u003e\n  \u003cscript src=\"//unpkg.com/text-to-canvas\"\u003e\u003c/script\u003e\n  \u003cscript\u003e\n    const { drawText, getTextHeight, splitText } = window.textToCanvas;\n    /// ...remainder is the same\n  \u003c/script\u003e\n\u003c/body\u003e\n```\n\n## Node\n\nTwo bundles are provided for this type of target:\n\n- `./dist/text-to-canvas.mjs` (ESM/MJS, `import`, Node v20.11.1+)\n- `./dist/text-to-canvas.cjs` (CJS, `require()`, Node v20.11.1+)\n\n\u003e ⚠️ Other than the bundles, __Node is not formally supported by this library__, and neither is the `node-canvas` library used in the demo. Whatever \"Node Canvas\" library you use, make sure it supports the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) API and it _should_ work.\n\nUsed implicitly when importing or requiring the library in your Node scripts:\n\n```javascript\nimport { drawText } from 'text-to-canvas'; // MJS\n// OR\nconst { drawText } = require('text-to-canvas'); // CJS\n```\n\nSee Node demo in [./src/demo/node-demo.ts](https://github.com/stefcameron/text-to-canvas/blob/master/src/demos/node-demo.mts) for an example.\n\nYou can run this demo locally with `npm run node:demo`\n\n# API\n\n## drawText config\n\n![](./src/docs/canvas.jpg)\n\n|  Properties       |   Default    | Description                                                   |\n| :---------------: | :----------: | :-----------------------------------------------------------: |\n| `width`           | **Required** | Width of the text box.                                        |\n| `height`          | **Required** | Height of the text box.                                       |\n| `x`               | `0`          | X position of the text box.                                   |\n| `y`               | `0`          | Y position of the text box.                                   |\n| `align`           | `center`     | Text align. Other possible values: `left`, `right`.           |\n| `vAlign`          | `middle`     | Text vertical align. Other possible values: `top`, `bottom`.  |\n| `fontFamily`      | `Arial`      | Base font family of the text.                                 |\n| `fontSize`        | `14`         | Base font size of the text in px.                             |\n| `fontStyle`       | `''`         | Base font style, same as css font-style. Examples: `italic`, `oblique 40deg`. |\n| `fontVariant`     | `''`         | Base font variant, same as css font-variant. Examples: `small-caps`. |\n| `fontWeight`      | `'400'`      | Base font weight, same as css font-weight. Examples: `bold`, `100`. |\n| `fontColor`       | `'black'`    | Base font color, same as css color. Examples: `blue`, `#00ff00`. |\n| `strokeColor`     | `'black'`    | Base stroke color, same as css color. Examples: `blue`, `#00ff00`. |\n| `strokeWidth`     | `0`          | Base stroke width. Positive number; `\u003c=0` means none. Can be fractional. ⚠️ Word splitting does not take into account the stroke, which is applied on the __center__ of the edges of the text via the [strokeText()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeText) Canvas API. Setting a thick stroke will cause it to bleed out of the text box. |\n| `justify`         | `false`      | Justify text if `true`, it will insert spaces between words when necessary. |\n| `inferWhitespace` | `true`       | If whitespace in the text should be inferred. Only applies if the text given to `drawText()` is a `Word[]`. If the text is a `string`, this config setting is ignored. |\n| `overflow`        | `true`       | Allows the text to overflow out of the box if the box is too narrow/short to fit it all. `false` will clip the text to the box's boundaries. |\n| `debug`           | `false`      | Draws the border and alignment lines of the text box for debugging purposes. |\n\n## Functions\n\n```js\nimport {\n  drawText,\n  specToJson,\n  splitText,\n  splitWords,\n  textToWords,\n  wordsToJson,\n  getTextHeight,\n  getWordHeight,\n  getTextStyle,\n  getTextFormat,\n} from 'text-to-canvas'\n```\n\n\u003e ⚠️ Varying font sizes on a `Word` level (as given to `drawText()` or `splitWords()`) is not supported very well at this time. For best results, keep the font size consistent by relying on a single base font size as specified in the `drawText()` [config options](#drawtext-config).\n\n- `drawText()`: Draws text (`string` or `Word[]`) to a given Canvas.\n- `specToJson()`: Converts a `RenderSpec` to a JSON string. Useful for sending it as a message through `Worker.postMessage()`.\n- `splitText()`: Splits a given `string` into wrapped lines.\n    - This is just a convenience over `splitWords()` if you aren't needing rich text. It's only real value is that it will return the input text as an array of strings according to how the text would be wrapped on Canvas.\n- `splitWords()`: Splits a given `Word[]` into wrapped lines.\n- `textToWords()`: Converts a `string` into a `Word[]`. Useful if you want to then apply rich formatting to certain words.\n- `wordsToJson()`: Converts a `Word[]` to a JSON string. Useful for sending it as a message to a Worker thread via `Worker.postMessage()`.\n- `getTextHeight()`: Gets the measured height of a given `string` using a given text style.\n- `getWordHeight()`: Gets the measured height of a given `Word` using its text style.\n- `getTextStyle()`: Generates a CSS Font `string` from a given `TextFormat` for use with `canvas.getContext('2d').font`\n- `getTextFormat()`: Generates a \"full\" `TextFormat` object (all properties specified) given one with only partial properties using prescribed defaults.\n\nTypeScript integration should provide helpful JSDocs for every function and each of its parameters to further help with their use.\n\n# Examples\n\n## Web Worker and OffscreenCanvas\n\nIf you want to draw the text yourself, or even offload the work of splitting the words to a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) using an [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), you can use the `splitWords()` [API](#api) directly.\n\n\u003e This requires using `wordsToJson()` and `specToJson()` APIs to ensure all required information is properly transferred between the UI/main thread and the worker thread, particularly concerning the cached [TextMetrics](https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics).\n\n\u003cdetails\u003e\n\u003csummary\u003eSample code\u003c/summary\u003e\n\u003cbr/\u003e\n\nAdd a Canvas to your DOM:\n\n```html\n\u003ccanvas id=\"my-canvas\" width=\"500\" height=\"500\"\u003e\u003c/canvas\u003e\n```\n\nDefine a Web Worker, `worker.js`:\n\n```javascript\nimport { splitWords, specToJson } from 'text-to-canvas';\n\nconst wrapLines = ({ containerWidth, wordsStr, baseFormat }) =\u003e {\n  // NOTE: height doesn't really matter (aside from being \u003e0) as text won't be\n  //  constrained by it; just affects alignment, especially if you're wanting to\n  //  do bottom/middle vertical alignment; for top/left-aligned, height for\n  //  splitting is basically inconsequential\n  const canvas = new OffscreenCanvas(containerWidth, 100);\n  const context = canvas.getContext('2d');\n\n  const words = JSON.parse(wordsStr);\n  const spec = splitWords({\n    ctx: context,\n    words,\n    x: 0,\n    y: 0,\n    width: containerWidth,\n    align: 'left',\n    vAlign: 'top',\n    format: baseFormat,\n    // doesn't really matter (aside from being \u003e0) as long as you only want\n    //  top/left alignment\n    height: 100,\n  });\n\n  self.postMessage({\n    type: 'renderSpec',\n    specStr: specToJson(spec), // because of `TextMetrics` objects that fail serialization\n  });\n};\n\nself.onmessage = (event) =\u003e {\n  if (event.data.type === 'split') {\n    wrapLines(event.data);\n  }\n};\n\nexport {}; // make bundler consider this an ES Module\n```\n\nUse the Worker thread to offload the work of measuring and splitting the words:\n\n```typescript\nimport { Word, RenderSpec, TextFormat, wordsToJson, getTextStyle } from 'text-to-canvas';\n\nconst canvas = document.getElementById('my-canvas');\nconst ctx = canvas.getContext('2d');\n\nconst drawWords = (baseFormat: TextFormat, spec: RenderSpec) =\u003e {\n  const {\n    lines,\n    height: totalHeight,\n    textBaseline,\n    textAlign,\n  } = spec;\n\n  ctx.save();\n  ctx.textAlign = textAlign;\n  ctx.textBaseline = textBaseline;\n  ctx.font = getTextStyle(baseFormat);\n  ctx.fillStyle = baseFormat.fontColor;\n  ctx.strokeStyle = baseFormat.strokeColor;\n  ctx.lineJoin = 'round';\n\n  const baseStrokeWidth = baseFormat.strokeWidth\n\n  lines.forEach((line) =\u003e {\n    line.forEach((pw) =\u003e {\n      if (!pw.isWhitespace) {\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          if (pw.format.strokeColor) {\n            ctx.strokeStyle = pw.format.strokeColor;\n          }\n        }\n        ctx.fillText(pw.word.text, pw.x, pw.y);\n        // stroke AFTER fill so it goes on top\n        const lineWidth = typeof pw.format?.strokeWidth === 'number'\n          ? pw.format.strokeWidth\n          : baseStrokeWidth;\n        if (lineWidth \u003e 0) {\n          ctx.lineWidth = lineWidth;\n          ctx.strokeText(pw.word.text, pw.x, pw.y);\n        }\n        if (pw.format) {\n          ctx.restore();\n        }\n      }\n    });\n  });\n};\n\nconst words: Word[] = [\n  { text: 'Lorem' },\n  { text: 'ipsum', format: { fontWeight: 'bold', fontColor: 'red' } },\n  { text: 'dolor', format: { fontStyle: 'italic' } },\n  { text: 'sit' },\n  { text: 'amet' },\n];\n\nconst baseFormat: TextFormat = {\n  fontSize: 12,\n  fontFamily: 'Times New Roman',\n  fontColor: 'black',\n};\n\n// create a worker thread\nconst worker = new Worker('./worker.js', { type: 'module' });\n\n// queue the worker to split and measure the words as necessary for the\n//  specified width given base and any word-specific formatting\nworker.postMessage({\n  type: 'split',\n  containerWidth: 500,\n  wordsStr: wordsToJson(words),\n  baseFormat,\n});\n\n// listen for the \"done\" signal from the worker\nworker.onmessage = (event) =\u003e {\n  if (event.data?.type === 'renderSpec') {\n    worker.terminate();\n    const spec: RenderSpec = JSON.parse(event.data.specStr);\n\n    // render the spec (containing the `PositionedWord[]` with all the necessary\n    //  information)\n    drawWords(baseFormat, spec);\n  }\n};\n```\n\n\u003c/details\u003e\n\n# Help\n\n## Blurry text\n\nIf you're experiencing an issue where, \"the text quality rendered on the canvas appears lower than that rendered in the DOM across various device resolutions,\" this may be caused by __pixel density__ settings. See the discussion on [issue #69](https://github.com/stefcameron/text-to-canvas/issues/69#issuecomment-2136498781) for a possible solution.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefcameron%2Ftext-to-canvas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstefcameron%2Ftext-to-canvas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefcameron%2Ftext-to-canvas/lists"}