Skip to content

feat(text): Canvas bidirectional (RTL) text rendering#51

Open
chiefcll wants to merge 4 commits into
feat/rtl-layout-mirroringfrom
feat/rtl-canvas-bidi-text
Open

feat(text): Canvas bidirectional (RTL) text rendering#51
chiefcll wants to merge 4 commits into
feat/rtl-layout-mirroringfrom
feat/rtl-canvas-bidi-text

Conversation

@chiefcll
Copy link
Copy Markdown
Contributor

@chiefcll chiefcll commented Jun 1, 2026

Summary

Phase 2 of RTL support, stacked on #50 (targets feat/rtl-layout-mirroring, not main). Adds correct bidirectional text rendering on the Canvas backend for Hebrew + mixed LTR/RTL content, building on the Phase 1 rtl flag.

When a node is rtl, each line is drawn with an RTL base direction so the browser's built-in fillText bidi reorders mixed Hebrew/Latin/number runs (e.g. שלום world 123world 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.direction is Chrome 63+, so it can't be relied on at the runtime floor. Base direction is instead forced portably by wrapping each line in RLE(U+202B)…PDF(U+202C) — zero-width control characters the browser's bidi engine honors on old and new browsers alike. ctx.direction is still set for modern browsers. Lines are anchored with textAlign = 'left' so positioning is direction-independent.

Known gaps (intentional)

  • SDF text still renders in logical order (unchanged) — SDF bidi is deferred.
  • Arabic shaping (contextual letter joining) is out of scope; this targets Hebrew + mixed LTR.
  • letterSpacing is forced to 0 for RTL text: the per-char draw path defeats bidi reordering, and native ctx.letterSpacing is Chrome 99+.

Test plan

  • 254 unit tests pass; tsc --build and Prettier clean.
  • New visual test 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.
  • Full Docker visual comparison: 171/171 snapshots pass — the LTR text path is pixel-identical (no regression from setting textAlign/direction).

🤖 Generated with Claude Code

chiefcll and others added 4 commits May 31, 2026 22:08
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>
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