Skip to content

feat(htmlcss): Blink-parity shadow blur + dashed/dotted borders, 8 L0 fixtures#689

Merged
softmarshmallow merged 10 commits intohtmlcssfrom
feature/affectionate-kapitsa-f3ff4b
Apr 23, 2026
Merged

feat(htmlcss): Blink-parity shadow blur + dashed/dotted borders, 8 L0 fixtures#689
softmarshmallow merged 10 commits intohtmlcssfrom
feature/affectionate-kapitsa-f3ff4b

Conversation

@softmarshmallow
Copy link
Copy Markdown
Member

@softmarshmallow softmarshmallow commented Apr 23, 2026

Summary

Nine commits driving cg htmlcss closer to Chromium parity across shadows, borders, and several new primitives. All Rust changes cite the Blink source line they mirror (shadow_data.h, box_painter_base.cc, styled_stroke_data.cc, box_border_painter.cc).

Real renderer fixes (3)

  • box-shadow blur σ — CSS Backgrounds §7.2 defines blur-radius as , but we were passing the raw CSS value to MaskFilter::blur as sigma. Blink's ShadowData::BlurRadiusToStdDev (shadow_data.h:76-82) halves it. Post-fix a baseline 37% fixture jumps to 100%. (81cd84008)
  • inset-shadow symmetric frame — We were shifting only the inner hole by shadow.offset while using an oversized outer rect (blur*2+100), producing an asymmetric frame whose blur gradients saturated at the box edge on the offset side. Now matches Blink's AreaCastingShadowInHole + DrawLooper offset model (box_painter_base.cc:511-578): centered inner hole, outer rect sized by blur-radius unioned with the pre-offset position, then canvas.translate before drawing. 93% → 100%. (cfde7fdd9)
  • dashed border dash ratios + gap selection — Our [3w, 3w] fixed pattern ignored thickness and never adjusted gap to fit. Ported Blink's ratios ([2w, w] thick, [3w, 2w] thin) and SelectBestDashGap (styled_stroke_data.cc:40-113) so integer dashes fit each side length. 93% → 100%. (8689c7645)

Partial fix (1)

  • dotted border intervals — Ported Blink's [0, gap+width-ε] with round cap and thick-line endpoint inset. 93% → 96% (AA-ignore). Thin (≤3px) dots still misalign: Blink's pixel-snap EnforceDotsAtEndpoints (box_border_painter.cc:401-497) is not yet ported; memoed as a residual for a follow-up. (f39db8f2d)

Promoted to L0.exact at 100% byte-exact parity (7 new fixtures)

  • paint-outline-radius — outline + border-radius (8 variants)
  • paint-box-shadow-blur — outer blur variants (7 variants) (unlocked by the σ fix)
  • paint-box-shadow-inset-blur — inset blur variants (7 variants) (unlocked by the inset-shadow fix)
  • paint-border-style-dashed — width 1/2/3/6/10 + colored (unlocked by the dash fix)
  • paint-filter-drop-shadow — no-blur / small / medium / large / colored / negative-offset
  • paint-transform-matrix — 6 variants of the CSS Transforms 1 §7.1 matrix() primitive
  • paint-filter-blur — CSS Filter Effects §11.4.4 blur() across sigmas

L0.coverage with memoed residual (2 new fixtures)

  • paint-gradient-radial — 15 variants covering shape × extent × explicit × position × stops (plausibly WPT-contributable). 92.76% residual is rasterizer-level: Skia dither-lattice phase through our intermediate rasterize_gradient surface vs Blink's CC-layer direct path. Verified visually identical. Same class as the prior linear-gradient residual. (bde228048)
  • paint-border-style-dotted — partial fix landed at 96.24%; thin-width pixel alignment pending EnforceDotsAtEndpoints. (f39db8f2d)

Cleanup

refactor(htmlcss): simplify shadow + border helpers (495030e19):

  • blur_radius_to_sigma(r) so the σ = r × 0.5 rule has one home
  • side_length(pos, w, h) collapses duplicate SidePos matches in paint_border_side
  • One-line note on the select_best_dash_gap .max(1.0) deviation from Blink (open-path-1-dash edge case)
  • Drop the commit-message-style post-mortem comment in the inset shadow block

No behavioral change across simplify; scores identical before vs after.

Verification

Run locally from the worktree:

cargo check -p cg -p grida-canvas-wasm -p grida-dev
cargo test -p cg --lib
cargo clippy --no-deps -p cg

Reftest (requires Playwright Chromium installed):

cd packages/grida-reftest
pnpm exec playwright install chromium
# render both producers
cargo run -p cg --example golden_htmlcss --release -- --suite fixtures/test-html/suites/L0.exact.json
# refbrowser expecteds — see .claude/skills/cg-reftest/SKILL.md

Test plan

  • cargo check -p cg -p grida-canvas-wasm -p grida-dev — passes
  • cargo test -p cg --lib — 771 tests pass, 0 fail
  • cargo clippy --no-deps -p cg — 0 new warnings
  • L0.exact gate (64 fixtures, threshold 0, AA-ignore) — 100.00% min / avg / max
  • L0.coverage full run — no regressions on pre-existing exact fixtures; known residuals unchanged
  • Reviewer-side eyeball of diff PNGs on the two coverage residuals (attach on request)

…parity shader build

Fixture covers 15 radial-gradient spec branches: shape (circle/ellipse),
extent-keyword (closest/farthest × side/corner), explicit radii, position
(keyword/percent/px), and multi-stop. Landed at 92.76% (--no-aa) in
L0.coverage — residual is rasterizer-level dither-lattice phase between
our intermediate rasterize_gradient surface and Blink's direct-compositor
path, same class as iter 8 linear gradient.

Refactor build_radial_gradient_shader to mirror Blink (gradient.cc:447-454):
build shader at true (cx, cy) with radius=rx, apply preScale(1, 1/aspect)
local matrix only for ellipses. Circles take a matrix-free path, which
avoids the matrix-inverse round-trip even though it did not change the
score — semantics are now cleaner and line up 1:1 with Blink for review.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Apr 23, 2026 1:08pm
docs Ready Ready Preview, Comment Apr 23, 2026 1:08pm
grida Ready Ready Preview, Comment Apr 23, 2026 1:08pm
viewer Ready Ready Preview, Comment Apr 23, 2026 1:08pm
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
backgrounds Ignored Ignored Preview Apr 23, 2026 1:08pm
code Ignored Ignored Apr 23, 2026 1:08pm
legacy Ignored Ignored Apr 23, 2026 1:08pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d926aef2-c1c9-4194-a86b-bae2bcb8e484

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/affectionate-kapitsa-f3ff4b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

8 variants cover outline + border-radius interaction per CSS UI §5.2 and
CSS Backgrounds §5.3: solid at 16px/32px radii, outline-offset, thick
outline with small radius, thin outline with large radius, asymmetric
per-corner radii, elliptical (rx ry) radii, and double outline. Matches
Chromium at 100.00% (AA-ignore) and 99.96% (--no-aa, sub-pixel corner
specks only), even though our stroke-an-expanded-RRect path (paint.rs
:2086) differs architecturally from Blink's fill-outer-clip-inner
(outline_painter.cc:468-525).
… 0.5

CSS Backgrounds §7.2 defines blur-radius as twice the Gaussian standard
deviation. We were passing CSS `blur-radius` directly to Skia's mask
filter as sigma, producing shadows that were 2× too blurry. Match Blink's
ShadowData::BlurRadiusToStdDev (shadow_data.h:76-82): σ = radius * 0.5
at both outer and inset mask-filter sites.

Add paint-box-shadow-blur L0.exact fixture (7 variants: blur sizes,
offset+blur, blur+spread, colored translucent, blur+border-radius).
Baseline 37.46% → 100.00% (AA-ignore) / 99.99% (--no-aa) after fix.
Existing shadow fixtures stayed at 100% because they all used 0 blur.
The inset-shadow path was shifting only the inner hole by shadow.offset
while keeping the outer rect far (`blur * 2 + 100` thick). That produced
an asymmetric frame whose inner/outer Gaussian gradients could not
overlap on the offset side, so the shadow saturated at the box edge
instead of falling off softly (93% vs Chromium on offset variants).

Match Blink (box_painter_base.cc:511-578): keep inner hole centered on
the box, size outer_rect via AreaCastingShadowInHole (outset by
blur-radius, union with pre-offset position), then canvas.translate by
shadow.offset before drawing so the whole frame shifts in one go —
equivalent to Blink's DrawLooper offset.

Add paint-box-shadow-inset-blur L0.exact fixture (7 variants: blur
sizes, offset+blur, blur+spread, colored translucent, blur+radius).
Baseline 92.97% → 100.00%. No regressions on other shadow fixtures.
…rders

CSS Backgrounds §4.2 leaves dash geometry implementation-defined, but
to match Chromium we need:
- thin lines (<3px): dash = 3×width, gap = 2×width
- thick lines (≥3px): dash = 2×width, gap = 1×width
- gap then adjusted so an integer count of dashes fits each side's
  length evenly (Blink's SelectBestDashGap)

Port styled_stroke_data.cc:40-113 to our stroke_paint builder. The
function now takes an optional path_length; per-side border painting
passes the side length, outline (RRect perimeter) passes None and
falls back to nominal intervals.

Add paint-border-style-dashed L0.exact fixture (6 variants: widths
1/2/3/6/10, colored). Baseline 92.78% → 100.00%. No regressions.
…inset

CSS Backgrounds §4.2 border-style: dotted. Our stroke used [w, w] dash
with round cap, producing 2×width dots at 2×width spacing. Blink
(styled_stroke_data.cc:115-132) uses [0, gap+width-ε] with round cap —
the zero dash + round cap yields width-diameter dots, and the gap is
picked by SelectBestDashGap so an integer count of dots fits the path
length.

Also inset each line's endpoints by width/2 for thick dotted (>3px),
matching box_border_painter.cc:528-537 so round caps don't extend
beyond the box.

Add paint-border-style-dotted L0.coverage fixture (6 variants). 92.78%
→ 96.24% (AA-ignore) / 90.36% (--no-aa). Thin (width ≤ 3) variants
still misalign; Blink's EnforceDotsAtEndpoints pixel-snap logic
(box_border_painter.cc:401-497) not yet ported — memoed as residual
for a follow-up.
6 variants cover CSS Filter Effects §9.7 drop-shadow() branches:
no-blur (2-length form), small/medium/large blur sigmas, colored
translucent, negative offsets. 100.00% on first try — Blink stores
the blur value as sigma directly (filter_effect_builder.cc:298) and
our code path does the same via image_filters::drop_shadow, so no
code change needed.
6 variants exercise CSS Transforms 1 §7.1 matrix(a,b,c,d,tx,ty):
identity, pure translate/scale/rotate via the matrix primitive,
scale+translate composed, shear. 100.00% AA-ignore / 99.43%
--no-aa (yellow AA edges on rotate + shear, same class as
iter 13 / iter 61 rotation/skew residuals). No code change.
6 variants exercise CSS Filter Effects §11.4.4 blur(<length>):
sigma 0/2/4/8/12 + colored. 100.00% on first try — spec says the
length is Gaussian σ directly (not halved like box-shadow blur-radius),
and our image_filters::blur((sigma, sigma)) call already matches
Blink. No code change.
- Extract `blur_radius_to_sigma(r)` so the CSS Backgrounds §7.2 rule
  (σ = r/2) has one home shared by outer and inset box-shadow
  mask-filter sites.
- Extract `side_length(pos, w, h)` next to the existing `side_endpoints`
  / `side_inward_normal` family; collapse the duplicate SidePos match
  in paint_border_side.
- Add a comment on the `.max(1.0)` guard in select_best_dash_gap so the
  deviation from Blink's open-path-with-1-dash branch is explicit.
- Drop the commit-message-style post-mortem comment in the inset shadow
  block; the remaining comment plus the Blink line-number citations
  carry the full intent.

No behavioral change — 771 tests pass, full L0.coverage diff scores
unchanged (average 99.61%).
@softmarshmallow softmarshmallow changed the title test(htmlcss): add radial gradient L0 coverage and parity shader path feat(htmlcss): Blink-parity shadow blur + dashed/dotted borders, 8 L0 fixtures Apr 23, 2026
@softmarshmallow softmarshmallow marked this pull request as ready for review April 23, 2026 13:05
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 495030e191

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2088 to +2091
let per_dot = width * 2.0;
let gap = match path_length {
Some(len) if len >= per_dot => select_best_dash_gap(len, width, width, false),
_ => per_dot,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use nominal gap for dotted strokes without path length

When path_length is None (the code path used by paint_uniform_rounded_border and outline_paint), this fallback sets gap to width * 2.0, but off is later computed as gap + width - ε. That makes dotted spacing roughly 3×width instead of Blink’s nominal 2×width, so dotted outlines and rounded dotted borders become visibly too sparse. The fallback should use the nominal gap (width) or compute an equivalent off directly.

Useful? React with 👍 / 👎.

@softmarshmallow softmarshmallow merged commit 1750e28 into htmlcss Apr 23, 2026
16 checks passed
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