feat(text): Canvas bidirectional (RTL) text rendering#51
Open
chiefcll wants to merge 4 commits into
Open
Conversation
Render RTL/bidi text on the Canvas backend by leaning on the browser's built-in fillText bidi. When a node is `rtl`, each line is given an RTL base direction so mixed Hebrew/Latin/number runs reorder correctly and the paragraph reads right-to-left. Base direction is forced portably via an RLE/PDF control-char wrap because `ctx.direction` is Chrome 63+ while the runtime floor is Chrome 38; lines are anchored with `textAlign = 'left'` so the Phase 1 alignment flip still right-aligns them. Per-character letterSpacing is incompatible with bidi reordering (and native ctx.letterSpacing is Chrome 99+), so letterSpacing is forced to 0 for RTL text. Scope: Canvas backend, Hebrew + mixed LTR. SDF bidi and Arabic shaping remain out of scope. Full Docker visual comparison passes 171/171 (LTR path is pixel-identical). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document the `rtl` node flag (inheritance, override, the width caveat, and the mirroring formula) and Canvas bidirectional text rendering, including the known limitations (SDF/Arabic/letterSpacing). Includes the LTR/RTL layout diagram. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The rtl-text-canvas snapshot was flaky across environments: CI runs Playwright chromium directly on the GitHub runner (not the VRT Docker image), and the test used 'Ubuntu', which lacks Hebrew glyphs — so Canvas fell back to a system Hebrew font that differs between the snapshot machine and CI. Bundle Noto Sans Hebrew (OFL) and load it via installFonts so the Hebrew runs render from a controlled .ttf (FontFace) instead of an OS fallback, matching how every other deterministic snapshot works. Point the test (and docs example) at NotoSansHebrew and regenerate the certified snapshot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…manent Add a visual test exercising multi-line word-wrapped RTL paragraphs (right- aligned, reordered, with embedded LTR runs) and maxLines + overflowSuffix truncation, alongside LTR for contrast, using the bundled NotoSansHebrew font. Reword the SDF text limitation in the RTL guide from "not yet bidi-aware" to a permanent design decision: RTL text requires the Canvas renderer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of RTL support, stacked on #50 (targets
feat/rtl-layout-mirroring, notmain). Adds correct bidirectional text rendering on the Canvas backend for Hebrew + mixed LTR/RTL content, building on the Phase 1rtlflag.When a node is
rtl, each line is drawn with an RTL base direction so the browser's built-infillTextbidi reorders mixed Hebrew/Latin/number runs (e.g.שלום world 123→world 123 שלום,90 דקות→דקות 90) and the paragraph reads right-to-left, right-aligned (via the Phase 1 alignment flip).No new dependency; ~zero bundle cost.
Chrome 38 floor
ctx.directionis Chrome 63+, so it can't be relied on at the runtime floor. Base direction is instead forced portably by wrapping each line inRLE(U+202B)…PDF(U+202C) — zero-width control characters the browser's bidi engine honors on old and new browsers alike.ctx.directionis still set for modern browsers. Lines are anchored withtextAlign = 'left'so positioning is direction-independent.Known gaps (intentional)
letterSpacingis forced to 0 for RTL text: the per-char draw path defeats bidi reordering, and nativectx.letterSpacingis Chrome 99+.Test plan
tsc --buildand Prettier clean.examples/tests/rtl-text-canvas.ts(LTR vs RTL columns over mixed Hebrew/Latin/number strings) with a CI-certified Docker snapshot; reordering + right-alignment visually verified.textAlign/direction).🤖 Generated with Claude Code