Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rich text and color support #1

Merged
merged 15 commits into from
Mar 4, 2024
Merged

Add rich text and color support #1

merged 15 commits into from
Mar 4, 2024

Conversation

stefcameron
Copy link
Owner

For posterity, this was original proposed as geongeorge/Canvas-Txt#95 but was too big of a change to take in. So it will now be published as a new package.

I tried my very best to keep the existing interface to splitText() but couldn't, so this is a breaking change from the original canvas-txt library's API, though not a huge one. I also changed the output file names since the .mjs was causing issues with our Webpack build and .js was the path of least resistance.


Adds rich text and color support by introducing the concept of "Words". Giving a simple string to drawText() will now, internally, convert it to a Word[] array. Each Word can have a format and can also prime the splitWords() calculations with previously-calculated metrics to improve performance (meaning context.measureText() will be skipped for that Word).

Moves the bulk of the code out of drawText() and into splitWords() (which is now called by the existing splitText()) so that splitWords() returns a "render spec" with a PositionedWord[] array and all drawText() needs to do now is loop over the words and draw them on the canvas at the pre-calculated positions.

As an easy bonus, while color has no effect on word splitting, drawText() now supports it. Each Word can have its own color.

I enhanced the demo to always render two words in a different color and font style.

The only thing that remains a "TODO" is handling different font sizes. It will render, but not nicely, because the code still assumes the text baseline is always the same while it probably needs to be different, and the words need to be aligned to it in some way. But sticking with a single font size and a single font family, the output is nice with different colors, bolded/italic text, etc.

Text still wraps and aligns as before.

Web demo

CanvasTxt-richtext-demo

Node demo output

output

__WARNING:__ Still a WIP. See `// DEBUG` comments throughout.

Instead of dealing with text as a string, introduce the concept of Words
where a Word is either a visible, written word, or whitespace, and have
the option to infer whitespace when an array of Words is given instead
of a string.

A Word then has its own formatting options aside from the "base" formatting
set in the `CanvasTextConfig` object. If a Word doesn't have `format` options,
it will simply use the "default" format in the 2D context, which may have been
modified based on the `CanvasTextConfig` object provided to `drawText()`.

To preserve the existing `drawText()` and `splitText()` APIs, `splitText()`
splits the text into an array of Words and then calls the new `splitWords()`
function that does all the work.

For now, providing an array of Words to `drawText()` throws an error since
this isn't supported yet. But everything else workds as before when providing
a string: All supported alignments, including justification.

Other fixes/changes:
- Breaks cannot occur within a word anymore, even when the box width gets
  too narrow to fit anything. At this point, a minimum of one word is rendered
  per line, on top of any hard line breaks specified with newline characters.
- The context's state is saved and restored in `drawText()`, meaning that
  after the call, the context's font, etc, styles/settings are back to what
  they were prior to calling it instead of being set to what was specified
  in the `CanvasTextConfig` object.
__WARNING:__ See remaining `// DEBUG` comments.

In order to make it possible to use a library other than canvas-txt to
actually draw the text, yet still leverage this library's text wrapping
and rich text features, `splitWords()` now does all the positioning
that `drawText()` use to do, which means that `splitText()`, which calls
`splitWords()` internally, must now be given many more parameters than
it used to.

Since `splitText()` was already exported as a utility from this library,
this must be a breaking change (major version update).

So now, `splitWords()` returns the positioned words along with the
required `textAlign` and `textBaseline` to set on the 2D context
in order to render the text what is now a very simple loop.

This makes it possible to call `splitWords()` directly and then use
the output to render each word using another library such as Konva
where each word could then be dragged or hit-tested, or do it
without a library altogether.

Plain text (single text format) works at this point, but rich text
still does not render properly due to misalignment issues.
__WARNING:__ See `// DEBUG` comments for what still needs doing/fixing.

Works quite well now, especially when NOT using different font sizes
for different words. If all words use the same size, just alter their
style, weight, or variant, it renders nicely.

Updated the demo app to always set "ipsum" to italic, and "consectetur"
to bold, if either is found in the sample text to render.
__WARNING:__ There are still remaining `// DEBUG` items to address

- Performance: Allow consumer to provide pre-computed `TextMetrics` objects
  in the `Words[]`, which would typically come from a previous call to
  `splitWords()` since the function now sets a new `Word.metrics` property
  to the measured metrics for the `Word.format` (if any). This will
  dramatically speed up the process when most Words have already been
  measured from a previous pass (assuming formatting hasn't changed).
- Performance: Change `WordMap` key to a string hash of the Word's
  `text` and `format` in order to reuse associated metrics between
  identical Words. Also impacts memory use.
- Memory: Re-use metrics between identical Words instead of re-measuring
  them unnecessarily. This is particularly significant with whitespace
  characters which show up in __huge__ numbers in any given set of Words
  and are nearly all identical.
Native Canvas `TextMetrics` objects fail to serialize (cause an exception)
when given as data to the `postMessage()` of a Web Worker thread (or from
the main thread).

Therefore, work with objects having a similar interface but not being
actual `TextMetrics` instances.

Also provide two JSON stringify helpers: `specToJson()` and `wordsToJson()`
This should help consumers to deal with native `TextMetrics` objects.
…ype, remove self as contributor from package
Node-based `canvas` rendering does not currently support the newer
`fontBoundingBox*` TextMetrics properties, so use a `textBaseline='bottom'`
fallback trick in that case.
@stefcameron stefcameron merged commit 478d621 into master Mar 4, 2024
1 check passed
@stefcameron stefcameron deleted the richtext branch March 4, 2024 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant