Skip to content

feat(text): shadows for laid-out text, one coverage surface per paragraph - #285

Merged
sidorares merged 1 commit into
masterfrom
claude/ntk-issue-283-cec41d
Aug 18, 2026
Merged

feat(text): shadows for laid-out text, one coverage surface per paragraph#285
sidorares merged 1 commit into
masterfrom
claude/ntk-issue-283-cec41d

Conversation

@sidorares

@sidorares sidorares commented Aug 18, 2026

Copy link
Copy Markdown
Owner

drawGlyphs now consults the shadow state, so text drawn through a
TextLayout casts a shadow exactly as fillText does. It was the one text
path that ignored it — 8.1.0 wired _shadowed into seven drawing operations
and drawGlyphs was not among them — so the feature was missing exactly
where a drawing wraps, which is where a paragraph has to go through
layout() in the first place.

ctx.shadowBlur = 8;
ctx.shadowColor = '#000';
ctx.fillText('one line', x, y);   // shadow, before and after
layout.draw(ctx, x, y);           // shadow only after this

before and after

Rendered by this branch, headless into a pixmap: the same specimen with
the glyph-path shadow suppressed, then with it. The headings are fillText,
which was shadowed already.

How

  • One coverage surface per call, not one per line. The runs already
    carry their own baselines, so they all go into the same padded a8
    surface and one blur, the same way they already go into one glyph
    composite. A paragraph whose spans change colour is drawn as several
    calls and casts a shadow per call — as consecutive fillTexts would.
  • No re-shaping. _shadowOfText shapes the string again to size its
    surface; the layout case is handed its runs, so it measures them
    directly. New positionedRunsInk in lib/text/glyphs.js is the run-shaped
    counterpart of the context's _shapedInk.
  • Cached on the runs, not on a string. The key is each run's identity —
    a shaped run is immutable and a layout keeps the ones its lines are made
    of, so a small integer per object names its glyphs at O runs rather than
    O glyphs — plus the run positions relative to the first. Two paragraphs
    of the same text at different widths therefore key differently, and
    re-drawing one layout anywhere on the target is a lookup and a composite.
    Runs rebuilt every frame have nothing stable to key on and rebuild, which
    is the honest answer.
  • Over maxPixels the shadow falls back to the clipped, uncached path,
    sized to the part that can actually be seen — the same escape the
    fillText path takes.

Tests

test/shadow.test.js grows a suite against the pure-JS X server: the same
string drawn through fillText and through layout() produces the same
amount of shadow, every line of a wrapped paragraph is shadowed, the
transform carries the shadow with the text, a wrapped paragraph is one
retained surface reused across draws while another width is a second, and a
transparent shadowColor still does no work at all. The first three fail on
master.

Docs

docs/context-2d.md lists drawGlyphs among the shadowed operations and
describes the layout cache and the per-call rule; docs/text.md says it on
both TextLayout.draw and the glyph-run contract.

Closes #283

…raph

`drawGlyphs` now consults the shadow state, so text drawn through a
`TextLayout` casts a shadow exactly as `fillText` does. It was the one
text path that ignored it — 8.1.0 wired `_shadowed` into seven drawing
operations and `drawGlyphs` was not among them, so a shadow became
unavailable exactly where a drawing wraps.

The runs of one call become one blurred a8 coverage surface, whatever the
line count, painted under the glyphs. Nothing is re-shaped: the caller
already has the runs, which is what makes this cheaper than the `fillText`
path rather than dearer. The cache is keyed on run identity plus the run
positions relative to the first, so re-drawing a layout anywhere on the
target is a lookup, while the same words wrapped to another width are a
different shadow.

Closes #283
@sidorares
sidorares merged commit 3c33112 into master Aug 18, 2026
6 checks passed
@sidorares
sidorares deleted the claude/ntk-issue-283-cec41d branch August 18, 2026 06:54
sidorares pushed a commit that referenced this pull request Aug 18, 2026
🤖 I have created a release *beep* *boop*
---


## [8.2.0](v8.1.1...v8.2.0)
(2026-08-18)


### Features

* **text:** shadows for laid-out text, one coverage surface per
paragraph ([#285](#285))
([3c33112](3c33112)),
closes [#283](#283)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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.

text: a laid-out paragraph casts no shadow — drawGlyphs is not one of the operations that consults the shadow state

1 participant