diff --git a/.agents/skills/cg-reftest/SKILL.md b/.agents/skills/cg-reftest/SKILL.md index 5ecd4b19d..0f058ab48 100644 --- a/.agents/skills/cg-reftest/SKILL.md +++ b/.agents/skills/cg-reftest/SKILL.md @@ -325,8 +325,8 @@ against the current suite config, move its entry from `coverage` → `exact`. Do **not** lower the exact suite's floor to fit new entries; the bar exists so regressions are loud. -Per-fixture `.reftest.json` sidecars **do not exist** anymore. All -config lives in the suite file. +All per-fixture config lives in the suite file. There are no +per-fixture `.reftest.json` sidecars. #### Suite JSON shape @@ -334,25 +334,23 @@ config lives in the suite file. { "name": "L0.exact", "description": "Byte-exact fixtures; any drop = regression.", - "gate": { "threshold": 0, "aa": false, "floor": 1.0 }, + "gate": { "threshold": 0, "aa": true, "floor": 1.0 }, "defaults": { "wait_for": ["fonts", "networkidle"], - "extra_css": ["../_reftest/hide-text.css"], + "extra_css": [ + "../_reftest/hide-text.css", + "../_reftest/transparent-body.css" + ], "full_page": true }, - "fixtures": [ - { - "path": "../L0/box-dimensions.html", - "viewport": { "width": 600, "height": 522 } - } - ] + "fixtures": [{ "path": "../L0/box-dimensions.html" }] } ``` - `defaults` — applied to every fixture. Each fixture entry can override any field. - `fixtures[].path` and every `extra_css[]` path resolve **relative to the suite file**. -- `viewport.height` must match cg's cull height for the diff to succeed; render cg once and read `WxH` to calibrate. - `gate.threshold` / `gate.aa` are inputs to the pixelmatch diff; `gate.floor` is the aggregate pass bar on similarity. +- **`aa: true` (default)** → pixelmatch `includeAA: false`. Pixelmatch's AA detector fires and excludes anti-aliased edge pixels from the diff count, separating rasterizer edge noise (Skia vs. Blink) from real divergence. Set `aa: false` for strict byte-exact accounting (e.g. probing an AA-class regression). #### The three-step pipeline @@ -395,9 +393,13 @@ cp "${TMPDIR:-/tmp}/grida-htmlcss-goldens/"*.png target/refbrowser/L0.exact/actu **3. Diff via `@grida/reftest`** — format-agnostic, same bucket layout and `report.json` schema as the Rust and refig runners. -Default refbrowser diff: **`--threshold 0`** (pixelmatch strictest, -AA off). Pass each fixture's similarity against the suite's -`gate.floor` — for `L0.exact`, that's `1.0` (100.00% byte-exact). +Default refbrowser diff: **`--threshold 0`** (pixelmatch's tightest +color-delta) with **AA-ignore mode on by default** (`aa: true` → +`includeAA: false`; pixelmatch's Vysniauskas AA detector fires and +excludes edge AA pixels from the diff count). Pass `--no-aa` to flip +to strict byte-exact accounting. Pass each fixture's similarity +against the suite's `gate.floor` — for `L0.exact`, that's `1.0` +(100.00% similarity with AA detection active). ```sh pnpm --filter @grida/reftest exec reftest \ @@ -419,43 +421,46 @@ Pass bar: the suite's `gate.floor`. For `L0.exact`, anything below 100.00% is a real divergence from Blink (rounding policy, layout math, AA emission, etc.) — not noise. See "Reading the score" below. -### Reading the score — do not trust it naively - -The similarity score is `1 - diff_pixels / scoring_pixels`, where -`scoring_pixels ≈ width × height` of the screenshot. **The denominator -is the whole canvas, not the subject under test.** - -This has two consequences you must internalize before reading any -report: - -1. **Background dominates the score.** A fixture that paints a - 100×100 subject on a 600×800 canvas has 92% background. A renderer - that emits _nothing_ for the subject still scores ~92%. A - renderer that paints the subject at 50% accuracy scores ~96%. - Neither number means what it naively looks like. -2. **Small fixtures inflate. Full-bleed fixtures are honest.** A - card-in-corner composition will always look "good" on the score - even when broken; a composition that fills the viewport gives - numeric feedback proportional to real error. - -**Fixture-authoring rule:** size the fixture so the subject under -test fills as much of the canvas as practical. Viewport height -tuned to the subject's bounding box (via the suite entry's -`viewport.height`) is the usual lever. Padding/margins around the -subject are scoring dead weight — use them only when the test is -_about_ spacing. - -**Reviewing rule:** never report a similarity number without -eyeballing the diff PNG. A 96% score on a sparse fixture and a 96% -score on a full-bleed fixture are _orders of magnitude_ apart in -severity. The diff image is the source of truth; the score is a -coarse index. - -For a true "fraction of the subject that matches," author a -probe-friendly fixture (see the probe test section) and assert on -specific pixels, or mask the background to transparent so -`mask: alpha` counts only subject pixels. Plain refbrowser scores -cannot give you that signal. +### Scoring model — content mask + +The similarity score is `1 - diff_pixels / scoring_pixels`. +`scoring_pixels` is the count of pixels where either side has +`alpha > 0` — the content mask, not the full canvas. Three +coupled defaults wire this up: + +- **Chromium** screenshots with `omitBackground: true` (in + `refbrowser_render.ts`). Root canvas default bg is dropped; PNG + alpha encodes "did the CSS cascade draw here?" +- **cg** clears its Skia surface with `Color::TRANSPARENT` and + renders at viewport dims (in `examples/golden_htmlcss.rs`). +- **Both sides** apply `_reftest/transparent-body.css` via + `extra_css`. `!important` forces `html, body { background: +transparent }`, so fixtures with `body { background: #fff }` + still produce a content mask without being edited. + +Chromium and cg produce identical alpha masks on every L0.exact +fixture. Diffs that appear under `alpha > 0` are genuine pixel +differences. + +**AA-ignore on by default** (`aa: true` → pixelmatch +`includeAA: false`). The Vysniauskas AA detector excludes +anti-aliased edge pixels from the diff count. Combined with the +content mask, this yields: + +| pattern | strict (`--no-aa`) | default (`--aa`) | meaning | +| ------------------- | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------ | +| **pass** | 100% | 100% | identical — no action. | +| **AA noise** | 99.9+% | 100% | Skia/Blink rasterizer edge jitter on curves, radii, tilted geometry. Safe to ignore. | +| **real divergence** | <100% | <100% | renderer bug or non-AA rasterizer mismatch (dither lattice, multi-color miter wedges). Inspect the diff PNG. | + +**Reviewing rule:** always eyeball the diff PNG. A fixture below +100% with `aa: true` has mismatched pixels that pixelmatch could +not explain as edge AA — treat as real. + +**Fixture-authoring rule:** the content mask excludes blank bg, so +there's no need to shrink viewports for scoring density. Focus on +minimality (one concept per fixture). Probe tests remain the tool +for vision-free pixel assertions at known coordinates. **Per-fixture fields inside a suite entry** — all optional, defaults shown; any field set on an entry overrides `defaults`. @@ -484,9 +489,10 @@ defaults shown; any field set on an entry overrides `defaults`. **Pre-built helper stylesheets** under `fixtures/test-html/_reftest/`: -| File | Effect | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `hide-text.css` | `color: transparent` + `line-height: 1`. Zeros glyph coverage and pins line-box height. Use when a fixture isn't testing text. | +| File | Effect | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `hide-text.css` | `color: transparent` + `line-height: 1`. Zeros glyph coverage and pins line-box height. Use when a fixture isn't testing text. | +| `transparent-body.css` | Forces `html, body { background: transparent !important }`. Enables the content mask (alpha>0 = drawn). Both L0 suites apply this by default; drop from `extra_css` for fixtures testing canvas bg. | Add more helpers here as divergence patterns emerge. Keep each one scoped to a single concern (hide text, normalize scrollbars, force @@ -558,9 +564,10 @@ PR description; let the score carry the truth. - **Scrollbar width** — default `full_page: true` captures document height and sidesteps scrollbar chrome; flip only when testing scrollbar geometry. -- **Dimension drift** — changing a fixture's layout invalidates its - `viewport.height` in the suite entry. Re-run `golden_htmlcss` with - `--suite`, update the entry's `viewport.height`, re-run refbrowser. +- **Dimensions** — cg renders at viewport dims (`width × height`); + Chromium screenshots `fullPage` at the same viewport. Setting an + explicit `viewport.height` is optional and only useful to trim + scoring area for very tall fixtures. **Oracle type summary:** diff --git a/.agents/skills/cg-reftest/scripts/refbrowser_render.ts b/.agents/skills/cg-reftest/scripts/refbrowser_render.ts index 46b88178e..c35622feb 100644 --- a/.agents/skills/cg-reftest/scripts/refbrowser_render.ts +++ b/.agents/skills/cg-reftest/scripts/refbrowser_render.ts @@ -216,6 +216,8 @@ async function renderOne( fullPage: config.full_page, animations: "disabled", caret: "hide", + // Alpha encodes "CSS drew here" — used as content mask by scoring. + omitBackground: true, }); await page.close(); diff --git a/.agents/skills/fixtures/SKILL.md b/.agents/skills/fixtures/SKILL.md index 953c090a5..d4d6e0207 100644 --- a/.agents/skills/fixtures/SKILL.md +++ b/.agents/skills/fixtures/SKILL.md @@ -43,6 +43,13 @@ edge case** that the codebase supports or intends to support. This includes: filename alone should tell you what's being tested. - **Labeled specimens.** Within a fixture, label each test case with the value being exercised so both humans and heuristics can identify regions. + Keep labels short, and pin the dimensions of any container holding a + label (flex item, grid cell, stretched block) so font-advance-width + differences between engines can't leak into box geometry. When a test + pipeline offers a text-neutralizing stylesheet (e.g. + `fixtures/test-html/_reftest/hide-text.css` for the htmlcss reftests), + prefer that over stripping the label — keeping the text helps the next + reader understand the fixture. - **Match the fixture's subject to the viewport policy.** For refbrowser fixtures under `fixtures/test-html/`, **paint / visual-property** fixtures should size their root to a preset viewport (via `min-height`) diff --git a/crates/grida-canvas/examples/golden_htmlcss.rs b/crates/grida-canvas/examples/golden_htmlcss.rs index 81ada4db3..69e78cfae 100644 --- a/crates/grida-canvas/examples/golden_htmlcss.rs +++ b/crates/grida-canvas/examples/golden_htmlcss.rs @@ -125,13 +125,14 @@ fn render_to_png( ) { let picture = htmlcss::render(html, width, height, fonts, &htmlcss::NoImages).expect("render failed"); - let cull = picture.cull_rect(); - let w = cull.width().max(1.0) as i32; - let h = cull.height().max(1.0) as i32; + // Full-viewport dims match Chromium's fullPage footprint; transparent clear + // lets PNG alpha double as the reftest content mask. + let w = width.max(1.0) as i32; + let h = height.max(1.0) as i32; let mut surface = surfaces::raster_n32_premul((w, h)).expect("surface"); let canvas = surface.canvas(); - canvas.clear(Color::WHITE); + canvas.clear(Color::TRANSPARENT); canvas.draw_picture(&picture, None, None); let image = surface.image_snapshot(); diff --git a/crates/grida-canvas/src/htmlcss/collect.rs b/crates/grida-canvas/src/htmlcss/collect.rs index 9df86ee46..cb29cea2a 100644 --- a/crates/grida-canvas/src/htmlcss/collect.rs +++ b/crates/grida-canvas/src/htmlcss/collect.rs @@ -871,7 +871,7 @@ fn collect_inline_items(el: &StyledElement, items: &mut Vec) { /// Returns `None` if the element has no visual box decoration. fn build_inline_decoration(el: &StyledElement) -> Option { let bg = el.background.first().and_then(|l| match l { - BackgroundLayer::Solid(c) if c.a > 0 => Some(*c), + BackgroundLayer::Solid { color, .. } if color.a > 0 => Some(*color), _ => None, }); @@ -1185,6 +1185,10 @@ fn extract_style(tag: &str, style: &ComputedValues) -> StyledElement { // Flex child el.flex_grow = style.clone_flex_grow().0; el.flex_shrink = style.clone_flex_shrink().0; + el.flex_basis = match &pos.flex_basis { + style::values::generics::flex::FlexBasis::Size(s) => extract_size(s), + style::values::generics::flex::FlexBasis::Content => CssLength::Auto, + }; // Grid container if el.display == types::Display::Grid { @@ -1571,18 +1575,41 @@ fn extract_gradient_interpolation( fn extract_border_radius(style: &ComputedValues) -> CornerRadii { let b = style.get_border(); - let lp = |lp: &style::values::computed::NonNegativeLengthPercentage| -> f32 { - lp.0.to_length().map(|l| l.px()).unwrap_or(0.0) + // Returns (px, percent_fraction). Percent is deferred to paint time, + // where the border-box w/h is known (CSS Backgrounds 3 §5.3). + let lp = |v: &style::values::computed::NonNegativeLengthPercentage| -> (f32, f32) { + match length_percentage_to_css(&v.0) { + CssLength::Px(p) => (p, 0.0), + CssLength::Percent(p) => (0.0, p), + CssLength::Calc { px, percent } => (px, percent), + CssLength::Auto => (0.0, 0.0), + } }; + let (tl_x, tl_x_pct) = lp(&b.border_top_left_radius.0.width); + let (tl_y, tl_y_pct) = lp(&b.border_top_left_radius.0.height); + let (tr_x, tr_x_pct) = lp(&b.border_top_right_radius.0.width); + let (tr_y, tr_y_pct) = lp(&b.border_top_right_radius.0.height); + let (br_x, br_x_pct) = lp(&b.border_bottom_right_radius.0.width); + let (br_y, br_y_pct) = lp(&b.border_bottom_right_radius.0.height); + let (bl_x, bl_x_pct) = lp(&b.border_bottom_left_radius.0.width); + let (bl_y, bl_y_pct) = lp(&b.border_bottom_left_radius.0.height); CornerRadii { - tl_x: lp(&b.border_top_left_radius.0.width), - tl_y: lp(&b.border_top_left_radius.0.height), - tr_x: lp(&b.border_top_right_radius.0.width), - tr_y: lp(&b.border_top_right_radius.0.height), - br_x: lp(&b.border_bottom_right_radius.0.width), - br_y: lp(&b.border_bottom_right_radius.0.height), - bl_x: lp(&b.border_bottom_left_radius.0.width), - bl_y: lp(&b.border_bottom_left_radius.0.height), + tl_x, + tl_y, + tr_x, + tr_y, + br_x, + br_y, + bl_x, + bl_y, + tl_x_pct, + tl_y_pct, + tr_x_pct, + tr_y_pct, + br_x_pct, + br_y_pct, + bl_x_pct, + bl_y_pct, } } @@ -1590,15 +1617,7 @@ fn extract_size( size: &GenericSize, ) -> CssLength { match size { - GenericSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - CssLength::Px(len.px()) - } else if let Some(pct) = lp.0.to_percentage() { - CssLength::Percent(pct.0) - } else { - CssLength::Auto - } - } + GenericSize::LengthPercentage(lp) => length_percentage_to_css(&lp.0), _ => CssLength::Auto, } } @@ -1610,15 +1629,7 @@ fn extract_max_size( ) -> CssLength { use style::values::generics::length::GenericMaxSize; match size { - GenericMaxSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - CssLength::Px(len.px()) - } else if let Some(pct) = lp.0.to_percentage() { - CssLength::Percent(pct.0) - } else { - CssLength::Auto - } - } + GenericMaxSize::LengthPercentage(lp) => length_percentage_to_css(&lp.0), _ => CssLength::Auto, // None, MaxContent, MinContent, FitContent, etc. } } @@ -1653,11 +1664,22 @@ fn extract_background(style: &ComputedValues, current_color: CGColor) -> Vec = Vec::new(); - // 1. Background color (bottom layer) + // 1. Background color (bottom layer). Per CSS Backgrounds 3 §2.5 the + // color uses the `background-clip` value from the *final* layer + // entry in the list. if let Some(abs) = bg.background_color.as_absolute() { let c = abs_color_to_cg(abs); if c.a > 0 { - layers.push(BackgroundLayer::Solid(c)); + let color_clip = bg + .background_clip + .0 + .last() + .map(extract_bg_clip) + .unwrap_or(BackgroundBox::BorderBox); + layers.push(BackgroundLayer::Solid { + color: c, + clip: color_clip, + }); } } @@ -2771,10 +2793,10 @@ fn map_overflow(ov: style::values::specified::box_::Overflow) -> types::Overflow fn abs_color_to_cg(color: &AbsoluteColor) -> CGColor { let srgb = color.to_color_space(ColorSpace::Srgb); CGColor::from_rgba( - (srgb.components.0.clamp(0.0, 1.0) * 255.0) as u8, - (srgb.components.1.clamp(0.0, 1.0) * 255.0) as u8, - (srgb.components.2.clamp(0.0, 1.0) * 255.0) as u8, - (srgb.alpha.clamp(0.0, 1.0) * 255.0) as u8, + (srgb.components.0.clamp(0.0, 1.0) * 255.0).round() as u8, + (srgb.components.1.clamp(0.0, 1.0) * 255.0).round() as u8, + (srgb.components.2.clamp(0.0, 1.0) * 255.0).round() as u8, + (srgb.alpha.clamp(0.0, 1.0) * 255.0).round() as u8, ) } diff --git a/crates/grida-canvas/src/htmlcss/paint.rs b/crates/grida-canvas/src/htmlcss/paint.rs index 0f444d728..ae66c2bbc 100644 --- a/crates/grida-canvas/src/htmlcss/paint.rs +++ b/crates/grida-canvas/src/htmlcss/paint.rs @@ -75,8 +75,10 @@ fn paint_box( let w = layout.width; let h = layout.height; - // ── Save state for opacity / filter / clip ── - let needs_layer = style.opacity < 1.0 || !style.filter.is_empty(); + // ── Save state for opacity / filter / mix-blend-mode / clip ── + let has_filter = !style.filter.is_empty(); + let has_blend_mode = !matches!(style.blend_mode, crate::cg::prelude::BlendMode::Normal); + let needs_layer = style.opacity < 1.0 || has_filter || has_blend_mode; let needs_clip = style.overflow_x != types::Overflow::Visible || style.overflow_y != types::Overflow::Visible; @@ -117,19 +119,27 @@ fn paint_box( if needs_layer { let mut layer_paint = Paint::default(); - layer_paint.set_alpha((style.opacity * 255.0) as u8); - let has_filter = !style.filter.is_empty(); + layer_paint.set_alpha_f(style.opacity); + // CSS `mix-blend-mode` composites this element's stacking context + // onto its parent using the given blend mode (CSS Compositing 1 + // §5). Apply as the layer's Skia blend mode. + if has_blend_mode { + layer_paint.set_blend_mode(style.blend_mode.into()); + } if has_filter { if let Some(filter) = build_filter_chain(&style.filter) { layer_paint.set_image_filter(filter); } } - // Skia clips a layer's output to its `bounds` hint, including any - // filter outset. Blur / drop-shadow extend the visible region past - // the source box, so when a filter is active we omit `bounds` and - // let Skia size the layer from the filter's own fast-bounds. + // Skia clips a layer's output to its `bounds` hint. Filters + // (blur/drop-shadow) extend past the source box, and + // `mix-blend-mode` composites whatever the element paints — + // including outer box-shadows, outlines, and overflowing + // descendants — onto the parent. In both cases we omit `bounds` + // and let Skia size the layer from the actual painted content so + // those pixels aren't clipped before compositing. let bounds = Rect::from_xywh(0.0, 0.0, w, h); - let layer_rec = if has_filter { + let layer_rec = if has_filter || has_blend_mode { skia_safe::canvas::SaveLayerRec::default().paint(&layer_paint) } else { skia_safe::canvas::SaveLayerRec::default() @@ -197,7 +207,7 @@ fn paint_box( cy, cw, ch, - &style.border_radius, + &style.border_radius.resolved(w, h), style.font.image_rendering, images, ); @@ -392,6 +402,8 @@ fn rasterize_gradient( let canvas = surface.canvas(); let mut paint = Paint::default(); paint.set_shader(shader); + // Match Blink: gradients are always dithered (gradient.cc:359). + paint.set_dither(true); canvas.draw_rect(Rect::from_wh(w, h), &paint); surface.image_snapshot().into() } @@ -409,12 +421,13 @@ fn paint_background( return; } - let rect = Rect::from_xywh(0.0, 0.0, w, h); - let r = &style.border_radius; + let resolved_r = style.border_radius.resolved(w, h); + let r = &resolved_r; + let border_rect = Rect::from_xywh(0.0, 0.0, w, h); for layer in &style.background { match layer { - BackgroundLayer::Solid(c) => { + BackgroundLayer::Solid { color: c, clip } => { if c.a == 0 { continue; } @@ -422,11 +435,13 @@ fn paint_background( paint.set_style(PaintStyle::Fill); paint.set_anti_alias(true); paint.set_color(Color::from_argb(c.a, c.r, c.g, c.b)); + let fill_rect = box_reference_rect(style, w, h, *clip); if r.is_zero() { - canvas.draw_rect(rect, &paint); + canvas.draw_rect(fill_rect, &paint); } else { + let radii = inset_radii(r, border_rect, fill_rect); let mut rrect = skia_safe::RRect::new(); - rrect.set_rect_radii(rect, &r.to_skia_radii()); + rrect.set_rect_radii(fill_rect, &radii); canvas.draw_rrect(rrect, &paint); } } @@ -661,7 +676,7 @@ fn paint_background_image_layer( // rounded to match the inner edge"). if !style.border_radius.is_zero() { let border_rect = Rect::from_xywh(0.0, 0.0, w, h); - let radii = inset_radii(&style.border_radius, border_rect, clip_rect); + let radii = inset_radii(&style.border_radius.resolved(w, h), border_rect, clip_rect); let mut rrect = skia_safe::RRect::new(); rrect.set_rect_radii(clip_rect, &radii); canvas.clip_rrect(rrect, ClipOp::Intersect, true); @@ -1340,7 +1355,10 @@ fn to_skia_interpolation(v: super::style::GradientInterpolation) -> Interpolatio HM::Decreasing => HueMethod::Decreasing, }; Interpolation { - in_premul: InPremul::No, + // Match Blink: legacy CSS gradients premultiply colors before + // interpolating (gradient.cc:282-285). For opaque stops this is a + // no-op; the difference shows up when stops have alpha. + in_premul: InPremul::Yes, color_space, hue_method, } @@ -1461,11 +1479,10 @@ fn build_radial_gradient_shader( let (cx, cy) = resolve_center(&grad.center, w, h); let (rx_full, ry_full) = radial_radii(grad.shape, grad.size, cx, cy, w, h); - // Use the larger axis as the gradient line length for px stop - // resolution. For circles rx = ry; for ellipses this is a reasonable - // convention — CSS defines the ending shape's "gradient line" as - // radius-like distance from the center. - let line_length = rx_full.max(ry_full); + // Use the x-axis radius as the gradient line length for px stop + // resolution. Matches Blink: the shader is built at the x radius + // and the y axis is stretched via a preScale local matrix. + let line_length = rx_full; let (colors, raw_positions) = build_gradient_data_with_line_length(&grad.stops, line_length); if colors.len() < 2 { @@ -1473,13 +1490,25 @@ fn build_radial_gradient_shader( } let (positions, cycle) = repeat_scale(raw_positions, grad.repeating); - let (rx, ry) = (rx_full * cycle, ry_full * cycle); + let rx = rx_full * cycle; + let aspect_ratio = if ry_full > 0.0 { + rx_full / ry_full + } else { + 1.0 + }; - // Unit-radius radial at origin; local matrix maps shader → paint space: - // (shader point p) → (cx + rx·p.x, cy + ry·p.y). Works for circles and - // ellipses uniformly. - let mut matrix = skia_safe::Matrix::scale((rx, ry)); - matrix.post_translate((cx, cy)); + // Match Blink (gradient.cc:447-454): build the radial shader at + // (cx, cy) with radius = rx, then preScale(1, 1/aspect) at the + // center for elliptical gradients. Circles take the identity path, + // which avoids the matrix-inverse round-trip and keeps dither phase + // aligned with Blink's non-matrix radial draw. + let matrix = if (aspect_ratio - 1.0).abs() > 1e-6 { + let mut m = skia_safe::Matrix::default(); + m.pre_scale((1.0, 1.0 / aspect_ratio), Some(Point::new(cx, cy))); + Some(m) + } else { + None + }; let gradient = make_gradient( &colors, @@ -1487,7 +1516,7 @@ fn build_radial_gradient_shader( tile_mode(grad.repeating), grad.interpolation, ); - skia_safe::shaders::radial_gradient((Point::new(0.0, 0.0), 1.0), &gradient, Some(&matrix)) + skia_safe::shaders::radial_gradient((Point::new(cx, cy), rx), &gradient, matrix.as_ref()) } fn build_conic_gradient_shader(grad: &ConicGradient, w: f32, h: f32) -> Option { @@ -1806,12 +1835,21 @@ fn paint_borders( && b.top.color == b.bottom.color && b.top.color == b.left.color && b.top.color == b.right.color; + // Stroke once as an RRect when sides are uniform *and* the style is + // one whose rendering doesn't depend on per-side color adjustments + // (inset / outset / groove / ridge darken/lighten per side). The + // per-side trapezoid path double-paints corners for translucent colors; + // the single-stroke path avoids that. + let uniform_stroke_style = matches!( + b.top.style, + types::BorderStyle::Solid | types::BorderStyle::Double + ); if uniform + && uniform_stroke_style && b.top.width > 0.0 - && b.top.style != types::BorderStyle::None - && !style.border_radius.is_zero() + && (b.top.style != types::BorderStyle::None || !style.border_radius.is_zero()) { - paint_uniform_rounded_border(canvas, &b.top, &style.border_radius, w, h); + paint_uniform_rounded_border(canvas, &b.top, &style.border_radius.resolved(w, h), w, h); return; } @@ -1853,7 +1891,7 @@ fn paint_uniform_rounded_border( }; if side.style == types::BorderStyle::Double { let sub_w = side.width / 3.0; - let paint = stroke_paint(side.color, sub_w, types::BorderStyle::Solid); + let paint = stroke_paint(side.color, sub_w, types::BorderStyle::Solid, None); // Outer ring: stroke center near the outside edge. let outer_inset = sub_w / 2.0; canvas.draw_rrect(stroke_center(outer_inset), &paint); @@ -1862,7 +1900,7 @@ fn paint_uniform_rounded_border( canvas.draw_rrect(stroke_center(inner_inset), &paint); return; } - let paint = stroke_paint(side.color, side.width, side.style); + let paint = stroke_paint(side.color, side.width, side.style, None); canvas.draw_rrect(stroke_center(side.width / 2.0), &paint); } @@ -1900,7 +1938,7 @@ fn paint_border_side(canvas: &Canvas, pos: SidePos, side: &BorderSide, w: f32, h let sub_w = side.width / 3.0; let (n_dx, n_dy) = side_inward_normal(pos); // Outer stroke (toward the element's outer edge). - let paint = stroke_paint(side.color, sub_w, BorderStyle::Solid); + let paint = stroke_paint(side.color, sub_w, BorderStyle::Solid, None); let out_off = -sub_w; let outer_p1 = (p1.0 + n_dx * out_off, p1.1 + n_dy * out_off); let outer_p2 = (p2.0 + n_dx * out_off, p2.1 + n_dy * out_off); @@ -1914,10 +1952,35 @@ fn paint_border_side(canvas: &Canvas, pos: SidePos, side: &BorderSide, w: f32, h } let effective_color = shaded_color(side.color, side.style, pos); - let paint = stroke_paint(effective_color, side.width, side.style); + let side_length = side_length(pos, w, h); + + // For thick dotted (width > 3px), Blink insets the line endpoints + // by width/2 so round caps stay inside the box and the gap calc + // sees the inner span (box_border_painter.cc:528-537). Thin dotted + // (≤ 3px) instead uses `EnforceDotsAtEndpoints` — not yet ported; + // those widths land with a small alignment residual. + let (p1, p2, dash_len) = if side.style == types::BorderStyle::Dotted && side.width > 3.0 { + let half = side.width / 2.0; + let (np1, np2) = match pos { + SidePos::Top | SidePos::Bottom => ((p1.0 + half, p1.1), (p2.0 - half, p2.1)), + SidePos::Left | SidePos::Right => ((p1.0, p1.1 + half), (p2.0, p2.1 - half)), + }; + (np1, np2, side_length - side.width) + } else { + (p1, p2, side_length) + }; + + let paint = stroke_paint(effective_color, side.width, side.style, Some(dash_len)); canvas.draw_line(p1, p2, &paint); } +fn side_length(pos: SidePos, w: f32, h: f32) -> f32 { + match pos { + SidePos::Top | SidePos::Bottom => w, + SidePos::Left | SidePos::Right => h, + } +} + fn side_endpoints(pos: SidePos, width: f32, w: f32, h: f32) -> ((f32, f32), (f32, f32)) { let half = width / 2.0; match pos { @@ -1978,7 +2041,17 @@ fn shaded_color(c: CGColor, style: types::BorderStyle, pos: SidePos) -> CGColor /// Shared stroke paint builder for border sides and outline. /// Applies dash/dot path effects for dashed/dotted styles. -fn stroke_paint(color: CGColor, width: f32, style: types::BorderStyle) -> Paint { +/// +/// `path_length` (Some) enables Blink-parity gap adjustment so dashes +/// meet side corners evenly (styled_stroke_data.cc:40-58). None falls +/// back to nominal intervals — used by paths where the length isn't +/// known up front (outline RRect perimeter). +fn stroke_paint( + color: CGColor, + width: f32, + style: types::BorderStyle, + path_length: Option, +) -> Paint { let mut paint = Paint::default(); paint.set_color(Color::from_argb(color.a, color.r, color.g, color.b)); paint.set_stroke_width(width); @@ -1987,13 +2060,42 @@ fn stroke_paint(color: CGColor, width: f32, style: types::BorderStyle) -> Paint match style { types::BorderStyle::Dashed => { - let dash_len = width * 3.0; - if let Some(effect) = skia_safe::PathEffect::dash(&[dash_len, dash_len], 0.0) { + // Blink (styled_stroke_data.cc:60-74): dash/gap relative to + // thickness — thin lines (<3px) use longer dashes/gaps so + // they don't read as dots or solid lines. + let (dash_ratio, gap_ratio) = if width >= 3.0 { + (2.0_f32, 1.0_f32) + } else { + (3.0_f32, 2.0_f32) + }; + let dash = width * dash_ratio; + let nominal_gap = width * gap_ratio; + let gap = match path_length { + Some(len) if len > dash * 2.0 => { + select_best_dash_gap(len, dash, nominal_gap, false) + } + _ => nominal_gap, + }; + if let Some(effect) = skia_safe::PathEffect::dash(&[dash, gap], 0.0) { paint.set_path_effect(effect); } } types::BorderStyle::Dotted => { - if let Some(effect) = skia_safe::PathEffect::dash(&[width, width], 0.0) { + // Blink (styled_stroke_data.cc:115-132): round-cap stroke, + // interval `[0, gap + width - ε]`. The zero "on" segment + // combined with round-cap produces a dot of diameter=width; + // the "off" span sets the center-to-center spacing. + // SelectBestDashGap picks a gap that fits an integer count + // of dots along the side. + 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, + }; + // Epsilon keeps the final dot inside the endpoint + // (styled_stroke_data.cc:127). + let off = (gap + width - 0.01).max(0.01); + if let Some(effect) = skia_safe::PathEffect::dash(&[0.0, off], 0.0) { paint.set_path_effect(effect); } paint.set_stroke_cap(skia_safe::paint::Cap::Round); @@ -2004,6 +2106,51 @@ fn stroke_paint(color: CGColor, width: f32, style: types::BorderStyle) -> Paint paint } +/// CSS Backgrounds §7.2 / Blink `ShadowData::BlurRadiusToStdDev` +/// (shadow_data.h:76-82): blur-radius is twice the Gaussian σ. +#[inline] +fn blur_radius_to_sigma(blur_radius: f32) -> f32 { + blur_radius * 0.5 +} + +/// Pick the gap that minimises deviation from `nominal_gap` while +/// leaving an integer count of dashes on `stroke_length`. Mirrors +/// Blink's `SelectBestDashGap` (styled_stroke_data.cc:40-58). +fn select_best_dash_gap( + stroke_length: f32, + dash_length: f32, + gap_length: f32, + closed_path: bool, +) -> f32 { + let available = if closed_path { + stroke_length + } else { + stroke_length + gap_length + }; + let min_num_dashes = (available / (dash_length + gap_length)).floor().max(1.0); + let max_num_dashes = min_num_dashes + 1.0; + // `.max(1.0)` guards div-by-zero when `min_num_dashes == 1` + // on an open path. Blink lets the divide produce +∞ and relies + // on the `max_gap <= 0.0` branch below to pick `min_gap` anyway. + let min_num_gaps = if closed_path { + min_num_dashes + } else { + (min_num_dashes - 1.0).max(1.0) + }; + let max_num_gaps = if closed_path { + max_num_dashes + } else { + (max_num_dashes - 1.0).max(1.0) + }; + let min_gap = (stroke_length - min_num_dashes * dash_length) / min_num_gaps; + let max_gap = (stroke_length - max_num_dashes * dash_length) / max_num_gaps; + if max_gap <= 0.0 || (min_gap - gap_length).abs() < (max_gap - gap_length).abs() { + min_gap + } else { + max_gap + } +} + // ─── Outline (Chromium: OutlinePainter::PaintOutlineRects) ───────────────── /// Paint CSS `outline` as a stroked rect/rrect around the element. @@ -2021,7 +2168,8 @@ fn paint_outline(canvas: &Canvas, style: &StyledElement, w: f32, h: f32) { return; } - let r = &style.border_radius; + let resolved_r = style.border_radius.resolved(w, h); + let r = &resolved_r; if outline.style == types::BorderStyle::Double { // Two concentric 1/3-width strokes separated by a 1/3-width gap. @@ -2029,7 +2177,7 @@ fn paint_outline(canvas: &Canvas, style: &StyledElement, w: f32, h: f32) { let sub_w = outline.width / 3.0; let outer_expand = outline.offset + outline.width - sub_w / 2.0; let inner_expand = outline.offset + sub_w / 2.0; - let paint = stroke_paint(outline.color, sub_w, types::BorderStyle::Solid); + let paint = stroke_paint(outline.color, sub_w, types::BorderStyle::Solid, None); draw_outline_ring(canvas, w, h, outer_expand, r, &paint); draw_outline_ring(canvas, w, h, inner_expand, r, &paint); return; @@ -2066,7 +2214,7 @@ fn draw_outline_ring( } fn outline_paint(outline: &Outline) -> Paint { - stroke_paint(outline.color, outline.width, outline.style) + stroke_paint(outline.color, outline.width, outline.style, None) } /// Expand border-radius values outward by `expand` pixels. @@ -2085,7 +2233,10 @@ fn expand_radii(r: &super::style::CornerRadii, expand: f32) -> [skia_safe::Point // ─── Box shadow (Chromium: BoxPainterBase::PaintNormalBoxShadow / PaintInsetBoxShadow) ── fn paint_box_shadow_outer(canvas: &Canvas, style: &StyledElement, w: f32, h: f32) { - for shadow in &style.box_shadow { + // CSS Backgrounds §7.2: first shadow listed is on top. Iterate in reverse + // so the last-listed shadow paints first (bottom), leaving the first-listed + // painted last (on top). + for shadow in style.box_shadow.iter().rev() { if shadow.inset { continue; } @@ -2099,11 +2250,9 @@ fn paint_box_shadow_outer(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); if shadow.blur > 0.0 { - // CSS `box-shadow` blur length is a Gaussian sigma per CSS - // Backgrounds §7.2; Skia's mask-filter takes sigma directly. paint.set_mask_filter(skia_safe::MaskFilter::blur( skia_safe::BlurStyle::Normal, - shadow.blur, + blur_radius_to_sigma(shadow.blur), false, )); } @@ -2115,7 +2264,8 @@ fn paint_box_shadow_outer(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 h + shadow.spread * 2.0, ); - let r = &style.border_radius; + let resolved_r = style.border_radius.resolved(w, h); + let r = &resolved_r; if r.is_zero() { canvas.draw_rect(shadow_rect, &paint); } else { @@ -2132,7 +2282,9 @@ fn paint_box_shadow_outer(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 /// then drawing a hollow rect (the box outline expanded outward) with a blur /// mask so that only the soft inner edge is visible. fn paint_box_shadow_inset(canvas: &Canvas, style: &StyledElement, w: f32, h: f32) { - for shadow in &style.box_shadow { + // CSS Backgrounds §7.2: first shadow listed is on top. Iterate in reverse + // so later-listed insets paint first, leaving the first-listed inset on top. + for shadow in style.box_shadow.iter().rev() { if !shadow.inset { continue; } @@ -2141,7 +2293,8 @@ fn paint_box_shadow_inset(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 // Clip to the box so shadow cannot bleed outside canvas.save(); - let r = &style.border_radius; + let resolved_r = style.border_radius.resolved(w, h); + let r = &resolved_r; if r.is_zero() { canvas.clip_rect(box_rect, ClipOp::Intersect, true); } else { @@ -2160,33 +2313,34 @@ fn paint_box_shadow_inset(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); if shadow.blur > 0.0 { - // CSS `box-shadow` blur length is a Gaussian sigma per CSS - // Backgrounds §7.2; Skia's mask-filter takes sigma directly. paint.set_mask_filter(skia_safe::MaskFilter::blur( skia_safe::BlurStyle::Normal, - shadow.blur, + blur_radius_to_sigma(shadow.blur), false, )); } - // Draw a large rect with a hole cut out, shifted by offset + spread. - // The blur on the outer edge of the hole creates the inset shadow. + // Centered frame, translated by `offset` via canvas.translate + // so the inner hole and outer edge shift together — matches + // Blink's DrawLooper offset semantics (box_painter_base.cc:566) + // and keeps the blur gradients symmetric across the box. let spread = shadow.spread; - let inner_rect = Rect::from_xywh( - shadow.offset_x + spread, - shadow.offset_y + spread, - w - spread * 2.0, - h - spread * 2.0, - ); - - // Outer rect large enough that its edges are outside the clip region - let expansion = shadow.blur * 2.0 + shadow.spread.abs() + 100.0; - let outer_rect = Rect::from_xywh( - -expansion + shadow.offset_x, - -expansion + shadow.offset_y, - w + expansion * 2.0, - h + expansion * 2.0, - ); + let inner_rect = Rect::from_xywh(spread, spread, w - spread * 2.0, h - spread * 2.0); + + // Outer rect per Blink's `AreaCastingShadowInHole` + // (box_painter_base.cc:511-522): outset hole by blur-radius + + // |negative_spread|, then union with the pre-offset position so + // the frame extends far enough to cover every pixel the shadow + // can reach after the translate below. Keeping the thickness at + // blur-radius lets inner/outer blur gradients overlap, which is + // what produces the soft fall-off toward the box center. + let outset = shadow.blur - shadow.spread.min(0.0); + let outer_l = (-outset).min(-outset - shadow.offset_x); + let outer_t = (-outset).min(-outset - shadow.offset_y); + let outer_r = (w + outset).max(w + outset - shadow.offset_x); + let outer_b = (h + outset).max(h + outset - shadow.offset_y); + let outer_rect = Rect::from_xywh(outer_l, outer_t, outer_r - outer_l, outer_b - outer_t); + canvas.translate((shadow.offset_x, shadow.offset_y)); // Build a path: outer rect minus inner rect (creates a frame). // EvenOdd fill makes the inner rect a hole. diff --git a/crates/grida-canvas/src/htmlcss/style.rs b/crates/grida-canvas/src/htmlcss/style.rs index 480827e4a..f6dcc7856 100644 --- a/crates/grida-canvas/src/htmlcss/style.rs +++ b/crates/grida-canvas/src/htmlcss/style.rs @@ -396,6 +396,13 @@ impl Outline { /// /// CSS: `border-radius: 10px / 20px` → each corner has (rx=10, ry=20). /// Skia: `RRect::set_rect_radii` takes `[Point; 4]` where each Point is (rx, ry). +/// +/// Percent components (`*_pct`, as fractions in [0, 1+]) stay unresolved +/// until paint time, when the border-box width/height is known. +/// Per CSS Backgrounds 3 §5.3: horizontal axis % resolves against box +/// width, vertical axis % against box height. Call [`Self::resolved`] +/// to materialize a px-only copy before consuming `*_x` / `*_y` fields +/// or [`Self::to_skia_radii`]. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct CornerRadii { pub tl_x: f32, @@ -406,6 +413,14 @@ pub struct CornerRadii { pub br_y: f32, pub bl_x: f32, pub bl_y: f32, + pub tl_x_pct: f32, + pub tl_y_pct: f32, + pub tr_x_pct: f32, + pub tr_y_pct: f32, + pub br_x_pct: f32, + pub br_y_pct: f32, + pub bl_x_pct: f32, + pub bl_y_pct: f32, } impl CornerRadii { @@ -420,6 +435,7 @@ impl CornerRadii { br_y: br, bl_x: bl, bl_y: bl, + ..Default::default() } } @@ -432,9 +448,42 @@ impl CornerRadii { && self.br_y == 0.0 && self.bl_x == 0.0 && self.bl_y == 0.0 + && self.tl_x_pct == 0.0 + && self.tl_y_pct == 0.0 + && self.tr_x_pct == 0.0 + && self.tr_y_pct == 0.0 + && self.br_x_pct == 0.0 + && self.br_y_pct == 0.0 + && self.bl_x_pct == 0.0 + && self.bl_y_pct == 0.0 + } + + /// Resolve percentage components against a box dimension, returning + /// a px-only `CornerRadii`. CSS Backgrounds 3 §5.3 — H axis against + /// width, V axis against height. + pub fn resolved(&self, w: f32, h: f32) -> Self { + Self { + tl_x: self.tl_x + self.tl_x_pct * w, + tl_y: self.tl_y + self.tl_y_pct * h, + tr_x: self.tr_x + self.tr_x_pct * w, + tr_y: self.tr_y + self.tr_y_pct * h, + br_x: self.br_x + self.br_x_pct * w, + br_y: self.br_y + self.br_y_pct * h, + bl_x: self.bl_x + self.bl_x_pct * w, + bl_y: self.bl_y + self.bl_y_pct * h, + tl_x_pct: 0.0, + tl_y_pct: 0.0, + tr_x_pct: 0.0, + tr_y_pct: 0.0, + br_x_pct: 0.0, + br_y_pct: 0.0, + bl_x_pct: 0.0, + bl_y_pct: 0.0, + } } /// Convert to Skia's `[Point; 4]` format for `RRect::set_rect_radii`. + /// Assumes percent fields have been resolved (see [`Self::resolved`]). pub fn to_skia_radii(&self) -> [skia_safe::Point; 4] { [ skia_safe::Point::new(self.tl_x, self.tl_y), @@ -444,7 +493,9 @@ impl CornerRadii { ] } - /// Max radius (for simplified single-value contexts like inline decoration). + /// Max px-radius (percent components are ignored). Used only for the + /// inline-decoration presence check in `collect.rs`, which needs a + /// single f32 and predates per-axis plumbing. pub fn max_radius(&self) -> f32 { self.tl_x .max(self.tl_y) @@ -647,8 +698,10 @@ pub enum StyleImage { /// slot. Our representation flattens this into a two-variant enum. #[derive(Debug, Clone)] pub enum BackgroundLayer { - /// Solid color fill (CSS `background-color`). - Solid(CGColor), + /// Solid color fill (CSS `background-color`). Per CSS Backgrounds 3 + /// §2.5 the color uses the `background-clip` value from the *final* + /// layer entry. + Solid { color: CGColor, clip: BackgroundBox }, /// Image layer with full CSS geometry (size, position, repeat, clip, origin). /// Chromium: `FillLayer` with image, size, position, repeat, clip, origin. Image(BackgroundImage), diff --git a/fixtures/test-html/L0/box-padding.html b/fixtures/test-html/L0/box-padding.html index c74036bef..1e79310ca 100644 --- a/fixtures/test-html/L0/box-padding.html +++ b/fixtures/test-html/L0/box-padding.html @@ -14,9 +14,10 @@ } .label { + width: 200px; + height: 16px; font-size: 11px; color: #666; - padding-bottom: 4px; } .columns { @@ -28,14 +29,12 @@ .outer { background: #eee; - border-radius: 8px; } .inner { background: #000; - border-radius: 4px; - font-size: 12px; - color: #fff; + width: 80px; + height: 24px; } .uniform { @@ -57,25 +56,25 @@
padding: 24px (uniform)
-
content
+
padding: 8px 32px (horizontal)
-
content
+
padding: 32px 8px (vertical)
-
content
+
padding: 8px 16px 32px 48px
-
content
+
diff --git a/fixtures/test-html/L0/layout-block-flow.html b/fixtures/test-html/L0/layout-block-flow.html new file mode 100644 index 000000000..9afd974ba --- /dev/null +++ b/fixtures/test-html/L0/layout-block-flow.html @@ -0,0 +1,53 @@ + + + + + Layout: Block flow + + + +
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-align-items.html b/fixtures/test-html/L0/layout-flex-align-items.html new file mode 100644 index 000000000..0be023279 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-align-items.html @@ -0,0 +1,88 @@ + + + + + Layout: Flex align-items + + + +
flex-start
+
+
+
+
+
+
flex-end
+
+
+
+
+
+
center
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-align-self.html b/fixtures/test-html/L0/layout-flex-align-self.html new file mode 100644 index 000000000..a709b287f --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-align-self.html @@ -0,0 +1,61 @@ + + + + + Layout: Flex Align Self + + + +
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-basis.html b/fixtures/test-html/L0/layout-flex-basis.html new file mode 100644 index 000000000..796a88e40 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-basis.html @@ -0,0 +1,52 @@ + + + + + Layout: Flex Basis + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-column-basic.html b/fixtures/test-html/L0/layout-flex-column-basic.html new file mode 100644 index 000000000..d767e2431 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-column-basic.html @@ -0,0 +1,95 @@ + + + + + Layout: Flex Column Basic + + + +
+
+
default (start)
+
+
+
+
+
+
+
+
gap 12
+
+
+
+
+
+
+
+
space-between
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-direction-reverse.html b/fixtures/test-html/L0/layout-flex-direction-reverse.html new file mode 100644 index 000000000..4212347e4 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-direction-reverse.html @@ -0,0 +1,81 @@ + + + + + Layout: Flex Direction Reverse + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-grow.html b/fixtures/test-html/L0/layout-flex-grow.html new file mode 100644 index 000000000..587c189a4 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-grow.html @@ -0,0 +1,77 @@ + + + + + Layout: Flex Grow + + + +
grow 1 / grow 1 / grow 1 (equal thirds)
+
+
+
+
+
+
80px fixed / grow 1 / 80px fixed
+
+
+
+
+
+
grow 1 / grow 2 (one third / two thirds)
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-row-basic.html b/fixtures/test-html/L0/layout-flex-row-basic.html new file mode 100644 index 000000000..f5f1bf517 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-row-basic.html @@ -0,0 +1,83 @@ + + + + + Layout: Flex Row Basic + + + +
default (start)
+
+
+
+
+
+
gap 16
+
+
+
+
+
+
justify-content: space-between
+
+
+
+
+
+
justify-content: flex-end
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-flex-wrap.html b/fixtures/test-html/L0/layout-flex-wrap.html new file mode 100644 index 000000000..200622d1f --- /dev/null +++ b/fixtures/test-html/L0/layout-flex-wrap.html @@ -0,0 +1,50 @@ + + + + + Layout: Flex Wrap + + + +
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-grid-autoflow.html b/fixtures/test-html/L0/layout-grid-autoflow.html new file mode 100644 index 000000000..12b28bed3 --- /dev/null +++ b/fixtures/test-html/L0/layout-grid-autoflow.html @@ -0,0 +1,56 @@ + + + + + Layout: Grid Autoflow + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-grid-basic.html b/fixtures/test-html/L0/layout-grid-basic.html new file mode 100644 index 000000000..8d8b59b18 --- /dev/null +++ b/fixtures/test-html/L0/layout-grid-basic.html @@ -0,0 +1,49 @@ + + + + + Layout: Grid Basic + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-grid-fr.html b/fixtures/test-html/L0/layout-grid-fr.html new file mode 100644 index 000000000..3612eb8d2 --- /dev/null +++ b/fixtures/test-html/L0/layout-grid-fr.html @@ -0,0 +1,68 @@ + + + + + Layout: Grid fractional (fr) tracks + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-grid-gap-asym.html b/fixtures/test-html/L0/layout-grid-gap-asym.html new file mode 100644 index 000000000..e7820d57c --- /dev/null +++ b/fixtures/test-html/L0/layout-grid-gap-asym.html @@ -0,0 +1,54 @@ + + + + + Layout: Grid asymmetric row/column gaps + + + +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/layout-grid-span.html b/fixtures/test-html/L0/layout-grid-span.html new file mode 100644 index 000000000..de272f652 --- /dev/null +++ b/fixtures/test-html/L0/layout-grid-span.html @@ -0,0 +1,70 @@ + + + + + Layout: Grid span placement + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-aspect-ratio.html b/fixtures/test-html/L0/paint-aspect-ratio.html new file mode 100644 index 000000000..bce4bbc09 --- /dev/null +++ b/fixtures/test-html/L0/paint-aspect-ratio.html @@ -0,0 +1,52 @@ + + + + + Paint: Aspect Ratio + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-background-clip-boxes.html b/fixtures/test-html/L0/paint-background-clip-boxes.html new file mode 100644 index 000000000..c01112b69 --- /dev/null +++ b/fixtures/test-html/L0/paint-background-clip-boxes.html @@ -0,0 +1,72 @@ + + + + + Paint: Background-clip (box variants) + + + +
+
+
border-box
+
+
+
+
padding-box
+
+
+
+
content-box
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-background-gradient-linear-simple.html b/fixtures/test-html/L0/paint-background-gradient-linear-simple.html new file mode 100644 index 000000000..830f926d5 --- /dev/null +++ b/fixtures/test-html/L0/paint-background-gradient-linear-simple.html @@ -0,0 +1,90 @@ + + + + + Paint: Linear Gradient (axis-aligned, two-stop) + + + +
+
+
to bottom
+
+
+
+
to top
+
+
+
+
to right
+
+
+
+
to left
+
+
+
+
red→blue (→)
+
+
+
+
red→blue (↓)
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-double-rect.html b/fixtures/test-html/L0/paint-border-double-rect.html new file mode 100644 index 000000000..28b8d7a97 --- /dev/null +++ b/fixtures/test-html/L0/paint-border-double-rect.html @@ -0,0 +1,63 @@ + + + + + Paint: Border Double (rectangular) + + + +
+
+
9px double black
+
+
+
+
12px double red
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-radius-individual.html b/fixtures/test-html/L0/paint-border-radius-individual.html new file mode 100644 index 000000000..27fc4602b --- /dev/null +++ b/fixtures/test-html/L0/paint-border-radius-individual.html @@ -0,0 +1,67 @@ + + + + + Paint: Border Radius Individual Corners + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-solid.html b/fixtures/test-html/L0/paint-border-solid.html new file mode 100644 index 000000000..d98bc1253 --- /dev/null +++ b/fixtures/test-html/L0/paint-border-solid.html @@ -0,0 +1,87 @@ + + + + + Paint: Solid Border + + + +
+
+
1px solid
+
+
+
+
3px solid
+
+
+
+
8px solid
+
+
+
+
top-only 4px
+
+
+
+
asym-width
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-style-dashed.html b/fixtures/test-html/L0/paint-border-style-dashed.html new file mode 100644 index 000000000..9299ef090 --- /dev/null +++ b/fixtures/test-html/L0/paint-border-style-dashed.html @@ -0,0 +1,67 @@ + + + + + Paint: Border Style Dashed + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-style-dotted.html b/fixtures/test-html/L0/paint-border-style-dotted.html new file mode 100644 index 000000000..20a5c6d3d --- /dev/null +++ b/fixtures/test-html/L0/paint-border-style-dotted.html @@ -0,0 +1,66 @@ + + + + + Paint: Border Style Dotted + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-border-translucent.html b/fixtures/test-html/L0/paint-border-translucent.html new file mode 100644 index 000000000..35f5d3998 --- /dev/null +++ b/fixtures/test-html/L0/paint-border-translucent.html @@ -0,0 +1,70 @@ + + + + + Paint: Translucent Border + + + +
+
+
4px red@0.5
+
+
+
+
12px blue@0.5
+
+
+
+
8px green@0.5
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-box-shadow-blur.html b/fixtures/test-html/L0/paint-box-shadow-blur.html new file mode 100644 index 000000000..f26b848a5 --- /dev/null +++ b/fixtures/test-html/L0/paint-box-shadow-blur.html @@ -0,0 +1,76 @@ + + + + + Paint: Box Shadow Blur + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-box-shadow-inset-blur.html b/fixtures/test-html/L0/paint-box-shadow-inset-blur.html new file mode 100644 index 000000000..50ab85ad4 --- /dev/null +++ b/fixtures/test-html/L0/paint-box-shadow-inset-blur.html @@ -0,0 +1,76 @@ + + + + + Paint: Box Shadow Inset Blur + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-box-shadow-inset-solid.html b/fixtures/test-html/L0/paint-box-shadow-inset-solid.html new file mode 100644 index 000000000..be0006047 --- /dev/null +++ b/fixtures/test-html/L0/paint-box-shadow-inset-solid.html @@ -0,0 +1,84 @@ + + + + + Paint: Inset Box Shadow (no blur) + + + +
+
+
inset spread 6
+
+
+
+
inset 8/8
+
+
+
+
inset -8/-8
+
+
+
+
inset 4/4 spread 2
+
+
+
+
inset colored
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-box-shadow-multiple.html b/fixtures/test-html/L0/paint-box-shadow-multiple.html new file mode 100644 index 000000000..7799680a7 --- /dev/null +++ b/fixtures/test-html/L0/paint-box-shadow-multiple.html @@ -0,0 +1,61 @@ + + + + + Paint: Box Shadow Multiple + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-box-shadow-solid.html b/fixtures/test-html/L0/paint-box-shadow-solid.html new file mode 100644 index 000000000..8d8622d12 --- /dev/null +++ b/fixtures/test-html/L0/paint-box-shadow-solid.html @@ -0,0 +1,84 @@ + + + + + Paint: Box Shadow (Solid / No Blur) + + + +
+
+
offset 6/6
+
+
+
+
offset -6/-6
+
+
+
+
spread 4
+
+
+
+
spread 2 + offset 4
+
+
+
+
colored #c00
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-clip-path-circle.html b/fixtures/test-html/L0/paint-clip-path-circle.html new file mode 100644 index 000000000..811db5fe1 --- /dev/null +++ b/fixtures/test-html/L0/paint-clip-path-circle.html @@ -0,0 +1,53 @@ + + + + + Paint: Clip-Path Circle + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-clip-path-ellipse.html b/fixtures/test-html/L0/paint-clip-path-ellipse.html new file mode 100644 index 000000000..adef5d69f --- /dev/null +++ b/fixtures/test-html/L0/paint-clip-path-ellipse.html @@ -0,0 +1,53 @@ + + + + + Paint: Clip-Path Ellipse + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-clip-path-inset.html b/fixtures/test-html/L0/paint-clip-path-inset.html new file mode 100644 index 000000000..8c75195de --- /dev/null +++ b/fixtures/test-html/L0/paint-clip-path-inset.html @@ -0,0 +1,77 @@ + + + + + Paint: Clip-Path Inset + + + +
+
+
inset 10px
+
+
+
+
inset 10/20/15/25
+
+
+
+
inset 10%/20%
+
+
+
+
inset 10px round 16px
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-clip-path-polygon.html b/fixtures/test-html/L0/paint-clip-path-polygon.html new file mode 100644 index 000000000..3a040fa75 --- /dev/null +++ b/fixtures/test-html/L0/paint-clip-path-polygon.html @@ -0,0 +1,49 @@ + + + + + Paint: Clip-Path Polygon + + + +
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-color-hex-alpha.html b/fixtures/test-html/L0/paint-color-hex-alpha.html new file mode 100644 index 000000000..0abc268c3 --- /dev/null +++ b/fixtures/test-html/L0/paint-color-hex-alpha.html @@ -0,0 +1,64 @@ + + + + + Paint: Hex/Alpha Color Forms + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-display-none.html b/fixtures/test-html/L0/paint-display-none.html new file mode 100644 index 000000000..98a5ae73d --- /dev/null +++ b/fixtures/test-html/L0/paint-display-none.html @@ -0,0 +1,47 @@ + + + + + Paint: Display None + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-filter-blur.html b/fixtures/test-html/L0/paint-filter-blur.html new file mode 100644 index 000000000..697f9f833 --- /dev/null +++ b/fixtures/test-html/L0/paint-filter-blur.html @@ -0,0 +1,64 @@ + + + + + Paint: Filter blur() + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-filter-chain.html b/fixtures/test-html/L0/paint-filter-chain.html new file mode 100644 index 000000000..113df9ae0 --- /dev/null +++ b/fixtures/test-html/L0/paint-filter-chain.html @@ -0,0 +1,53 @@ + + + + + Paint: Filter chain + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-filter-drop-shadow.html b/fixtures/test-html/L0/paint-filter-drop-shadow.html new file mode 100644 index 000000000..b4d96d158 --- /dev/null +++ b/fixtures/test-html/L0/paint-filter-drop-shadow.html @@ -0,0 +1,64 @@ + + + + + Paint: Filter drop-shadow + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-filter-simple.html b/fixtures/test-html/L0/paint-filter-simple.html new file mode 100644 index 000000000..7fae8343c --- /dev/null +++ b/fixtures/test-html/L0/paint-filter-simple.html @@ -0,0 +1,61 @@ + + + + + Paint: Filter (simple functions) + + + +
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-gradient-radial.html b/fixtures/test-html/L0/paint-gradient-radial.html index 6c7cf737a..47d44c232 100644 --- a/fixtures/test-html/L0/paint-gradient-radial.html +++ b/fixtures/test-html/L0/paint-gradient-radial.html @@ -4,19 +4,16 @@ Paint: Radial Gradient
-
-
circle
-
circle
-
-
-
ellipse
-
ellipse
-
-
-
multi-stop
-
multi-stop
-
-
-
at top left
-
at top left
-
-
-
at bottom right
-
at bottom right
-
-
-
at 25% 75%
-
at 25% 75%
-
-
-
at 30px 30px
-
at 30px
-
-
-
closest-side
-
closest-side
-
-
-
farthest-corner
-
farthest-corner
-
-
-
closest-corner @ 25% 25%
-
closest-corner
-
-
-
circle 40px
-
circle 40px
-
-
-
ellipse 80px 30px
-
ellipse 80×30
-
-
-
stacked on solid bg
-
stacked
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/test-html/L0/paint-individual-transform-props.html b/fixtures/test-html/L0/paint-individual-transform-props.html new file mode 100644 index 000000000..b239d4aca --- /dev/null +++ b/fixtures/test-html/L0/paint-individual-transform-props.html @@ -0,0 +1,61 @@ + + + + + Paint: Individual Transform Properties + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-margin-auto-center.html b/fixtures/test-html/L0/paint-margin-auto-center.html new file mode 100644 index 000000000..ca2e574a5 --- /dev/null +++ b/fixtures/test-html/L0/paint-margin-auto-center.html @@ -0,0 +1,52 @@ + + + + + Paint: Margin Auto Centering + + + +
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-margin-simple.html b/fixtures/test-html/L0/paint-margin-simple.html new file mode 100644 index 000000000..79f8c6b65 --- /dev/null +++ b/fixtures/test-html/L0/paint-margin-simple.html @@ -0,0 +1,56 @@ + + + + + Paint: Margin (simple) + + + +
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-max-min-size.html b/fixtures/test-html/L0/paint-max-min-size.html new file mode 100644 index 000000000..3601a3838 --- /dev/null +++ b/fixtures/test-html/L0/paint-max-min-size.html @@ -0,0 +1,77 @@ + + + + + Paint: max-/min-width/height + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-mix-blend-mode.html b/fixtures/test-html/L0/paint-mix-blend-mode.html new file mode 100644 index 000000000..9b7dc5bc8 --- /dev/null +++ b/fixtures/test-html/L0/paint-mix-blend-mode.html @@ -0,0 +1,75 @@ + + + + + Paint: Mix Blend Mode + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-opacity-levels.html b/fixtures/test-html/L0/paint-opacity-levels.html new file mode 100644 index 000000000..83c50cb71 --- /dev/null +++ b/fixtures/test-html/L0/paint-opacity-levels.html @@ -0,0 +1,91 @@ + + + + + Paint: Opacity Levels + + + +
+
+
1.0
+
+
+
+
0.75
+
+
+
+
0.5
+
+
+
+
0.25
+
+
+
+
0.1
+
+
+
+
0
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-opacity-nested.html b/fixtures/test-html/L0/paint-opacity-nested.html new file mode 100644 index 000000000..99c6cf33c --- /dev/null +++ b/fixtures/test-html/L0/paint-opacity-nested.html @@ -0,0 +1,71 @@ + + + + + Paint: Opacity Nested + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-opacity.html b/fixtures/test-html/L0/paint-opacity.html index e89d7f285..122c3283d 100644 --- a/fixtures/test-html/L0/paint-opacity.html +++ b/fixtures/test-html/L0/paint-opacity.html @@ -78,19 +78,19 @@
1.0
-
100%
+
0.75
-
75%
+
0.5
-
50%
+
0.25
-
25%
+
diff --git a/fixtures/test-html/L0/paint-outline-double-rect.html b/fixtures/test-html/L0/paint-outline-double-rect.html new file mode 100644 index 000000000..06ec5d0ad --- /dev/null +++ b/fixtures/test-html/L0/paint-outline-double-rect.html @@ -0,0 +1,64 @@ + + + + + Paint: Outline Double (rectangular) + + + +
+
+
9px double
+
+
+
+
9px double + offset 6
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-outline-offset.html b/fixtures/test-html/L0/paint-outline-offset.html new file mode 100644 index 000000000..0f448ded9 --- /dev/null +++ b/fixtures/test-html/L0/paint-outline-offset.html @@ -0,0 +1,52 @@ + + + + + Paint: Outline Offset + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-outline-radius.html b/fixtures/test-html/L0/paint-outline-radius.html new file mode 100644 index 000000000..d39f32d2c --- /dev/null +++ b/fixtures/test-html/L0/paint-outline-radius.html @@ -0,0 +1,97 @@ + + + + + Paint: Outline + border-radius + + + +
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-outline-solid.html b/fixtures/test-html/L0/paint-outline-solid.html new file mode 100644 index 000000000..935cf8a92 --- /dev/null +++ b/fixtures/test-html/L0/paint-outline-solid.html @@ -0,0 +1,85 @@ + + + + + Paint: Solid Outline + + + +
+
+
1px solid
+
+
+
+
3px solid
+
+
+
+
6px solid
+
+
+
+
3px solid #c00
+
+
+
+
offset +8
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-overflow-hidden.html b/fixtures/test-html/L0/paint-overflow-hidden.html new file mode 100644 index 000000000..76c5b513a --- /dev/null +++ b/fixtures/test-html/L0/paint-overflow-hidden.html @@ -0,0 +1,65 @@ + + + + + Paint: Overflow Hidden + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-padding-simple.html b/fixtures/test-html/L0/paint-padding-simple.html new file mode 100644 index 000000000..6f0d76818 --- /dev/null +++ b/fixtures/test-html/L0/paint-padding-simple.html @@ -0,0 +1,65 @@ + + + + + Paint: Padding (simple) + + + +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-position-absolute-simple.html b/fixtures/test-html/L0/paint-position-absolute-simple.html new file mode 100644 index 000000000..4ca686db2 --- /dev/null +++ b/fixtures/test-html/L0/paint-position-absolute-simple.html @@ -0,0 +1,70 @@ + + + + + Paint: Position Absolute (simple) + + + +
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-position-relative.html b/fixtures/test-html/L0/paint-position-relative.html new file mode 100644 index 000000000..cf0883d0b --- /dev/null +++ b/fixtures/test-html/L0/paint-position-relative.html @@ -0,0 +1,60 @@ + + + + + Paint: Position Relative + + + +
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-combined.html b/fixtures/test-html/L0/paint-transform-combined.html new file mode 100644 index 000000000..9ce2b70bd --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-combined.html @@ -0,0 +1,56 @@ + + + + + Paint: Transform (combined functions) + + + +
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-matrix.html b/fixtures/test-html/L0/paint-transform-matrix.html new file mode 100644 index 000000000..bbeccadd7 --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-matrix.html @@ -0,0 +1,88 @@ + + + + + Paint: transform matrix() + + + +
+
+
+
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-origin.html b/fixtures/test-html/L0/paint-transform-origin.html new file mode 100644 index 000000000..7028a94ca --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-origin.html @@ -0,0 +1,63 @@ + + + + + Paint: Transform Origin + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-rotate.html b/fixtures/test-html/L0/paint-transform-rotate.html new file mode 100644 index 000000000..74853d394 --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-rotate.html @@ -0,0 +1,76 @@ + + + + + Paint: Transform Rotate + + + +
+
+
rotate 90deg
+
+
+
+
rotate 180deg
+
+
+
+
rotate -90deg
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-scale.html b/fixtures/test-html/L0/paint-transform-scale.html new file mode 100644 index 000000000..b85b5fa1a --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-scale.html @@ -0,0 +1,84 @@ + + + + + Paint: Transform Scale + + + +
+
+
scale 0.5
+
+
+
+
scale 0.75, 0.5
+
+
+
+
scaleX 0.5
+
+
+
+
scaleY 0.75
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-skew.html b/fixtures/test-html/L0/paint-transform-skew.html new file mode 100644 index 000000000..8b9109b98 --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-skew.html @@ -0,0 +1,48 @@ + + + + + Paint: Transform Skew + + + +
+
+
+ + diff --git a/fixtures/test-html/L0/paint-transform-translate.html b/fixtures/test-html/L0/paint-transform-translate.html new file mode 100644 index 000000000..3ed396603 --- /dev/null +++ b/fixtures/test-html/L0/paint-transform-translate.html @@ -0,0 +1,84 @@ + + + + + Paint: Transform Translate + + + +
+
+
translate 16/12
+
+
+
+
translateX 20
+
+
+
+
translateY -8
+
+
+
+
translate -12/-16
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-visibility.html b/fixtures/test-html/L0/paint-visibility.html new file mode 100644 index 000000000..0579475b3 --- /dev/null +++ b/fixtures/test-html/L0/paint-visibility.html @@ -0,0 +1,50 @@ + + + + + Paint: Visibility + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/L0/paint-z-index-simple.html b/fixtures/test-html/L0/paint-z-index-simple.html new file mode 100644 index 000000000..9645a449c --- /dev/null +++ b/fixtures/test-html/L0/paint-z-index-simple.html @@ -0,0 +1,62 @@ + + + + + Paint: Z-Index (simple stacking) + + + +
+
+
+
+
+ + diff --git a/fixtures/test-html/README.md b/fixtures/test-html/README.md index c99c1371f..f33610fe5 100644 --- a/fixtures/test-html/README.md +++ b/fixtures/test-html/README.md @@ -123,6 +123,27 @@ sides should now be at identical dimensions. layout changes its natural cull, invalidating `viewport.height` in the suite. Re-measure and update. +## Captions and labels + +Short captions next to each specimen (`"0.75"`, `"padding: 24px"`, +etc.) are welcome — they help humans reading the fixture identify what +each region is testing. Two rules: + +- **Keep them short.** The caption is not the subject; if it grows + long enough to shape the layout it's in the way. +- **Don't let captions drive layout.** When captions sit inside flex + items, grid cells, or stretched blocks, pin the enclosing element's + dimensions (`width`, `height`) so font-advance-width differences + between Chromium and cg can't leak into box geometry. Otherwise a + 1px shaping difference in "padding: 24px (uniform)" propagates to + every following sibling. + +When text is incidental (labels, glyph placeholders, captions), inject +`_reftest/hide-text.css` via the suite's `extra_css` — it neutralizes +color, text-shadow, and line-height while preserving advance widths +and block flow. The suite defaults already pull it in; see +`.agents/skills/cg-reftest/SKILL.md` for details. + ## Adding a new fixture 1. **Name** — `-[-].html`. The filename is diff --git a/fixtures/test-html/_reftest/transparent-body.css b/fixtures/test-html/_reftest/transparent-body.css new file mode 100644 index 000000000..d25929b70 --- /dev/null +++ b/fixtures/test-html/_reftest/transparent-body.css @@ -0,0 +1,14 @@ +/* + * Reftest scoring helper — force html/body to paint no background so that + * PNG alpha encodes "this pixel was drawn by the CSS cascade," not "the + * viewport happens to be white." The reftest harness uses alpha>0 as the + * content mask for its scoring denominator. + * + * Fixtures that legitimately test background or canvas-blend-mode behavior + * should opt out via a per-fixture override (future: `canvas_bg: "#fff"`). + */ +html, +body { + background: transparent !important; + background-color: transparent !important; +} diff --git a/fixtures/test-html/suites/L0.coverage.json b/fixtures/test-html/suites/L0.coverage.json index 688fbce4c..d9df49095 100644 --- a/fixtures/test-html/suites/L0.coverage.json +++ b/fixtures/test-html/suites/L0.coverage.json @@ -4,7 +4,10 @@ "defaults": { "viewport": { "width": 600, "height": 800 }, "wait_for": ["fonts", "networkidle"], - "extra_css": ["../_reftest/hide-text.css"], + "extra_css": [ + "../_reftest/hide-text.css", + "../_reftest/transparent-body.css" + ], "full_page": true }, "fixtures": [ @@ -14,10 +17,73 @@ }, { "path": "../L0/box-padding.html", - "viewport": { "width": 600, "height": 222 } + "viewport": { "width": 600, "height": 256 } }, { "path": "../L0/paint-background-solid.html" }, { "path": "../L0/paint-opacity.html" }, - { "path": "../L0/paint-border-radius.html" } + { "path": "../L0/paint-border-radius.html" }, + { "path": "../L0/paint-border-solid.html" }, + { "path": "../L0/paint-outline-solid.html" }, + { "path": "../L0/paint-box-shadow-solid.html" }, + { "path": "../L0/paint-background-gradient-linear-simple.html" }, + { "path": "../L0/paint-clip-path-inset.html" }, + { "path": "../L0/paint-box-shadow-inset-solid.html" }, + { "path": "../L0/paint-transform-translate.html" }, + { "path": "../L0/paint-transform-scale.html" }, + { "path": "../L0/paint-transform-rotate.html" }, + { "path": "../L0/paint-opacity-levels.html" }, + { "path": "../L0/paint-position-absolute-simple.html" }, + { "path": "../L0/paint-z-index-simple.html" }, + { "path": "../L0/paint-overflow-hidden.html" }, + { "path": "../L0/paint-visibility.html" }, + { "path": "../L0/paint-display-none.html" }, + { "path": "../L0/layout-flex-row-basic.html" }, + { "path": "../L0/layout-flex-column-basic.html" }, + { "path": "../L0/paint-background-clip-boxes.html" }, + { "path": "../L0/paint-border-translucent.html" }, + { "path": "../L0/paint-margin-simple.html" }, + { "path": "../L0/paint-padding-simple.html" }, + { "path": "../L0/paint-border-double-rect.html" }, + { "path": "../L0/paint-aspect-ratio.html" }, + { "path": "../L0/paint-max-min-size.html" }, + { "path": "../L0/paint-position-relative.html" }, + { "path": "../L0/layout-flex-align-items.html" }, + { "path": "../L0/paint-margin-auto-center.html" }, + { "path": "../L0/layout-flex-grow.html" }, + { "path": "../L0/paint-color-hex-alpha.html" }, + { "path": "../L0/layout-flex-wrap.html" }, + { "path": "../L0/layout-grid-basic.html" }, + { "path": "../L0/layout-grid-fr.html" }, + { "path": "../L0/layout-grid-span.html" }, + { "path": "../L0/layout-block-flow.html" }, + { "path": "../L0/paint-outline-double-rect.html" }, + { "path": "../L0/paint-border-radius-individual.html" }, + { "path": "../L0/paint-filter-simple.html" }, + { "path": "../L0/paint-mix-blend-mode.html" }, + { "path": "../L0/paint-filter-chain.html" }, + { "path": "../L0/layout-grid-gap-asym.html" }, + { "path": "../L0/paint-transform-combined.html" }, + { "path": "../L0/paint-transform-origin.html" }, + { "path": "../L0/paint-individual-transform-props.html" }, + { "path": "../L0/paint-clip-path-circle.html" }, + { "path": "../L0/paint-clip-path-polygon.html" }, + { "path": "../L0/paint-clip-path-ellipse.html" }, + { "path": "../L0/paint-opacity-nested.html" }, + { "path": "../L0/layout-flex-direction-reverse.html" }, + { "path": "../L0/paint-box-shadow-multiple.html" }, + { "path": "../L0/paint-outline-offset.html" }, + { "path": "../L0/layout-flex-align-self.html" }, + { "path": "../L0/layout-grid-autoflow.html" }, + { "path": "../L0/layout-flex-basis.html" }, + { "path": "../L0/paint-transform-skew.html" }, + { "path": "../L0/paint-gradient-radial.html" }, + { "path": "../L0/paint-outline-radius.html" }, + { "path": "../L0/paint-box-shadow-blur.html" }, + { "path": "../L0/paint-box-shadow-inset-blur.html" }, + { "path": "../L0/paint-border-style-dashed.html" }, + { "path": "../L0/paint-border-style-dotted.html" }, + { "path": "../L0/paint-filter-drop-shadow.html" }, + { "path": "../L0/paint-transform-matrix.html" }, + { "path": "../L0/paint-filter-blur.html" } ] } diff --git a/fixtures/test-html/suites/L0.exact.json b/fixtures/test-html/suites/L0.exact.json index eed77d830..2e21f5434 100644 --- a/fixtures/test-html/suites/L0.exact.json +++ b/fixtures/test-html/suites/L0.exact.json @@ -3,19 +3,88 @@ "description": "Byte-exact L0 fixtures. Every fixture here MUST stay at 100.00% similarity against the Chromium oracle. Any drop = real regression.", "gate": { "threshold": 0, - "aa": false, + "aa": true, "floor": 1.0 }, "defaults": { "viewport": { "width": 600, "height": 800 }, "wait_for": ["fonts", "networkidle"], - "extra_css": ["../_reftest/hide-text.css"], + "extra_css": [ + "../_reftest/hide-text.css", + "../_reftest/transparent-body.css" + ], "full_page": true }, "fixtures": [ { "path": "../L0/box-dimensions.html", "viewport": { "width": 600, "height": 522 } - } + }, + { + "path": "../L0/box-padding.html", + "viewport": { "width": 600, "height": 256 } + }, + { "path": "../L0/paint-opacity.html" }, + { "path": "../L0/paint-background-solid.html" }, + { "path": "../L0/paint-border-radius.html" }, + { "path": "../L0/paint-border-solid.html" }, + { "path": "../L0/paint-outline-solid.html" }, + { "path": "../L0/paint-box-shadow-solid.html" }, + { "path": "../L0/paint-clip-path-inset.html" }, + { "path": "../L0/paint-box-shadow-inset-solid.html" }, + { "path": "../L0/paint-transform-translate.html" }, + { "path": "../L0/paint-transform-scale.html" }, + { "path": "../L0/paint-opacity-levels.html" }, + { "path": "../L0/paint-position-absolute-simple.html" }, + { "path": "../L0/paint-z-index-simple.html" }, + { "path": "../L0/paint-overflow-hidden.html" }, + { "path": "../L0/paint-visibility.html" }, + { "path": "../L0/paint-display-none.html" }, + { "path": "../L0/layout-flex-row-basic.html" }, + { "path": "../L0/layout-flex-column-basic.html" }, + { "path": "../L0/paint-background-clip-boxes.html" }, + { "path": "../L0/paint-border-translucent.html" }, + { "path": "../L0/paint-margin-simple.html" }, + { "path": "../L0/paint-padding-simple.html" }, + { "path": "../L0/paint-border-double-rect.html" }, + { "path": "../L0/paint-aspect-ratio.html" }, + { "path": "../L0/paint-max-min-size.html" }, + { "path": "../L0/paint-position-relative.html" }, + { "path": "../L0/layout-flex-align-items.html" }, + { "path": "../L0/paint-margin-auto-center.html" }, + { "path": "../L0/layout-flex-grow.html" }, + { "path": "../L0/paint-color-hex-alpha.html" }, + { "path": "../L0/layout-flex-wrap.html" }, + { "path": "../L0/layout-grid-basic.html" }, + { "path": "../L0/layout-grid-fr.html" }, + { "path": "../L0/layout-grid-span.html" }, + { "path": "../L0/layout-block-flow.html" }, + { "path": "../L0/paint-outline-double-rect.html" }, + { "path": "../L0/paint-border-radius-individual.html" }, + { "path": "../L0/paint-filter-simple.html" }, + { "path": "../L0/paint-mix-blend-mode.html" }, + { "path": "../L0/paint-filter-chain.html" }, + { "path": "../L0/layout-grid-gap-asym.html" }, + { "path": "../L0/paint-transform-combined.html" }, + { "path": "../L0/paint-transform-origin.html" }, + { "path": "../L0/paint-individual-transform-props.html" }, + { "path": "../L0/paint-clip-path-circle.html" }, + { "path": "../L0/paint-clip-path-polygon.html" }, + { "path": "../L0/paint-clip-path-ellipse.html" }, + { "path": "../L0/paint-opacity-nested.html" }, + { "path": "../L0/layout-flex-direction-reverse.html" }, + { "path": "../L0/paint-box-shadow-multiple.html" }, + { "path": "../L0/paint-outline-offset.html" }, + { "path": "../L0/layout-flex-align-self.html" }, + { "path": "../L0/layout-grid-autoflow.html" }, + { "path": "../L0/layout-flex-basis.html" }, + { "path": "../L0/paint-transform-skew.html" }, + { "path": "../L0/paint-outline-radius.html" }, + { "path": "../L0/paint-box-shadow-blur.html" }, + { "path": "../L0/paint-box-shadow-inset-blur.html" }, + { "path": "../L0/paint-border-style-dashed.html" }, + { "path": "../L0/paint-filter-drop-shadow.html" }, + { "path": "../L0/paint-transform-matrix.html" }, + { "path": "../L0/paint-filter-blur.html" } ] } diff --git a/packages/grida-reftest/src/cli.ts b/packages/grida-reftest/src/cli.ts index 1d72cf0de..2cd17a5a6 100644 --- a/packages/grida-reftest/src/cli.ts +++ b/packages/grida-reftest/src/cli.ts @@ -28,8 +28,12 @@ program ) .option( "--aa", - "ignore anti-aliased edges (pixelmatch includeAA=false)", - false + "ignore anti-aliased edges (pixelmatch includeAA=false) — default", + true + ) + .option( + "--no-aa", + "strict: count anti-aliased pixels as diffs (pixelmatch includeAA=true)" ) .option( "--bg ", @@ -108,7 +112,8 @@ program .addOption( new Option("--threshold ", "pixelmatch YIQ threshold per pixel") ) - .option("--aa", "ignore anti-aliased edges") + .option("--aa", "ignore anti-aliased edges (default)") + .option("--no-aa", "strict: count AA pixels as diffs") .option("--bg ", "composite background: white|black") .option("--mask ", "scoring denominator: alpha|none") .option("--overwrite", "clear output dir on start") @@ -180,7 +185,7 @@ program ? parseNumber(opts.threshold, "--threshold", 0, 1) : (config?.diff?.threshold ?? 0.1); const aa = - opts.aa !== undefined ? Boolean(opts.aa) : (config?.diff?.aa ?? false); + opts.aa !== undefined ? Boolean(opts.aa) : (config?.diff?.aa ?? true); const bg = opts.bg ? parseBg(opts.bg) : (config?.bg ?? "white"); const mask = opts.mask ? parseMask(opts.mask) diff --git a/packages/grida-reftest/src/compare.ts b/packages/grida-reftest/src/compare.ts index 32b6dcb47..3b60f3e8e 100644 --- a/packages/grida-reftest/src/compare.ts +++ b/packages/grida-reftest/src/compare.ts @@ -11,7 +11,9 @@ import type { } from "./types.js"; const DEFAULT_THRESHOLD = 0.1; -const DEFAULT_AA = false; +// Default: ignore anti-aliased edges (pixelmatch `includeAA: false`). Cross-engine +// AA coverage differences are not a real diff; set `aa: false` for strict mode. +const DEFAULT_AA = true; const DEFAULT_BG: BgColor = "white"; const DEFAULT_MASK: ScoringMask = "alpha";