feat(text): shadows for laid-out text, one coverage surface per paragraph - #285
Merged
Conversation
…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
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>
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.
drawGlyphsnow consults the shadow state, so text drawn through aTextLayoutcasts a shadow exactly asfillTextdoes. It was the one textpath that ignored it — 8.1.0 wired
_shadowedinto seven drawing operationsand
drawGlyphswas not among them — so the feature was missing exactlywhere a drawing wraps, which is where a paragraph has to go through
layout()in the first place.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
carry their own baselines, so they all go into the same padded
a8surface 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._shadowOfTextshapes the string again to size itssurface; the layout case is handed its runs, so it measures them
directly. New
positionedRunsInkinlib/text/glyphs.jsis the run-shapedcounterpart of the context's
_shapedInk.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.
maxPixelsthe shadow falls back to the clipped, uncached path,sized to the part that can actually be seen — the same escape the
fillTextpath takes.Tests
test/shadow.test.jsgrows a suite against the pure-JS X server: the samestring drawn through
fillTextand throughlayout()produces the sameamount 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
shadowColorstill does no work at all. The first three fail onmaster.Docs
docs/context-2d.mdlistsdrawGlyphsamong the shadowed operations anddescribes the layout cache and the per-call rule;
docs/text.mdsays it onboth
TextLayout.drawand the glyph-run contract.Closes #283