Skip to content

Commit

Permalink
Personalize the docs app and add Web Worker example
Browse files Browse the repository at this point in the history
  • Loading branch information
stefcameron committed Mar 8, 2024
1 parent c214485 commit 0aacdd2
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 32 deletions.
137 changes: 136 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Render multiline plain or rich text into textboxes on HTML5 Canvas with automati

## Origins

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.
🙌 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.

The main feature that sparked `text-to-canvas` is a significant update to (departure from) 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).

Expand Down Expand Up @@ -85,6 +85,141 @@ const { height } = drawText(...);

> ⚠️ 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.
## Web Worker and OffscreenCanvas

If 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.

> This requires using `wordsToJson()` and `specToJson()` APIs to ensure all required information is properly transferred between the UI/main thread and the worker thread.
Add a Canvas to your DOM:

```html
<canvas id="my-canvas" width="500" height="500"></canvas>
```

Define a Web Worker, `worker.js`:

```javascript
import { splitWords, specToJson } from 'text-to-canvas';

const wrapLines = ({ containerWidth, wordsStr, baseFormat }) => {
// NOTE: height doesn't really matter (aside from being >0) as text won't be
// constrained by it; just affects alignment, especially if you're wanting to
// do bottom/middle vertical alignment; for top/left-aligned, height for
// splitting is basically inconsequential
const canvas = new OffscreenCanvas(containerWidth, 100);
const context = canvas.getContext('2d');

const words = JSON.parse(wordsStr);
const spec = splitWords({
ctx: context,
words,
x: 0,
y: 0,
width: containerWidth,
align: 'left',
vAlign: 'top',
format: baseFormat,
// doesn't really matter (aside from being >0) as long as you only want
// top/left alignment
height: 100,
});

self.postMessage({
type: 'renderSpec',
specStr: specToJson(spec), // because of `TextMetrics` objects that fail serialization
});
};

self.onmessage = (event) => {
if (event.data.type === 'split') {
wrapLines(event.data);
}
};

export {}; // make bundler consider this an ES Module
```

Use the Worker thread to offload the work of measuring and splitting the words:

```typescript
import { Word, RenderSpec, TextFormat, wordsToJson, getTextStyle } from 'text-to-canvas';

const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');

const drawWords = (baseFormat: TextFormat, spec: RenderSpec) => {
const {
lines,
height: totalHeight,
textBaseline,
textAlign,
} = spec;

ctx.save();
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = getTextStyle(baseFormat);
ctx.fillStyle = baseFormat.fontColor;

lines.forEach((line) => {
line.forEach((pw) => {
if (!pw.isWhitespace) {
if (pw.format) {
ctx.save();
ctx.font = getTextStyle(pw.format);
if (pw.format.fontColor) {
ctx.fillStyle = pw.format.fontColor;
}
}
ctx.fillText(pw.word.text, pw.x, pw.y);
if (pw.format) {
ctx.restore();
}
}
});
});
};

const words: Word[] = [
{ text: 'Lorem' },
{ text: 'ipsum', format: { fontWeight: 'bold', color: 'red' } },
{ text: 'dolor', format: { fontStyle: 'italic' } },
{ text: 'sit' },
{ text: 'amet' },
];

const baseFormat: TextFormat = {
fontSize: 12,
fontFamily: 'Times New Roman',
fontColor: 'black',
};

// create a worker thread
const worker = new Worker('./worker.js', { type: 'module' });

// queue the worker to split and measure the words as necessary for the
// specified width given base and any word-specific formatting
worker.postMessage({
type: 'split',
containerWidth: 500,
wordsStr: wordsToJson(words),
baseFormat,
});

// listen for the "done" signal from the worker
worker.onmessage = (event) => {
if (event.data?.type === 'renderSpec') {
worker.terminate();
const spec: RenderSpec = JSON.parse(event.data.specStr);

// render the spec (containing the `PositionedWord[]` with all the necessary
// information)
drawWords(baseFormat, spec);
}
};
```

## Node

Two bundles are provided for this type of target:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "git",
"url": "git+https://github.com/stefcameron/text-to-canvas.git"
},
"author": "Stefan Cameron <[email protected]> (https://stefcameron.com/)",
"author": "Stefan Cameron <[email protected]> (https://stefancameron.com/)",
"contributors": [
"Geon George <[email protected]> (https://geongeorge.com/)"
],
Expand Down
47 changes: 32 additions & 15 deletions src/docs/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,58 @@ const year = new Date().getFullYear();
<div class="common-layout">
<ElContainer>
<ElHeader class="container">
<h1>Canvas-Txt: Text on HTML5 Canvas</h1>
<h1>Rich text on HTML5 Canvas with text-to-canvas</h1>
<p>
A tiny, zero external dependency library that makes it easier to
render multiline text on HTML5 Canvas
Render multiline plain or rich text into textboxes on HTML5 Canvas
with automatic line wrapping, in the browser or in Node.
</p>
<p>
Read docs on
<a href="https://github.com/geongeorge/Canvas-Txt/">Github</a>
Read the docs on
<a href="https://github.com/stefcameron/text-to-canvas/">Github</a>.
</p>
</ElHeader>
<ElMain class="container">
<AppCanvas />

<br />
<br />
</ElMain>
<ElHeader class="container">
<div class="flex">
<div class="flex-1">
<h1>Browser and Node.js Support</h1>
<p>Works with all modern browsers and Node.js with node-canvas.</p>
<h2>Supports browsers, Web Workers, and Node</h2>
<p
>Works with all modern browsers, as well as Node with
<code>node-canvas</code>.</p
>
<p>
Star on
<a href="https://github.com/geongeorge/Canvas-Txt/">Github</a>
Also works with
<a
href="https://github.com/stefcameron/text-to-canvas?tab=readme-ov-file#web-worker-and-offscreencanvas"
>Web Workers</a
>
and <code>OffscreenCanvas</code>.
</p>
</div>

<div class="flex-1">
<img src="./featured-2.png" class="image" />
<h2>Plain or rich text rendering</h2>
<p>
The main <code>drawText()</code>
<a
href="https://github.com/stefcameron/text-to-canvas?tab=readme-ov-file#api"
>API</a
>
accepts either a plain string or an array of
<code>Word</code> objects. Define your base formatting options and
then use a string to render everything the same, or use
<code>Word</code> objects to apply overrides to the base
formatting as necessary.
</p>
</div>
</div>
</ElHeader>
<ElFooter>
<div class="footer-text">
{{ year }} | <a href="https://geongeorge.com">Geon George</a>
Copyright (c) {{ year }}
<a href="https://stefancameron.com">Stefan Cameron</a> |
<a href="https://github.com/stefcameron/text-to-canvas/">Github</a>
</div>
</ElFooter>
</ElContainer>
Expand Down
22 changes: 10 additions & 12 deletions src/docs/AppCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,21 @@ function renderText() {
y: config.pos.y,
width: config.size.w,
height: config.size.h,
fontFamily:
"Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue'",
fontFamily: 'Times New Roman, serif',
fontSize: 24,
fontWeight: '100',
// fontStyle: 'oblique',
// fontVariant: 'small-caps',
debug: config.debug,
fontWeight: '400',
align: config.align,
vAlign: config.vAlign,
justify: config.justify,
debug: config.debug,
};
const words = textToWords(config.text);
words.forEach((word) => {
if (word.text === 'ipsum') {
word.format = { fontStyle: 'italic', fontColor: 'red' };
} else if (word.text === 'consectetur') {
word.format = { fontWeight: '400', fontColor: 'blue' };
word.format = { fontWeight: 'bold', fontColor: 'blue' };
}
});
Expand All @@ -84,7 +81,7 @@ function redrawAndMeasure() {
renderTime.value = t1 - t0;
// eslint-disable-next-line no-console
console.log(`Rendering took ${renderTime.value} milliseconds.`);
console.log(`Rendering took ${renderTime.value} milliseconds`);
}
const debouncedRedrawAndMeasure = debounce(redrawAndMeasure, 10);
Expand Down Expand Up @@ -117,12 +114,13 @@ onMounted(() => {
placeholder="Please input"
/>
<p>
Canvas-txt uses the concept of textboxes borrowed from popular image
editing softwares. You draw a rectangular box then place the text in
the box. Turn on the debug mode (below) to see what is happening.
The library uses the concept of textboxes borrowed from popular image
editing software. You draw a rectangular box then place the text in
the box. Turn on <strong>debug mode</strong> (below) to see what is
happening.
</p>
<p>
To keep the demo app simple while showing Canvas-txt's rich text
💬 To keep the demo app simple while showing the library's rich text
features, the word "ipsum" is always rendered in italics/red and the
word "consectetur" always in bold/blue.
</p>
Expand Down
4 changes: 1 addition & 3 deletions src/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Canvas-Txt | Render multiline text on HTML5 Canvas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Render multiline rich text on HTML5 Canvas | text-to-canvas</title>
</head>

<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>

<script type="module" src="./index.js"></script>
</body>

Expand Down
1 change: 1 addition & 0 deletions src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface TextFormat {
/** Font variant (CSS value). */
fontVariant?: 'normal' | 'small-caps' | '';

/** CSS color value. */
fontColor?: string;

// NOTE: line height is not currently supported
Expand Down

0 comments on commit 0aacdd2

Please sign in to comment.