Skip to content

docs(2d): how strong a blurred shadow gets, and how to test one - #288

Merged
sidorares merged 1 commit into
masterfrom
claude/fix-issue-287-63b42d
Aug 18, 2026
Merged

docs(2d): how strong a blurred shadow gets, and how to test one#288
sidorares merged 1 commit into
masterfrom
claude/fix-issue-287-63b42d

Conversation

@sidorares

@sidorares sidorares commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Closes #287.

What I found

Shadows are not missing on the in-process JS X server. They paint exactly
what Xorg paints.

Drawing the issue's own snippet on both servers, with the same pinned font
so the glyphs are identical, gives byte-identical pixels — fillText and
TextLayout.draw alike, and the same for rects, paths, strokes and images
across blur sizes. Nothing in lib/ needed changing, so nothing in lib/
changed.

What the report actually found is how strong a blurred shadow gets. A
blur is coverage convolved with a gaussian, so a shadow reaches
shadowColor only where the shape casting it is wide compared with the
blur:

what casts it shadowBlur peak alpha
a 60x40 rect 8, sigma 4 1.00
a 60x40 rect 30, sigma 15 0.78
48px glyph stems 14, sigma 7 0.37

A five-pixel stem against sigma 7 keeps about 0.28 of its coverage,
which is the gaussian's own answer, and it is what a browser draws too. So
countPixels(ctx, band, '#ff0000', 90) returns 0 on a canvas whose red
glyph shadow is plainly visible — on any server, XQuartz included. The
comparison in the issue was a visual check on one server against an
exact-colour count on the other.

The picture

Same code, same font, two servers. Left is the in-process JS server with no
$DISPLAY; right is XQuartz. Differences outside the small captions: 21
bytes in the glyph-shadow band and 174 in the rect band, none larger than 1.
The captions differ because small-glyph rasterization is deliberately not
pixel-exact, which docs/xserver.md already said.

the same shadow rendered by the in-process JS X server and by XQuartz, side by side

What changed

  • docs/context-2d.md — a section on the peak alpha a shadow reaches,
    the three measured numbers, and what to assert instead of a colour: the
    shadow's own alpha on a transparent target, a difference between two
    places, or the gaussian's CDF profile.
  • docs/xserver.md — server-side filtering is exact on the JS server
    even though rasterization is not, so a shadow that seems to vanish under
    a headless harness is an assertion problem rather than a missing request.
  • test/shadow.test.js — hermetic: the wide-shape case pinned at 200/255
    and the 48px glyph case at about 94/255, with the reason in the comment.
  • test/smoke-canvas.test.js — the same 0.784 asserted against Xorg.
    That cross-server equality is the claim the issue needed and nothing was
    checking.

For the react-x11 side

The font explorer's shadow is testable as it stands. Assert the shadow's
alpha rather than its colour: draw with fillStyle transparent so only the
shadow paints and read the alpha channel, or compare a pixel in the shadow
against one outside it. An exact #ff0000 count will keep returning 0 on
XQuartz too.

Issue #287 reported that shadows paint nothing on node-x11's in-process JS
X server. They paint exactly what Xorg paints. Rendering the issue's own
snippet on both servers with the same pinned font gives byte-identical
pixels, for `fillText` and for `TextLayout.draw` alike, and the same holds
for rects, paths, strokes and images across blur sizes.

What the report actually found is how strong a blurred shadow gets. A blur
is coverage convolved with a gaussian, so a shadow only reaches
`shadowColor` where the shape casting it is wide compared with the blur.
A 60x40 rect at `shadowBlur: 30` peaks at 0.78 alpha; 48px glyph stems at
`shadowBlur: 14` peak at 0.37, because a five-pixel stem against sigma 7
keeps under a third of its coverage. So a "count pixels within 90 of
#ff0000" assertion finds nothing on a canvas whose red glyph shadow is
plainly visible - on any server, including Xorg.

Nothing to fix in `lib/`; what was missing was the number and a test that
holds both servers to it.

- docs/context-2d.md: a section on the peak alpha a shadow reaches, with
  the three measured numbers, and what to assert instead of a colour
- docs/xserver.md: filtering is exact on the JS server even though
  rasterization is not, so a vanishing shadow there is an assertion
  problem, not a missing request
- test/shadow.test.js: the wide-shape case pinned at 200/255 and the
  48px glyph case at ~94/255, hermetically
- test/smoke-canvas.test.js: the same 0.784 asserted against Xorg, which
  is the cross-server claim nothing was checking
@sidorares
sidorares merged commit 5eb86a8 into master Aug 18, 2026
6 checks passed
@sidorares
sidorares deleted the claude/fix-issue-287-63b42d branch August 18, 2026 08:43
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.

2d: shadows paint nothing on an in-process (JS) X server, while every primitive they use works there

1 participant