-
Notifications
You must be signed in to change notification settings - Fork 49
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 #95
Conversation
__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.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Hi @stefcameron Thank you so much for this PR. |
You're welcome, and I'm glad you like the updated demo! 😄 When making changes to the API, I tried my best to maintain compatibility for the 3 exported functions, but as I mentioned, I wasn't able to. I had to introduce a small change, but that's still a breaking change, unfortunately. So I'd recommend publishing these changes under a new major version. I also exported some additional functions to help with converting a string to a And finally, I exported the new Please LMK if you have any questions about the changes I made to the API. |
@geongeorge Are there any specific questions about the API changes I could answer to help you with your review? |
Thank you for considering my changes. I realize this is a huge PR and hard to take in, but I do need to get it captured and published. On Monday (Mar 4), I will close this PR and start work on publishing it to a new package from a new repo so that these improvements can be captured more permanently. |
@stefcameron Thank you for your pull request. Currently, I don't have the availability to review these changes before the specified date. Please feel free to release a new package :) |
Published as [email protected] |
@stefcameron Amazing work! Wonderful name as well :) |
@geongeorge From one OSS maintainer to another, I apologize for the sheer size of this PR and the extent of the changes. I know it may be daunting, maybe even frustrating. I would have made smaller PRs over time, but I was time-crunched for a POC and your library was the perfect foundation to achieve what I needed: Wrapped rich text rendering on canvas, with the ability to do all calculations on an
OffscreenCanvas
in a Web Worker thread.I tried my very best to keep the existing interface to
splitText()
but couldn't, so this is also a breaking change (major version...), 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.I wanted to give you an opportunity to consider taking in these changes, but I totally understand if this is too much and you don't care for them. If that's the case, please let me know and I'll publish this to my own new
canvas-rich-text
package with credits to you and your previous contributors.Adds rich text and color support by introducing the concept of "Words". Giving a simple string to
drawText()
will now, internally, convert it to aWord[]
array. EachWord
can have aformat
and can also prime thesplitWords()
calculations with previously-calculatedmetrics
to improve performance (meaningcontext.measureText()
will be skipped for thatWord
).Moves the bulk of the code out of
drawText()
and intosplitWords()
(which is now called by the existingsplitText()
) so thatsplitWords()
returns a "render spec" with aPositionedWord[]
array and alldrawText()
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. EachWord
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
Node demo output