Canvas Commons v0.4.0
Say hello to Canvas Commons v0.4.0, the LaTeXt update! This release brings
improvements to the Latex component, adds Layout Animations, overhauls the
Txt component, and adds a new MediaBunny exporter. It also fixes some bugs,
resolves some issues with shaders, and deprecates the old ffmpeg exporter.
This is a "major" release for the library, but because we're still in the
v0.x.x range this is versioned as a minor release; issues may arise and things
may still be unstable. Use at your own risk, and report any issues you find on
GitHub or
Discord.
Special thanks to @xiaoxiae's help on the
MediaBunny exporter, @hikarikoda's help with
Shaders and Text Paths, and @0byte-coding's
CORS proxy and mouse coordinate improvements.
@TimerErTim,
@hikarikoda,
@0byte-coding, and
@xiaoxiae all contributed for their first time
on this release. Thank you for your help!
Breaking Changes and Migration Guide
Layouts and Text have changed underlying libraries and that means that some animations will have slight differences. Key changes:
View2D.shadowRootandLayout.element/Layout.stylesare removed; useLayout.yogaNodeandgetDomContainer()(for SVG / arc measurement) instead.TxtLeafnow extendsNodeinstead ofShape.- Direct
<TxtLeaf>styling no longer applies. Styles instead cascade from the parent<Txt>. FlexBasisdropscontent,min-content,max-content, andfit-content.LengthLimitdropsmax-contentandmin-content.textWrapnow defaults totrueso width-bounded<Txt>wraps by default.node.relativeTo(other)now returns coordinates in the other node's coordinate space. Usenode.position.abs().sub(b.position().abs())to get the old behavior.
Major Features and Tweaks
Exporter
The exporter has been replaced with a MediaBunny-backed webcodecs exporter. This
means about a 2x speedup in export times, and saves us from needing to do
complicated things to get ffmpeg installed on a system. It's a new option when
creating a project with npm create canvas-commons@latest, and can be added to
an existing project with npm install @canvas-commons/webcodecs and by adding
it to your vite.config.ts:
import {defineConfig} from 'vite';
import canvasCommons from '@canvas-commons/vite-plugin';
import webcodecs from '@canvas-commons/webcodecs';
export default defineConfig({
plugins: [canvasCommons(), webcodecs()],
});
Text
The Txt component is now much more capable. It's backed by pretext, which
helps us render and measure text without going through the DOM or a canvas
element. There are still some kinks we're working out, but I've found it much
more intuitive so far.
Text Paths
One of the most commonly requested Txt features is to draw text along a path.
This is now possible:
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const txt = createRef<Txt>();
const circle = createRef<Circle>();
view.add(
<>
<Circle ref={circle} size={200} />,
<Txt
ref={txt}
text="Hello, world!"
textPath={circle()}
pathOffset={435}
fill={v.get('--cc-text', '#cdd6f4')}
fontSize={24}
size={200}
position={view.size().mul(0.5).add([0, 100])}
cachePadding={100}
/>
,
</>,
);
yield* txt().pathOffset(435 + Math.PI * 200, 2);
});
Text Splitting
Another common request is to be able to animate individual letters or words.
Txt.split will explode a text node into standalone Txt nodes for each word,
letter, or sentence.
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const label = createRef<Txt>();
view.add(
<Txt ref={label} fontSize={64} fill={v.get('--cc-text', '#cdd6f4')}>
Split me into words
</Txt>,
);
const pieces = label().split('word');
view.add(<Node position={label().position()}>{pieces}</Node>);
label().opacity(0);
yield* sequence(0.15, ...pieces.map(piece => piece.y(-40, 0.3).back(0.3)));
yield* waitFor(0.3);
});
Justification, Hyphenation, and Knuth-Plass
textAlign={'justify'} does what you might expect, balancing the whitespace
between words on a line. wrapMode={'knuth-plass'} decides where to break the
lines. hyphenate takes a word splitter and enables soft breaks mid-word.
import ...
const text =
'This is some text, that evenly spreads its whitespace between words ' +
'while trying to avoid orphaned words and other badness when justified.';
function hyphenate(word: string): string[] {
if (word.length <= 6) return [word];
const parts: string[] = [];
for (let i = 0; i < word.length; i += 4) parts.push(word.slice(i, i + 4));
return parts;
}
export default makeScene2D(function* (view) {
const v = useScene().variables;
const fill = v.get('--cc-text', '#cdd6f4');
view.add(
<Layout layout gap={40}>
<Txt width={340} fontSize={28} fill={fill}>
{text}
</Txt>
<Txt
width={340}
fontSize={28}
fill={fill}
wrapMode={'knuth-plass'}
textAlign={'justify'}
hyphenate={() => hyphenate}
>
{text}
</Txt>
</Layout>,
);
yield* waitFor(1);
});
Exclusions
exclusions lets you give areas that the text will avoid. Useful for embedding
an image or other content in the text. Coordinates are measured from the
top-left of the text block, so a sibling's corner can be read straight from its
cardinal point signals.
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const body = createRef<Txt>();
const box = createRef<Rect>();
view.add(
<>
<Rect
ref={box}
position={[200, -30]}
size={[200, 90]}
radius={12}
lineWidth={4}
stroke={v.get('--cc-mauve', '#cba6f7')}
/>
<Txt
ref={body}
anchor={[-1, -1]}
position={[-400, -70]}
width={800}
fontSize={28}
fill={v.get('--cc-text', '#cdd6f4')}
textAlign={'left'}
exclusions={() => {
const center = box().position();
return [
{
kind: 'rect',
x: center.x,
y: center.y,
width: box().width(),
height: box().height(),
horizontalPadding: 24,
verticalPadding: 12,
},
];
}}
/>
</>,
);
yield* body().text(
'This is some text. Instead of running into the purple box, it wraps ' +
'around it, just like wrapping around a picture in an encyclopedia. ' +
'If the text gets long enough, it might even wrap underneath the box.',
2.5,
);
yield* waitFor(0.5);
});
Exclusions also accept a {kind: 'node', node: box} form as well, which can be
a little more convenient than manually creating a rect.
Fixes
In the process, we've also fixed a bunch of bugs and issues with text layout; RTL and CJK should work much better, and both Code and Txt should use far less memory and cause fewer GC pauses.
LaTeX
Latex now has new edit and replace methods that describe a change as one
raw template whose holes carry both states of the fragment that occupies them,
with morph, fade, and partialFade choosing how each one animates.
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const tex = createRef<Latex>();
view.add(
<Latex
ref={tex}
fontSize={52}
fill={v.get('--cc-text', '#cdd6f4')}
tex="1 + 2 + \cdots + n"
/>,
);
yield* waitFor(0.4);
yield* tex().edit(0.9)`1 + 2 + \cdots + n${fade('', ' = \\frac{n(n+1)}{2}')}`;
yield* waitFor(0.3);
yield* tex().replace(
0.9,
fade('n', '100'),
fade('n', '100'),
morph('n', '100'),
);
yield* waitFor(0.5);
});
Layout
Layout is now backed by Yoga instead of the DOM, one more step towards headless rendering.
Layout Animations
Layout.add, insert, and remove take an optional duration, animating any
changes to the layout instead of performing them immediately.
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const row = createRef<Layout>();
const fourth = createRef<Rect>();
const COLORS = [
v.get('--cc-red', '#f38ba8'),
v.get('--cc-yellow', '#f9e2af'),
v.get('--cc-green', '#a6e3a1'),
v.get('--cc-blue', '#89b4fa'),
v.get('--cc-purple', '#cba6f7'),
];
view.add(
<Layout ref={row} layout direction="row" gap={20}>
{COLORS.slice(0, 3).map(color => (
<Rect width={100} height={100} fill={color} radius={12} />
))}
</Layout>,
);
yield* row().insert(
<Rect ref={fourth} width={100} height={100} fill={COLORS[3]} radius={12} />,
1,
0.6,
);
yield* row().add(
<Rect width={100} height={100} fill={COLORS[4]} radius={12} />,
0.6,
);
yield* waitFor(0.2);
yield* fourth().remove(0.6);
yield* row().editLayout(0.6, n => {
n.direction('column');
n.alignItems('end');
});
yield* waitFor(0.2);
yield* row().editLayout(0.6, n => {
n.direction('row');
n.alignItems('center');
});
row().freezeLayout();
yield* row().children()[0].position.x(-260, 0.5).back(0.5);
yield* row().thawLayout(0.3);
});
layout on a <Layout> is now an alias for layoutSelf and layoutChildren,
and a new translate signal offsets a node visually without moving its
siblings.
Expanded Origin Transforms
The origin signals (top, left, bottomRight, and friends) and the
components of position and scale now get origin transforms, and have had
several issues fixed.
import ...
export default makeScene2D(function* (view) {
const v = useScene().variables;
const card = createRef<Rect>();
const marker = createRef<Circle>();
view.add(
<Layout position={[120, 40]} rotation={15}>
<Rect
ref={card}
size={[260, 160]}
radius={16}
fill={v.get('--cc-surface0', '#313244')}
/>
</Layout>,
);
view.add(
<Circle ref={marker} size={24} fill={v.get('--cc-mauve', '#cba6f7')} />,
);
marker().position(card().topRight.view());
yield* card().topLeft.view([-300, -160], 1);
yield* card().x.abs(0, 1);
yield* waitFor(0.3);
});
Package Access
Tired of Hunter taking forever to release changes to the main version? Now you
can run npm create @canvas-commons@next to get the latest changes right away.
Note that these may break without warning, so use at your own risk.
Full Changelog
New features 🎉
@canvas-commons/createOffer the WebCodecs video exporter in the render prompt, selected by default. The FFmpeg exporter is still available but is now deprecated.
#165
@canvas-commons/corechange the default color interpolation space from
lchtooklabCloses #120
#121
@canvas-commons/fiddleAdd
#183@canvas-commons/fiddle, a browser playground runtime. It compiles a scene in a worker, runs it in a sandboxed iframe against vendored copies ofcoreand2d, and edits it in CodeMirror with TypeScript completions and diagnostics.
@canvas-commons/2dAdd
#157Latex.edit, which describes a change as one raw template whose holes carry both states of the fragment that occupies them, withmorph,fadeandpartialFadechoosing how each one animates.
@canvas-commons/2dTween
#157<Latex>fragment by fragment, matching fragments by their sub-tex and their shapes by the glyph each renders, instead of pairing every shape in the formula by position. AddsfragmentTransitionto choose how a changed fragment animates anddebugFragmentsto outline and label each one.
@canvas-commons/2dAdd
#157Latex.replace, which matches edit fragments against the current formula so only the parts that change have to be written out.
@canvas-commons/2dLayout.add/insert/removeaccept an optionalduration. Without it they behave as before; with it they return aThreadGeneratorthat scales the child into or out of its slot while the surrounding flex layout reflows.#110yield * row().insert(<Rect width={100} height={100} fill="red" />, 1, 0.6);yield * row().children()[0].remove(0.6);
@canvas-commons/2dAdd
Layout.editLayout(duration, mutator)to animate a batch of layout properties (direction,wrap,alignItems,padding,gap, …) as one tween. Children glide from their pre-mutation positions to wherever the post-mutation layout places them.#110yield *row().editLayout(0.3, n => {n.direction('column');n.alignItems('end');});
@canvas-commons/2dAdd
Layout.freezeLayout/thawLayout.freezeLayoutsnaps children to their current visual positions and turnslayoutChildrenoff so they can be animated by hand;thawLayoutturns it back on and tweens children to their flex slots.#110row().freezeLayout();yield * row().children()[0].position.x(-200, 1).back(1);yield * row().thawLayout(0.3);
@canvas-commons/2dComponents of position, scale, and layout origin signals reach the coordinate spaces:
#185node.x.abs(value, 1),node.right.x.view(value, 1).relativeTo(node)on layout origin signals (left,right, ...) returns the curried signal that its type declares. Layout originabsandviewgive the correct point for nodes that are not at the origin of their parent.
@canvas-commons/2dSplit
#110layouton<Layout>intolayoutSelf(does this node participate in its parent's flex) andlayoutChildren(does this node lay out its own children). Both default tonulland fall back tolayout, so existing code is unaffected.Layout.applyLayoutandLayout.requestLayoutUpdateare now public.
@canvas-commons/2dAdd
Node.transitionTo(newParent, [index,] duration)andNode.morphTo(other, duration).transitionToreparents a node while tweening its world position, rotation, and opacity from the old slot to the new one.morphTocross-fades into a separate destination node.#110yield * card.transitionTo(grid, 0, 0.6);yield * button.morphTo(modal, 0.4);
@canvas-commons/2dAdd
#110translateto<Layout>— aVector2Signal(withtranslateX/translateYprops) that visually offsets the node without disturbing siblings, analogous to CSStransform: translate().
@canvas-commons/playerAdd a
#171pausedattribute to pause embedded animations, including autoplay.
@canvas-commons/editorfeat: Display mouse coordinates local to the selected node within the preview window.
#150
@canvas-commons/2dAdd
#123autoSizeto<Txt>— when enabled with a fixedwidthandheight, the text takes the largest whole-pixel size at or belowfontSizewhose layout fits the box. A line beside an exclusion has to fit the band the exclusion leaves, so there the size can be smaller than one that fits the box alone.
@canvas-commons/2dAdd
#123exclusionsto<Txt>— rect, polygon and node regions that text flows around, CSSshape-outside-style. Coordinates are Txt-local and center-origin; a rect'sx/yis its center. A{kind: 'node', node}entry reads its shape from a live node. A flex layout may place the text, the node, or both. A node that this text's own flow places throws.
@canvas-commons/2dReplace DOM-based layout and text with Yoga and Pretext.
Breaking:
View2D.shadowRootandLayout.element/Layout.stylesare removed; useLayout.yogaNodeandgetDomContainer()(for SVG / arc measurement) instead.TxtLeafnow extendsNode, notShape— direct<TxtLeaf>styling no longer applies; styles cascade from the enclosing<Txt>.FlexBasis/LengthLimitdrop the unsupported content-keyword variants,FlexContentadds'normal',FlexItemsadds'auto', andtextWrapnow defaults totrueso width-bounded<Txt>wraps without opt-in.Text breaks and spaces as CSS does: a CRLF is one line break, a space or tab next to a line break is dropped, and a space at a soft wrap hangs past the line, so a wrapped
<Txt>shrink-wraps to its ink. Runs of different sizes on one line share one baseline, and a word that a colour or font change cuts keeps the kerning of the whole word.Flex items keep the automatic minimum size CSS gives them: a
<Txt>in a row stays as wide as the widest word it lays out, a container keeps room for the text under it, and an unconstrained column item keeps its content height. SetminWidth={0}orminHeight={0}to shrink past that.A nested
#108<Txt>no longer lays out its own text: itssize()is the extent of the text it paints in the root paragraph, across every line it wraps onto, not the size of its text set alone on one line. It ignoreswidth,heightandpadding, and itstextLines(),textWords()andsplit()read the root's layout.
@canvas-commons/2dAdd support for inline non-text children inside
#123<Txt>— directLayoutchildren are placed in the text flow as atomic slots sized by their ownwidth/height, and a line may break on either side of one, as beside a replaced element in CSS.
@canvas-commons/2dAdd
#123textAlign={'justify'}and averticalAlignsignal to<Txt>— justify distributes line slack across word gaps and leaves a line that ends on a manual newline at its natural width, as CSS does;verticalAlignlines text up to the top, middle, or bottom of its box. A line wider than its box starts at its start edge and overflows past the other, whatever the alignment, as in CSS.
@canvas-commons/2dAdd text-on-a-path to
#123<Txt>:textPathlays a single line along SVG path data, aCurveProfile, or a liveCurvenode, withpathOffset,pathAlign,pathSmoothness, andpathSplitcontrolling placement, cross-path alignment, and shaping.
@canvas-commons/2d
#187Txtwraps text around a closedtextPathwhenpathOffsetloops around the end. Open paths still clip.
@canvas-commons/2dAdd
#123textWords/textGlyphs/textSentencesquery accessors to<Txt>that return per-unit text and positions in Txt-local coordinates.
@canvas-commons/2dAdd
#123Txt.split(granularity)— explode a text node into one standalone, style-matchedTxtper grapheme, word, or sentence, each positioned to reproduce the source render exactly (kerning included) so the pieces can be animated independently.
@canvas-commons/2dAdd
#123wrapMode,hyphenateandoverflowWrapto<Txt>— opt into Knuth-Plass line breaking and bring-your-own soft-hyphen insertion.overflowWrap={'anywhere'}breaks a word wider than the line at any grapheme; the default'normal'keeps it whole, as CSSoverflow-wrapdoes.
@canvas-commons/webcodecsAdd
#119@canvas-commons/webcodecs, a self-contained in-browser video exporter. It encodes video and audio entirely in the browser via Mediabunny and the Web Audio API.
@canvas-commons/vite-pluginPre-bundle
#108yoga-layoutand@chenglou/pretextso the dev-time dep optimizer doesn't discover them mid-session and force a page reload.
Fixed bugs 🐛
@canvas-commons/coreThe audio preview gain uses the amplitude formula (
#16710^(db/20)), matching the exporters.
@canvas-commons/2dWait for a web font that a text node requests before the scene draws, so the first frame no longer uses fallback metrics.
#173
@canvas-commons/2dCancelling a
#190Code.codereplace,append, orprependtween holds the code at its current progress and avoids memory growth.
@canvas-commons/2dCreating many
#190Codenodes uses less memory.
@canvas-commons/coreAdd Intl.Segmenter types to the shared tsconfig so projects extending it typecheck.
#179
@canvas-commons/coreStop webpack from resolving the dynamic plugin import, which removes its critical dependency warning.
#182
@canvas-commons/createdeclare kleur as a direct dependency so scaffolding works under pnpm's strict node_modules layout instead of relying on npm hoisting it from prompts.
#115
@canvas-commons/2dRespect aspect ratios on standalone nodes when only width or height is set.
#168
@canvas-commons/2d, @canvas-commons/coreCollect a yielded node's asynchronous resources inside its scene, so an icon that finishes loading while the scene waits for it renders.
#177
@canvas-commons/2dKnuth-Plass wrapping treats manual newlines as mandatory breaks instead of optional break candidates.
#158
@canvas-commons/2dKnuth-Plass wrapping no longer plans a last line, or any line of text that is not justified, wider than the node. A word wider than the node now gets its own line instead of collapsing the whole paragraph onto one line.
#175
@canvas-commons/2d
#179Latexno longer takes its size from the x-height of the page font.fontSizenow equals the size of the math font itself, so existingLatexnodes render about 10-15% smaller in the editor.
@canvas-commons/2dFix
#157<Latex>fragments claiming glyphs that belong to a neighbouring fragment when MathJax does not emit their own contiguously, and repair fragments that a split leaves without the argument or the\endtheir commands need instead of reporting them as invalid MathJax.
@canvas-commons/2dConcurrent-tween fixes for
<Latex>,<Txt>, and<Layout>.padding,margin, andgaptweens now compose withsizetweens on the same node;Latex.tweenTextweens the container size alongside the fragment morph;Txttext tweens stay smooth whenfontSizeanimates at the same time.
#110Layout.lockLayout/releaseLayoutreplacelockSize/releaseSize.
@canvas-commons/2dA percent size on a node outside of a layout resolves against the nearest ancestor that has a size or a layout.
#192
@canvas-commons/2dShaders receive the
#191frameuniform.
@canvas-commons/2dText-heavy scenes avoid memory growth from disposed nodes and repeated measurements.
#190
@canvas-commons/2dKeep the shear of an SVG matrix transform, and read SVG presentation properties from the
#169styleattribute as well as the matching attribute.
@canvas-commons/2dA
#190Txtreuses its text leaves when reactive string children change.
@canvas-commons/2d
#190Txtrenders the string representation of non-stringtextvalues supplied by untyped code.
@canvas-commons/2d
#190Txtrenders empty text when its value resolves tonullorundefined.
@canvas-commons/2d
#190Txtretains its text leaf when a reactivetextvalue changes.
@canvas-commons/2dRight-to-left text renders at the positions reported by its layout.
#190
@canvas-commons/2dA
#174<Txt>text tween whose target changes while it runs now re-wraps at the box width instead of laying the rest of the tween out on one line.
@canvas-commons/2dKeep
#158textWrapactive during text tweens onTxtnodes with a fixed width, a percent width or amaxWidth.
@canvas-commons/2d
#190Txtwraps at a resolved layout width of zero instead of painting one line.
@canvas-commons/2d
#155<Video>sources are now routed through the CORS proxy, the same as<Img>sources. Remote videos no longer taint the canvas when the proxy is enabled.
Other changes
@canvas-commons/2dHonor a caller-supplied interpolation function when tweening
#157<Path>data, and use it for the path morphs in<SVG>and<Latex>so they tween through their signals.
Check out the Update Guide for information on how to update your existing projects.