diff --git a/crates/n0/src/drawlist.rs b/crates/n0/src/drawlist.rs index 887e8765..99815e36 100644 --- a/crates/n0/src/drawlist.rs +++ b/crates/n0/src/drawlist.rs @@ -299,12 +299,23 @@ impl StrokeDashPhase { Self(value) } - /// The local-space offset into the paired dash interval cycle. + /// The offset into the paired dash interval cycle, in the stroke's + /// declared construction space. pub const fn value(self) -> f32 { self.0 } } +/// Private projection of the resolved coordinate system in which a stroke is +/// dashed and widened. The source-neutral contract owns the meaning; this enum +/// only keeps the painter from recovering it from transforms. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum StrokeSpace { + #[default] + Local, + Frame, +} + /// One checked alpha stage applied after each paint's intrinsic alpha has /// materialized. /// @@ -448,6 +459,7 @@ pub enum ItemKind { geometry: ResolvedPatternGeometry, pattern: Arc, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -485,6 +497,7 @@ pub enum ItemKind { corner_radius: RectangularCornerRadius, corner_smoothing: CornerSmoothing, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -492,6 +505,7 @@ pub enum ItemKind { w: f32, h: f32, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -504,6 +518,7 @@ pub enum ItemKind { w: f32, h: f32, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -515,6 +530,7 @@ pub enum ItemKind { paint_w: f32, paint_h: f32, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -523,6 +539,7 @@ pub enum ItemKind { h: f32, path: Arc, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -531,6 +548,7 @@ pub enum ItemKind { paint_w: f32, paint_h: f32, stroke: Stroke, + space: StrokeSpace, dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, }, @@ -980,6 +998,7 @@ fn emit( corner_radius: values.corner_radius(id), corner_smoothing, stroke, + space: StrokeSpace::Local, dash_phase: StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -989,6 +1008,7 @@ fn emit( w: b.w, h: b.h, stroke, + space: StrokeSpace::Local, dash_phase: StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1002,6 +1022,7 @@ fn emit( paint_w: b.w, paint_h: b.h, stroke, + space: StrokeSpace::Local, dash_phase: StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1012,6 +1033,7 @@ fn emit( h: b.h, path: Arc::clone(resolved.resolved_path_of(id)), stroke, + space: StrokeSpace::Local, dash_phase: StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1023,6 +1045,7 @@ fn emit( paint_w: b.w, paint_h: b.h, stroke, + space: StrokeSpace::Local, dash_phase: StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, diff --git a/crates/n0/src/drawlist_vector_join_spike.rs b/crates/n0/src/drawlist_vector_join_spike.rs index af2cb449..a34155b5 100644 --- a/crates/n0/src/drawlist_vector_join_spike.rs +++ b/crates/n0/src/drawlist_vector_join_spike.rs @@ -1193,6 +1193,7 @@ fn compile_vectors( corner_radius: corners.model(), corner_smoothing: CornerSmoothing::default(), stroke, + space: crate::drawlist::StrokeSpace::Local, dash_phase: crate::drawlist::StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1200,6 +1201,7 @@ fn compile_vectors( w: *w, h: *h, stroke, + space: crate::drawlist::StrokeSpace::Local, dash_phase: crate::drawlist::StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1212,6 +1214,7 @@ fn compile_vectors( .expect("every candidate path was lowered once"), ), stroke, + space: crate::drawlist::StrokeSpace::Local, dash_phase: crate::drawlist::StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, @@ -1230,6 +1233,7 @@ fn compile_vectors( paint_w: *paint_w, paint_h: *paint_h, stroke, + space: crate::drawlist::StrokeSpace::Local, dash_phase: crate::drawlist::StrokeDashPhase::ZERO, post_paint_opacity: PostPaintOpacity::IDENTITY, }, diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index d08ad73c..7a79783d 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -36,7 +36,7 @@ use crate::drawlist::{ ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterLightSource, ResolvedFilterMorphology, ResolvedFilterNode, ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, ResolvedPattern, - ResolvedPatternGeometry, StrokeDashPhase, + ResolvedPatternGeometry, StrokeDashPhase, StrokeSpace, }; use crate::frame::FrameExecutionError; use crate::paint::PaintCtx; @@ -621,15 +621,29 @@ pub fn compile(resolved: Frame) -> Result { } Geometry::Path(_) => to_affine(node.transform), }; - // A stroke paints outside the geometry, so the covered area — what the - // damage policy repaints — is the local geometry inflated by the - // stroke's own reach, mapped through the same transform. Reach is a - // wide derived fact: project it in f64, then intersect with the frame - // clip before encoding the finite bounded RectF used by damage. Paint - // reference boxes above deliberately remain the geometry's own box. + // A stroke paints outside the geometry, so coverage — what the damage + // policy repaints — includes its checked reach. Local-space reach is + // inflated before projection; frame-space reach is inflated around + // the already-projected centerline bounds. Both routes intersect with + // the frame clip before encoding the finite RectF used by damage. + // Paint reference boxes above deliberately remain the geometry's box. let coverage = match (&node.stroke, node.paints.is_empty()) { (None, true) => None, (None, false) => bounded_geometry_coverage(node.bounds, resolved.bounds), + (Some(stroke), _) + if stroke.space() == rframe::StrokeSpace::Frame + && !transform_has_identity_linear_part(&node.transform) => + { + if frame_stroke_transform_is_unusable(&node.transform) { + if node.paints.is_empty() { + None + } else { + bounded_geometry_coverage(node.bounds, resolved.bounds) + } + } else { + bounded_frame_stroke_coverage(node.bounds, stroke.outset(), resolved.bounds) + } + } (Some(stroke), _) => { let box_world = matches!(&node.geometry, Geometry::Rect(_) | Geometry::Ellipse(_)) .then_some(&box_world); @@ -699,6 +713,14 @@ pub fn compile(resolved: Frame) -> Result { // other in the same private drawlist, which is why a stroke needs no // group scope. if let Some(stroke) = &node.stroke { + // Blink cannot project a frame-space stroke outline back through a + // singular local-to-frame map. The stroke is therefore the exact + // nothing; an independent fill, if any, already emitted above. + if stroke.space() == rframe::StrokeSpace::Frame + && frame_stroke_transform_is_unusable(&node.transform) + { + continue; + } let stroke_pattern = stroke .paints() .pattern() @@ -717,7 +739,8 @@ pub fn compile(resolved: Frame) -> Result { && rect.width > 0.0 && rect.height > 0.0 && stroke.dash().is_some(); - let (stroke, dash_phase, post_paint_opacity) = compile_stroke(stroke, unit_offset); + let (stroke, space, dash_phase, post_paint_opacity) = + compile_stroke(stroke, unit_offset); if let Some(pattern) = stroke_pattern { items.push(Item { node: owner, @@ -726,6 +749,7 @@ pub fn compile(resolved: Frame) -> Result { geometry: compile_pattern_geometry(&node.geometry), pattern, stroke, + space, dash_phase, post_paint_opacity, }, @@ -739,6 +763,7 @@ pub fn compile(resolved: Frame) -> Result { corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing::default(), stroke, + space, dash_phase, post_paint_opacity, }, @@ -748,6 +773,7 @@ pub fn compile(resolved: Frame) -> Result { w, h, stroke, + space, dash_phase, post_paint_opacity, }, @@ -755,6 +781,7 @@ pub fn compile(resolved: Frame) -> Result { w, h, stroke, + space, dash_phase, post_paint_opacity, }, @@ -763,6 +790,7 @@ pub fn compile(resolved: Frame) -> Result { h, path: Arc::clone(path.as_ref().expect("path geometry compiled its stream")), stroke, + space, dash_phase, post_paint_opacity, }, @@ -1145,13 +1173,14 @@ fn compile_filter(filter: &rframe::Filter) -> ResolvedFilter { /// alignment, so the projection names it rather than relying on a default. /// Width is uniform because a Web stroke has one width. A checked dash cycle /// crosses unchanged: its intervals are already even, finite, non-negative, -/// positive-sum local-space distances, so the private painter has nothing to -/// parse or resolve again. Its paired canonical phase crosses as the same -/// scalar; normalization remains the contract producer's responsibility. +/// positive-sum distances in the stroke's declared construction space, so the +/// private painter has nothing to parse or resolve again. Its paired canonical +/// phase crosses as the same scalar; normalization remains the contract +/// producer's responsibility. fn compile_stroke( stroke: &rframe::Stroke, unit_offset: Option<(f32, f32)>, -) -> (Stroke, StrokeDashPhase, PostPaintOpacity) { +) -> (Stroke, StrokeSpace, StrokeDashPhase, PostPaintOpacity) { let dash = stroke.dash(); let material = Stroke { paints: compile_paints(stroke.paints(), unit_offset), @@ -1173,8 +1202,28 @@ fn compile_stroke( let phase = dash.map_or(StrokeDashPhase::ZERO, |dash| { StrokeDashPhase::from_canonical(dash.phase()) }); + let space = match stroke.space() { + rframe::StrokeSpace::Local => StrokeSpace::Local, + rframe::StrokeSpace::Frame => StrokeSpace::Frame, + }; let post_paint_opacity = PostPaintOpacity::from_resolved(stroke.paints().alpha_factor().get()); - (material, phase, post_paint_opacity) + (material, space, phase, post_paint_opacity) +} + +fn transform_has_identity_linear_part(transform: &math2::transform::AffineTransform) -> bool { + let [[a, c, _], [b, d, _]] = transform.matrix; + a == 1.0 && b == 0.0 && c == 0.0 && d == 1.0 +} + +/// Blink and Skia suppress non-scaling-stroke when the f32 affine cannot +/// supply a finite nonzero determinant to the backend path projection. This +/// intentionally includes mathematical transforms whose determinant +/// underflows or overflows f32; Chromium paints no stroke for both measured +/// classes, while widening the centerline would paint a silent extra dot. +fn frame_stroke_transform_is_unusable(transform: &math2::transform::AffineTransform) -> bool { + let [[a, c, _], [b, d, _]] = transform.matrix; + let determinant = a * d - b * c; + determinant == 0.0 || !determinant.is_finite() } fn validate_rect(rect: math2::Rectangle) -> Result<(), ()> { @@ -1574,6 +1623,17 @@ fn bounded_stroke_coverage( Some(rectf_covering_bounded(clipped, frame_bounds)) } +fn bounded_frame_stroke_coverage( + geometry_bounds: math2::Rectangle, + outset: f64, + frame_bounds: math2::Rectangle, +) -> Option { + let clipped = WideRect::from_rectangle(geometry_bounds) + .inflated(outset) + .intersection(WideRect::from_rectangle(frame_bounds))?; + Some(rectf_covering_bounded(clipped, frame_bounds)) +} + fn bounded_union_rectf( a: n0_model::math::RectF, b: n0_model::math::RectF, @@ -1961,6 +2021,24 @@ mod tests { .expect("test product has one stroke item") } + fn private_stroke_space(product: &FrameProduct) -> StrokeSpace { + product + .drawlist + .items + .iter() + .find_map(|item| match &item.kind { + ItemKind::PatternStroke { space, .. } + | ItemKind::RectStroke { space, .. } + | ItemKind::OvalStroke { space, .. } + | ItemKind::AbsoluteDashedOvalStroke { space, .. } + | ItemKind::LineStroke { space, .. } + | ItemKind::PathStroke { space, .. } + | ItemKind::TextStroke { space, .. } => Some(*space), + _ => None, + }) + .expect("test product has one stroke item") + } + fn assert_finite_bounded_coverage(coverage: n0_model::math::RectF, frame: Rectangle) { assert!( [coverage.x, coverage.y, coverage.w, coverage.h] @@ -2401,6 +2479,188 @@ mod tests { ); } + /// The frame contract states the construction space directly. The private + /// drawlist preserves it, coverage expands after centerline projection, + /// and rasterization is identical to an explicitly transformed centerline + /// stroked at the same nominal width. + #[test] + fn frame_space_stroke_projects_without_recovering_meaning_from_transform() { + let geometry = Geometry::Rect(Rectangle::from_xywh(8.0, 8.0, 8.0, 8.0)); + let transform = AffineTransform::from_acebdf(2.0, 0.0, 0.0, 0.0, 0.5, 0.0); + let stroke = checked_stroke( + 4.0, + rframe::StrokeCap::Butt, + rframe::StrokeJoin::Bevel, + 1.0, + None, + ) + .with_space(rframe::StrokeSpace::Frame); + let projected = compile(frame_of(FrameItems::from_nodes(vec![stroked_node( + RECT_OWNER, + geometry, + transform, + stroke.clone(), + )]))) + .expect("frame-space stroke compiles"); + + assert_eq!(private_stroke_space(&projected), StrokeSpace::Frame); + let coverage = coverage_for(&projected, RECT_OWNER).expect("stroke has coverage"); + let wide = WideRect::from_rectf(coverage); + assert!(wide.left <= 14.0 && wide.left > 13.9); + assert!(wide.top <= 2.0 && wide.top > 1.9); + assert!(wide.right >= 34.0 && wide.right < 34.1); + assert!(wide.bottom >= 10.0 && wide.bottom < 10.1); + assert_finite_bounded_coverage(coverage, projected.resolved.bounds); + + let manual = compile(frame_of(FrameItems::from_nodes(vec![stroked_node( + RECT_OWNER, + Geometry::Rect(Rectangle::from_xywh(16.0, 4.0, 16.0, 4.0)), + AffineTransform::identity(), + stroke.with_space(rframe::StrokeSpace::Local), + )]))) + .expect("manual transformed-centerline control compiles"); + let ctx = PaintCtx::new(None); + assert_eq!( + projected + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &ctx) + .expect("frame-space raster"), + manual + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &ctx) + .expect("manual raster") + ); + } + + #[test] + fn translation_only_frame_space_stroke_keeps_the_local_f32_draw_order() { + let path = rframe::PathData::new( + vec![ + rframe::PathCommand::MoveTo { + x: 100_000_008.0, + y: 100_000_016.0, + }, + rframe::PathCommand::LineTo { + x: 100_000_048.0, + y: 100_000_016.0, + }, + rframe::PathCommand::LineTo { + x: 100_000_048.0, + y: 100_000_040.0, + }, + ], + rframe::FillRule::NonZero, + ) + .expect("large translated path is valid"); + let transform = + AffineTransform::from_acebdf(1.0, 0.0, -100_000_000.0, 0.0, 1.0, -100_000_000.0); + let local = checked_stroke( + 4.0, + rframe::StrokeCap::Round, + rframe::StrokeJoin::Round, + 1.0, + None, + ); + let scene = |space| { + compile(frame_of(FrameItems::from_nodes(vec![stroked_node( + RECT_OWNER, + Geometry::Path(Arc::new(path.clone())), + transform, + local.clone().with_space(space), + )]))) + .expect("translated stroke compiles") + }; + let ordinary = scene(rframe::StrokeSpace::Local); + let non_scaling = scene(rframe::StrokeSpace::Frame); + assert_eq!(private_stroke_space(&non_scaling), StrokeSpace::Frame); + let ctx = PaintCtx::new(None); + assert_eq!( + non_scaling + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &ctx) + .expect("frame-space translation raster"), + ordinary + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &ctx) + .expect("ordinary translation raster"), + "translation is an equivalent local execution, including f32 cancellation order" + ); + } + + #[test] + fn backend_unusable_frame_space_affines_suppress_the_stroke() { + let path = rframe::PathData::new( + vec![ + rframe::PathCommand::MoveTo { x: 0.0, y: 0.0 }, + rframe::PathCommand::LineTo { x: 0.0, y: 0.0 }, + ], + rframe::FillRule::NonZero, + ) + .expect("zero-length path is valid"); + for scale in [1.0e-30, 1.0e30] { + let transform = AffineTransform::from_acebdf(scale, 0.0, 32.0, 0.0, scale, 24.0); + let stroke = checked_stroke( + 8.0, + rframe::StrokeCap::Round, + rframe::StrokeJoin::Round, + 1.0, + None, + ) + .with_space(rframe::StrokeSpace::Frame); + let product = compile(frame_of(FrameItems::from_nodes(vec![stroked_node( + RECT_OWNER, + Geometry::Path(Arc::new(path.clone())), + transform, + stroke, + )]))) + .expect("backend-unusable frame affine resolves to no stroke"); + assert_eq!(coverage_for(&product, RECT_OWNER), None, "scale={scale}"); + assert!( + product.drawlist.items.iter().all(|item| !matches!( + item.kind, + ItemKind::PathStroke { .. } | ItemKind::PatternStroke { .. } + )), + "scale={scale} must emit no stroke item" + ); + } + } + + #[test] + fn singular_frame_space_stroke_is_exact_nothing_but_fill_survives() { + let geometry = Geometry::Rect(Rectangle::from_xywh(8.0, 8.0, 8.0, 8.0)); + let transform = AffineTransform::from_acebdf(0.0, 0.0, 24.0, 0.0, 1.0, 0.0); + let stroke = checked_stroke( + 4.0, + rframe::StrokeCap::Round, + rframe::StrokeJoin::Round, + 1.0, + None, + ) + .with_space(rframe::StrokeSpace::Frame); + let mut node = stroked_node(RECT_OWNER, geometry, transform, stroke); + node.paints = PaintStack::solid(CGColor::BLACK); + let product = compile(frame_of(FrameItems::from_nodes(vec![node]))) + .expect("singular frame-space stroke does not poison its fill"); + + assert!( + product.drawlist.items.iter().all(|item| !matches!( + item.kind, + ItemKind::PatternStroke { .. } + | ItemKind::RectStroke { .. } + | ItemKind::OvalStroke { .. } + | ItemKind::AbsoluteDashedOvalStroke { .. } + | ItemKind::LineStroke { .. } + | ItemKind::PathStroke { .. } + | ItemKind::TextStroke { .. } + )), + "the singular stroke emits no private paint item" + ); + assert!( + product + .drawlist + .items + .iter() + .any(|item| matches!(item.kind, ItemKind::RectFill { .. })), + "the independent fill remains" + ); + } + /// Changing only the resolved dash cycle changes the private draw item and /// therefore participates in the complete-frame damage policy. #[test] diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 466bc4fc..0fffe582 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -39,7 +39,7 @@ use crate::drawlist::{ ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterLightSource, ResolvedFilterMorphology, ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, - ResolvedMaskMode, ResolvedPattern, ResolvedPatternGeometry, StrokeDashPhase, + ResolvedMaskMode, ResolvedPattern, ResolvedPatternGeometry, StrokeDashPhase, StrokeSpace, }; /// The gradient family whose local matrix could not be represented by the @@ -639,6 +639,41 @@ fn paint_box_matrix(paint_box: PaintBox, transform: &Affine) -> Matrix { matrix } +/// Map an existing local paint coordinate system into the coordinates in +/// which geometry is submitted to the canvas. Identity deliberately performs +/// no matrix write so every ordinary draw retains its established f32 route. +fn mapped_paint_box_matrix( + paint_box: PaintBox, + transform: &Affine, + paint_to_canvas: &Affine, +) -> Matrix { + let mut matrix = paint_box_matrix(paint_box, transform); + if *paint_to_canvas != Affine::IDENTITY { + // SkMatrix::postConcat is `other * self`: unit -> paint box -> model + // paint transform -> frame, matching the transformed centerline. + matrix.post_concat(&skia_matrix(paint_to_canvas)); + } + matrix +} + +/// Translation does not change stroke construction. Keeping that exact case +/// on the established local draw route avoids an otherwise observable f32 +/// cancellation when large source coordinates meet an opposite translation. +/// The resolved contract remains frame-space; this is only an equivalent +/// backend execution route. +fn effective_stroke_space(space: StrokeSpace, world: &Affine) -> StrokeSpace { + if space == StrokeSpace::Frame + && world.a == 1.0 + && world.b == 0.0 + && world.c == 0.0 + && world.d == 1.0 + { + StrokeSpace::Local + } else { + space + } +} + fn gradient_transform(model: &ModelPaint) -> Option<(GradientKind, &Affine)> { match model { ModelPaint::LinearGradient(gradient) => Some((GradientKind::Linear, &gradient.transform)), @@ -655,6 +690,7 @@ fn preflight_paints( context: PaintUseContext, paints: &Paints, paint_box: PaintBox, + paint_to_canvas: &Affine, ) -> Result<(), GradientPreflightError> { for (visible_paint_index, model) in paints.iter().enumerate() { let Some((gradient, transform)) = gradient_transform(model) else { @@ -670,7 +706,10 @@ fn preflight_paints( reason: GradientPreflightReason::InvalidPaint(error), }); } - if paint_box_matrix(paint_box, transform).invert().is_none() { + if mapped_paint_box_matrix(paint_box, transform, paint_to_canvas) + .invert() + .is_none() + { return Err(GradientPreflightError { node, gradient, @@ -681,11 +720,17 @@ fn preflight_paints( }); } let shader_exists = match model { - ModelPaint::LinearGradient(model) => linear_gradient_shader(model, paint_box).is_some(), - ModelPaint::RadialGradient(model) => radial_gradient_shader(model, paint_box).is_some(), - ModelPaint::SweepGradient(model) => sweep_gradient_shader(model, paint_box).is_some(), + ModelPaint::LinearGradient(model) => { + linear_gradient_shader_mapped(model, paint_box, paint_to_canvas).is_some() + } + ModelPaint::RadialGradient(model) => { + radial_gradient_shader_mapped(model, paint_box, paint_to_canvas).is_some() + } + ModelPaint::SweepGradient(model) => { + sweep_gradient_shader_mapped(model, paint_box, paint_to_canvas).is_some() + } ModelPaint::DiamondGradient(model) => { - diamond_gradient_shader(model, paint_box).is_some() + diamond_gradient_shader_mapped(model, paint_box, paint_to_canvas).is_some() } ModelPaint::Solid(_) | ModelPaint::Image(_) => unreachable!(), }; @@ -721,6 +766,7 @@ pub(crate) fn preflight_gradients( PaintUseContext::Fill, paints, PaintBox::from_size(*w, *h), + &Affine::IDENTITY, )?, ItemKind::TextFill { layout, @@ -742,39 +788,86 @@ pub(crate) fn preflight_gradients( }, run_paints, paint_box, + &Affine::IDENTITY, )?; } } - ItemKind::RectStroke { w, h, stroke, .. } - | ItemKind::OvalStroke { w, h, stroke, .. } - | ItemKind::PathStroke { w, h, stroke, .. } => preflight_paints( - item.node, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_size(*w, *h), - )?, + ItemKind::RectStroke { + w, + h, + stroke, + space, + .. + } + | ItemKind::OvalStroke { + w, + h, + stroke, + space, + .. + } + | ItemKind::PathStroke { + w, + h, + stroke, + space, + .. + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_paints( + item.node, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_size(*w, *h), + paint_to_canvas, + )?; + } ItemKind::AbsoluteDashedOvalStroke { - x, y, w, h, stroke, .. - } => preflight_paints( - item.node, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_xywh(*x, *y, *w, *h), - )?, + x, + y, + w, + h, + stroke, + space, + .. + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_paints( + item.node, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_xywh(*x, *y, *w, *h), + paint_to_canvas, + )?; + } ItemKind::LineStroke { paint_w, paint_h, stroke, + space, .. - } => preflight_paints( - item.node, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_size(*paint_w, *paint_h), - )?, + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_paints( + item.node, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_size(*paint_w, *paint_h), + paint_to_canvas, + )?; + } ItemKind::TextStroke { layout, paint_w, @@ -789,6 +882,7 @@ pub(crate) fn preflight_gradients( PaintUseContext::Stroke, &stroke.paints, PaintBox::from_size(*paint_w, *paint_h), + &Affine::IDENTITY, )?; } } @@ -830,6 +924,7 @@ fn preflight_image_paints( context: PaintUseContext, paints: &Paints, paint_box: PaintBox, + paint_to_canvas: &Affine, view: &Affine, ctx: &PaintCtx, ) -> Result<(), ImagePreflightError> { @@ -874,7 +969,7 @@ fn preflight_image_paints( { return Err(fail(ImagePreflightReason::TotalMatrixNotInvertible)); } - if image_shader(model, paint_box, ctx).is_none() { + if image_shader_mapped(model, paint_box, ctx, paint_to_canvas).is_none() { return Err(fail(ImagePreflightReason::ShaderConstructionFailed)); } } @@ -899,6 +994,7 @@ pub(crate) fn preflight_images( PaintUseContext::Fill, paints, PaintBox::from_size(*w, *h), + &Affine::IDENTITY, view, ctx, )?, @@ -920,48 +1016,95 @@ pub(crate) fn preflight_images( }, run_paints, paint_box, + &Affine::IDENTITY, view, ctx, )?; } } } - ItemKind::RectStroke { w, h, stroke, .. } - | ItemKind::OvalStroke { w, h, stroke, .. } - | ItemKind::PathStroke { w, h, stroke, .. } => preflight_image_paints( - item, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_size(*w, *h), - view, - ctx, - )?, + ItemKind::RectStroke { + w, + h, + stroke, + space, + .. + } + | ItemKind::OvalStroke { + w, + h, + stroke, + space, + .. + } + | ItemKind::PathStroke { + w, + h, + stroke, + space, + .. + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_image_paints( + item, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_size(*w, *h), + paint_to_canvas, + view, + ctx, + )?; + } ItemKind::AbsoluteDashedOvalStroke { - x, y, w, h, stroke, .. - } => preflight_image_paints( - item, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_xywh(*x, *y, *w, *h), - view, - ctx, - )?, + x, + y, + w, + h, + stroke, + space, + .. + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_image_paints( + item, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_xywh(*x, *y, *w, *h), + paint_to_canvas, + view, + ctx, + )?; + } ItemKind::LineStroke { paint_w, paint_h, stroke, + space, .. - } => preflight_image_paints( - item, - draw_item, - PaintUseContext::Stroke, - &stroke.paints, - PaintBox::from_size(*paint_w, *paint_h), - view, - ctx, - )?, + } => { + let paint_to_canvas = match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }; + preflight_image_paints( + item, + draw_item, + PaintUseContext::Stroke, + &stroke.paints, + PaintBox::from_size(*paint_w, *paint_h), + paint_to_canvas, + view, + ctx, + )?; + } ItemKind::TextStroke { layout, paint_w, @@ -976,6 +1119,7 @@ pub(crate) fn preflight_images( PaintUseContext::Stroke, &stroke.paints, PaintBox::from_size(*paint_w, *paint_h), + &Affine::IDENTITY, view, ctx, )?; @@ -1010,30 +1154,46 @@ fn alignment_uv(alignment: Alignment) -> (f32, f32) { ) } -fn linear_gradient_shader(paint: &LinearGradientPaint, paint_box: PaintBox) -> Option { +fn linear_gradient_shader_mapped( + paint: &LinearGradientPaint, + paint_box: PaintBox, + paint_to_canvas: &Affine, +) -> Option { let (colors, positions) = gradient_stops(&paint.stops); let from = alignment_uv(paint.xy1); let to = alignment_uv(paint.xy2); let stops = gradient(&colors, &positions, sk_tile_mode(paint.tile_mode)); - let matrix = paint_box_matrix(paint_box, &paint.transform); + let matrix = mapped_paint_box_matrix(paint_box, &paint.transform, paint_to_canvas); shaders::linear_gradient((from, to), &stops, Some(&matrix)) } -fn radial_gradient_shader(paint: &RadialGradientPaint, paint_box: PaintBox) -> Option { +fn radial_gradient_shader_mapped( + paint: &RadialGradientPaint, + paint_box: PaintBox, + paint_to_canvas: &Affine, +) -> Option { let (colors, positions) = gradient_stops(&paint.stops); let stops = gradient(&colors, &positions, sk_tile_mode(paint.tile_mode)); - let matrix = paint_box_matrix(paint_box, &paint.transform); + let matrix = mapped_paint_box_matrix(paint_box, &paint.transform, paint_to_canvas); shaders::radial_gradient(((0.5, 0.5), 0.5), &stops, Some(&matrix)) } -fn sweep_gradient_shader(paint: &SweepGradientPaint, paint_box: PaintBox) -> Option { +fn sweep_gradient_shader_mapped( + paint: &SweepGradientPaint, + paint_box: PaintBox, + paint_to_canvas: &Affine, +) -> Option { let (colors, positions) = gradient_stops(&paint.stops); let stops = gradient(&colors, &positions, skia_safe::TileMode::Clamp); - let matrix = paint_box_matrix(paint_box, &paint.transform); + let matrix = mapped_paint_box_matrix(paint_box, &paint.transform, paint_to_canvas); shaders::sweep_gradient((0.5, 0.5), (0.0, 360.0), &stops, Some(&matrix)) } -fn diamond_gradient_shader(paint: &DiamondGradientPaint, paint_box: PaintBox) -> Option { +fn diamond_gradient_shader_mapped( + paint: &DiamondGradientPaint, + paint_box: PaintBox, + paint_to_canvas: &Affine, +) -> Option { let (colors, positions) = gradient_stops(&paint.stops); let stops = gradient(&colors, &positions, skia_safe::TileMode::Clamp); let ramp = shaders::linear_gradient(((0.0, 0.0), (1.0, 0.0)), &stops, None)?; @@ -1047,7 +1207,7 @@ fn diamond_gradient_shader(paint: &DiamondGradientPaint, paint_box: PaintBox) -> } "#; let effect = skia_safe::RuntimeEffect::make_for_shader(SKSL, None).ok()?; - let matrix = paint_box_matrix(paint_box, &paint.transform); + let matrix = mapped_paint_box_matrix(paint_box, &paint.transform, paint_to_canvas); effect.make_shader(Data::new_copy(&[]), &[ramp.into()], Some(&matrix)) } @@ -1073,7 +1233,12 @@ fn image_fit_matrix(image: &Image, paint_box: PaintBox, fit: BoxFit) -> Matrix { Matrix::new_all(sx, 0.0, tx, 0.0, sy, ty, 0.0, 0.0, 1.0) } -fn image_shader(paint: &ImagePaint, paint_box: PaintBox, ctx: &PaintCtx) -> Option { +fn image_shader_mapped( + paint: &ImagePaint, + paint_box: PaintBox, + ctx: &PaintCtx, + paint_to_canvas: &Affine, +) -> Option { if paint.quarter_turns != 0 || paint.alignment != n0_model::model::Alignment::CENTER || paint.filters != ImageFilters::default() @@ -1087,7 +1252,10 @@ fn image_shader(paint: &ImagePaint, paint_box: PaintBox, ctx: &PaintCtx) -> Opti ResourceRef::Rid(rid) | ResourceRef::Hash(rid) => rid, }; let image = ctx.image(rid)?; - let matrix = image_fit_matrix(image, paint_box, fit); + let mut matrix = image_fit_matrix(image, paint_box, fit); + if *paint_to_canvas != Affine::IDENTITY { + matrix.post_concat(&skia_matrix(paint_to_canvas)); + } let sampling = SamplingOptions::from(CubicResampler::mitchell()); let shader = image.to_shader( Some((skia_safe::TileMode::Decal, skia_safe::TileMode::Decal)), @@ -1101,16 +1269,24 @@ fn image_shader(paint: &ImagePaint, paint_box: PaintBox, ctx: &PaintCtx) -> Opti /// repeating shader. The nested list starts with its own hard frame clip, so /// content outside `(0, 0, width, height)` cannot leak into a neighbouring /// tile before repetition. -fn pattern_shader(pattern: &ResolvedPattern, ctx: &PaintCtx) -> Option { +fn pattern_shader_mapped( + pattern: &ResolvedPattern, + ctx: &PaintCtx, + paint_to_canvas: &Affine, +) -> Option { let tile = Rect::from_wh(pattern.width, pattern.height); let mut recorder = PictureRecorder::new(); let canvas = recorder.begin_recording(tile, false); execute_unchecked(canvas, &pattern.program, &Affine::IDENTITY, ctx); let picture = recorder.finish_recording_as_picture(Some(&tile))?; + let mut matrix = skia_matrix(&pattern.transform); + if *paint_to_canvas != Affine::IDENTITY { + matrix.post_concat(&skia_matrix(paint_to_canvas)); + } Some(picture.to_shader( Some((skia_safe::TileMode::Repeat, skia_safe::TileMode::Repeat)), FilterMode::Linear, - Some(&skia_matrix(&pattern.transform)), + Some(&matrix), Some(&tile), )) } @@ -1119,10 +1295,19 @@ fn pattern_paint( pattern: &ResolvedPattern, post_paint_opacity: PostPaintOpacity, ctx: &PaintCtx, +) -> Option { + pattern_paint_mapped(pattern, post_paint_opacity, ctx, &Affine::IDENTITY) +} + +fn pattern_paint_mapped( + pattern: &ResolvedPattern, + post_paint_opacity: PostPaintOpacity, + ctx: &PaintCtx, + paint_to_canvas: &Affine, ) -> Option { let mut paint = Paint::default(); paint.set_anti_alias(true); - paint.set_shader(pattern_shader(pattern, ctx)?); + paint.set_shader(pattern_shader_mapped(pattern, ctx, paint_to_canvas)?); // Pattern paint opacity follows the same byte-alpha materialization as a // gradient shader. A one-draw element-opacity fold then multiplies that // materialized alpha without another quantization. @@ -1141,9 +1326,27 @@ fn pattern_stroke_paint( dash_phase: StrokeDashPhase, post_paint_opacity: PostPaintOpacity, ctx: &PaintCtx, +) -> Option { + pattern_stroke_paint_mapped( + pattern, + stroke, + dash_phase, + post_paint_opacity, + ctx, + &Affine::IDENTITY, + ) +} + +fn pattern_stroke_paint_mapped( + pattern: &ResolvedPattern, + stroke: &Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + ctx: &PaintCtx, + paint_to_canvas: &Affine, ) -> Option { let width = uniform_stroke_width(stroke)?; - let mut paint = pattern_paint(pattern, post_paint_opacity, ctx)?; + let mut paint = pattern_paint_mapped(pattern, post_paint_opacity, ctx, paint_to_canvas)?; paint.set_style(PaintStyle::Stroke); paint.set_stroke_width(width); paint.set_stroke_cap(sk_stroke_cap(stroke.cap)); @@ -1158,11 +1361,11 @@ fn pattern_stroke_paint( Some(paint) } -fn preflight_pattern(pattern: &ResolvedPattern, ctx: &PaintCtx) -> bool { +fn preflight_pattern(pattern: &ResolvedPattern, ctx: &PaintCtx, paint_to_canvas: &Affine) -> bool { if preflight_patterns(&pattern.program, ctx).is_err() { return false; } - pattern_shader(pattern, ctx).is_some() + pattern_shader_mapped(pattern, ctx, paint_to_canvas).is_some() } /// Prove every nested picture/repeat shader before the first target draw. @@ -1173,13 +1376,18 @@ pub(crate) fn preflight_patterns( ctx: &PaintCtx, ) -> Result<(), PatternPreflightError> { for (draw_item, item) in list.items.iter().enumerate() { - let pattern = match &item.kind { - ItemKind::PatternFill { pattern, .. } | ItemKind::PatternStroke { pattern, .. } => { - pattern - } + let (pattern, paint_to_canvas) = match &item.kind { + ItemKind::PatternFill { pattern, .. } => (pattern, &Affine::IDENTITY), + ItemKind::PatternStroke { pattern, space, .. } => ( + pattern, + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => &Affine::IDENTITY, + StrokeSpace::Frame => &item.world, + }, + ), _ => continue, }; - if !preflight_pattern(pattern, ctx) { + if !preflight_pattern(pattern, ctx, paint_to_canvas) { return Err(PatternPreflightError { draw_item }); } } @@ -1194,6 +1402,16 @@ fn sk_paint( paint_box: PaintBox, ctx: &PaintCtx, post_paint_opacity: PostPaintOpacity, +) -> Option { + sk_paint_mapped(model, paint_box, ctx, post_paint_opacity, &Affine::IDENTITY) +} + +fn sk_paint_mapped( + model: &ModelPaint, + paint_box: PaintBox, + ctx: &PaintCtx, + post_paint_opacity: PostPaintOpacity, + paint_to_canvas: &Affine, ) -> Option { let mut paint = Paint::default(); paint.set_anti_alias(true); @@ -1217,30 +1435,30 @@ fn sk_paint( } ModelPaint::LinearGradient(model) => { paint.set_shader( - linear_gradient_shader(model, paint_box) + linear_gradient_shader_mapped(model, paint_box, paint_to_canvas) .expect("preflighted linear gradient shader construction failed"), ); } ModelPaint::RadialGradient(model) => { paint.set_shader( - radial_gradient_shader(model, paint_box) + radial_gradient_shader_mapped(model, paint_box, paint_to_canvas) .expect("preflighted radial gradient shader construction failed"), ); } ModelPaint::SweepGradient(model) => { paint.set_shader( - sweep_gradient_shader(model, paint_box) + sweep_gradient_shader_mapped(model, paint_box, paint_to_canvas) .expect("preflighted sweep gradient shader construction failed"), ); } ModelPaint::DiamondGradient(model) => { paint.set_shader( - diamond_gradient_shader(model, paint_box) + diamond_gradient_shader_mapped(model, paint_box, paint_to_canvas) .expect("preflighted diamond gradient shader construction failed"), ); } ModelPaint::Image(model) => { - paint.set_shader(image_shader(model, paint_box, ctx)?); + paint.set_shader(image_shader_mapped(model, paint_box, ctx, paint_to_canvas)?); } } // Solids store opacity in their RGBA8 color. An image's opacity stays the @@ -3726,9 +3944,29 @@ fn native_stroke_paint( post_paint_opacity: PostPaintOpacity, paint_box: PaintBox, ctx: &PaintCtx, +) -> Option { + native_stroke_paint_mapped( + model, + stroke, + dash_phase, + post_paint_opacity, + paint_box, + ctx, + &Affine::IDENTITY, + ) +} + +fn native_stroke_paint_mapped( + model: &ModelPaint, + stroke: &Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + paint_box: PaintBox, + ctx: &PaintCtx, + paint_to_canvas: &Affine, ) -> Option { let width = uniform_stroke_width(stroke)?; - let mut paint = sk_paint(model, paint_box, ctx, post_paint_opacity)?; + let mut paint = sk_paint_mapped(model, paint_box, ctx, post_paint_opacity, paint_to_canvas)?; paint.set_style(PaintStyle::Stroke); paint.set_stroke_width(width); paint.set_stroke_cap(sk_stroke_cap(stroke.cap)); @@ -3765,6 +4003,62 @@ fn draw_native_centered_stroke( } } +fn draw_native_centered_stroke_mapped( + stroke: &Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + paint_box: PaintBox, + ctx: &PaintCtx, + paint_to_canvas: &Affine, + mut draw: impl FnMut(&Paint), +) { + for model in stroke.paints.iter() { + if let Some(paint) = native_stroke_paint_mapped( + model, + stroke, + dash_phase, + post_paint_opacity, + paint_box, + ctx, + paint_to_canvas, + ) { + draw(&paint); + } + } +} + +/// Draw one centerline already expressed in item-local coordinates. A +/// frame-space stroke maps that centerline before dashing/widening while its +/// paint mapping follows the same local-to-frame transform. The host view is +/// deliberately left outside this operation. +#[allow(clippy::too_many_arguments)] +fn draw_frame_space_stroke( + canvas: &Canvas, + view: &Affine, + world: &Affine, + source: &Path, + stroke: &Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + paint_box: PaintBox, + ctx: &PaintCtx, +) { + let transformed = source.make_transform(&skia_matrix(world)); + with_local_transform(canvas, view, &Affine::IDENTITY, || { + draw_native_centered_stroke_mapped( + stroke, + dash_phase, + post_paint_opacity, + paint_box, + ctx, + world, + |paint| { + canvas.draw_path(&transformed, paint); + }, + ); + }); +} + #[derive(Default)] struct GlyphScratch { ids: Vec, @@ -4029,42 +4323,83 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, geometry, pattern, stroke, + space, dash_phase, post_paint_opacity, } => { - with_local_transform(canvas, view, &item.world, || { - debug_assert_eq!(stroke.align, StrokeAlign::Center); - let adjusted = match geometry { - ResolvedPatternGeometry::Oval { w, h, .. } if *w > 0.0 && *h > 0.0 => { - stroke_cap_for_closed_contours(stroke) - } - ResolvedPatternGeometry::Path(path) - if path.all_contours_closed && !any_contour_may_be_degenerate(path) => - { - stroke_cap_for_closed_contours(stroke) - } - _ => stroke.clone(), - }; - let paint = pattern_stroke_paint( - pattern, - &adjusted, - *dash_phase, - *post_paint_opacity, - ctx, - ) - .expect("preflighted pattern stroke shader construction failed"); - match geometry { - ResolvedPatternGeometry::Rect { x, y, w, h } => { - canvas.draw_rect(Rect::from_xywh(*x, *y, *w, *h), &paint); - } - ResolvedPatternGeometry::Oval { x, y, w, h } => { - canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), &paint); - } - ResolvedPatternGeometry::Path(path) => { - canvas.draw_path(&backend_path(path), &paint); - } + debug_assert_eq!(stroke.align, StrokeAlign::Center); + let adjusted = match geometry { + ResolvedPatternGeometry::Oval { w, h, .. } if *w > 0.0 && *h > 0.0 => { + stroke_cap_for_closed_contours(stroke) } - }); + ResolvedPatternGeometry::Path(path) + if path.all_contours_closed && !any_contour_may_be_degenerate(path) => + { + stroke_cap_for_closed_contours(stroke) + } + _ => stroke.clone(), + }; + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + let paint = pattern_stroke_paint( + pattern, + &adjusted, + *dash_phase, + *post_paint_opacity, + ctx, + ) + .expect("preflighted pattern stroke shader construction failed"); + match geometry { + ResolvedPatternGeometry::Rect { x, y, w, h } => { + canvas.draw_rect(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Oval { x, y, w, h } => { + canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Path(path) => { + canvas.draw_path(&backend_path(path), &paint); + } + } + }); + } + StrokeSpace::Frame => { + let source = match geometry { + ResolvedPatternGeometry::Rect { x, y, w, h } => { + let mut builder = PathBuilder::new(); + builder.add_rect( + Rect::from_xywh(*x, *y, *w, *h), + Some(PathDirection::CW), + Some(0), + ); + builder.snapshot() + } + ResolvedPatternGeometry::Oval { x, y, w, h } => { + let mut builder = PathBuilder::new(); + builder.add_oval( + Rect::from_xywh(*x, *y, *w, *h), + Some(PathDirection::CW), + Some(1), + ); + builder.snapshot() + } + ResolvedPatternGeometry::Path(path) => backend_path(path), + }; + let transformed = source.make_transform(&skia_matrix(&item.world)); + with_local_transform(canvas, view, &Affine::IDENTITY, || { + let paint = pattern_stroke_paint_mapped( + pattern, + &adjusted, + *dash_phase, + *post_paint_opacity, + ctx, + &item.world, + ) + .expect("preflighted pattern stroke shader construction failed"); + canvas.draw_path(&transformed, &paint); + }); + } + } } ItemKind::RectFill { w, @@ -4168,27 +4503,125 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, corner_radius, corner_smoothing, stroke, + space, dash_phase, post_paint_opacity, } => { - with_local_transform(canvas, view, &item.world, || { - let paint_box = PaintBox::from_size(*w, *h); - match stroke.width.normalized() { - StrokeWidth::None => {} - StrokeWidth::Rectangular(widths) => draw_rectangular_stroke( + let paint_box = PaintBox::from_size(*w, *h); + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + match stroke.width.normalized() { + StrokeWidth::None => {} + StrokeWidth::Rectangular(widths) => draw_rectangular_stroke( + canvas, + *w, + *h, + corner_radius, + widths, + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ), + StrokeWidth::Uniform(_) => { + if corner_radius.is_zero() + && stroke.align == StrokeAlign::Center + { + draw_native_centered_stroke( + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + |paint| { + canvas.draw_rect(Rect::from_wh(*w, *h), paint); + }, + ); + } else { + let path = rounded_rect_path( + *w, + *h, + corner_radius, + corner_smoothing.value(), + ); + if stroke.align == StrokeAlign::Center { + draw_native_centered_stroke( + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + |paint| { + canvas.draw_path(&path, paint); + }, + ); + } else { + draw_stroke( + canvas, + &path, + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ); + } + } + } + } + }); + } + StrokeSpace::Frame => { + debug_assert_eq!(stroke.align, StrokeAlign::Center); + debug_assert!(matches!(stroke.width.normalized(), StrokeWidth::Uniform(_))); + let source = if corner_radius.is_zero() { + let mut builder = PathBuilder::new(); + builder.add_rect( + Rect::from_wh(*w, *h), + Some(PathDirection::CW), + Some(0), + ); + builder.snapshot() + } else { + rounded_rect_path(*w, *h, corner_radius, corner_smoothing.value()) + }; + draw_frame_space_stroke( canvas, - *w, - *h, - corner_radius, - widths, + view, + &item.world, + &source, stroke, *dash_phase, *post_paint_opacity, paint_box, ctx, - ), - StrokeWidth::Uniform(_) => { - if corner_radius.is_zero() && stroke.align == StrokeAlign::Center { + ); + } + } + } + ItemKind::OvalStroke { + w, + h, + stroke, + space, + dash_phase, + post_paint_opacity, + } => { + let paint_box = PaintBox::from_size(*w, *h); + // A solid oval is one closed contour, so its cap is inert; + // a dashed oval keeps the authored cap because every dash has + // ends. A zero-axis oval instead degenerates to a segment. + let stroke = if *w > 0.0 && *h > 0.0 { + &stroke_cap_for_closed_contours(stroke) + } else { + stroke + }; + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + if stroke.align == StrokeAlign::Center { draw_native_centered_stroke( stroke, *dash_phase, @@ -4196,79 +4629,79 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, paint_box, ctx, |paint| { - canvas.draw_rect(Rect::from_wh(*w, *h), paint); + canvas.draw_oval(Rect::from_wh(*w, *h), paint); }, ); } else { - let path = rounded_rect_path( - *w, - *h, - corner_radius, - corner_smoothing.value(), + draw_stroke( + canvas, + &oval_path(*w, *h), + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, ); - if stroke.align == StrokeAlign::Center { - draw_native_centered_stroke( - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - |paint| { - canvas.draw_path(&path, paint); - }, - ); - } else { - draw_stroke( - canvas, - &path, - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - ); - } } - } + }); } - }); + StrokeSpace::Frame => draw_frame_space_stroke( + canvas, + view, + &item.world, + &oval_path(*w, *h), + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ), + } } - ItemKind::OvalStroke { + ItemKind::AbsoluteDashedOvalStroke { + x, + y, w, h, stroke, + space, dash_phase, post_paint_opacity, } => { - with_local_transform(canvas, view, &item.world, || { - let paint_box = PaintBox::from_size(*w, *h); - // A solid oval is one closed contour, so its cap is inert; - // a dashed oval keeps the authored cap because every dash - // has ends. See [`stroke_cap_for_closed_contours`], which - // also carries the thin-solid normalization this arm needs - // where the rect arm does not. An oval with no extent is - // the other exception: it degenerates to a segment whose - // ends the cap is the only thing that renders. - let stroke = if *w > 0.0 && *h > 0.0 { - &stroke_cap_for_closed_contours(stroke) - } else { - stroke - }; - if stroke.align == StrokeAlign::Center { - draw_native_centered_stroke( - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - |paint| { - canvas.draw_oval(Rect::from_wh(*w, *h), paint); - }, + let paint_box = PaintBox::from_xywh(*x, *y, *w, *h); + let stroke = if *w > 0.0 && *h > 0.0 { + &stroke_cap_for_closed_contours(stroke) + } else { + stroke + }; + debug_assert_eq!(stroke.align, StrokeAlign::Center); + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + draw_native_centered_stroke( + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + |paint| { + canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), paint); + }, + ); + }); + } + StrokeSpace::Frame => { + let mut builder = PathBuilder::new(); + builder.add_oval( + Rect::from_xywh(*x, *y, *w, *h), + Some(PathDirection::CW), + Some(1), ); - } else { - draw_stroke( + draw_frame_space_stroke( canvas, - &oval_path(*w, *h), + view, + &item.world, + &builder.snapshot(), stroke, *dash_phase, *post_paint_opacity, @@ -4276,36 +4709,7 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, ctx, ); } - }); - } - ItemKind::AbsoluteDashedOvalStroke { - x, - y, - w, - h, - stroke, - dash_phase, - post_paint_opacity, - } => { - with_local_transform(canvas, view, &item.world, || { - let paint_box = PaintBox::from_xywh(*x, *y, *w, *h); - let stroke = if *w > 0.0 && *h > 0.0 { - &stroke_cap_for_closed_contours(stroke) - } else { - stroke - }; - debug_assert_eq!(stroke.align, StrokeAlign::Center); - draw_native_centered_stroke( - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - |paint| { - canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), paint); - }, - ); - }); + } } ItemKind::LineStroke { x1, @@ -4315,95 +4719,122 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, paint_w, paint_h, stroke, + space, dash_phase, post_paint_opacity, } => { - with_local_transform(canvas, view, &item.world, || { - let paint_box = PaintBox::from_size(*paint_w, *paint_h); - if stroke.align == StrokeAlign::Center { - draw_native_centered_stroke( - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - |paint| { - canvas.draw_line((*x1, *y1), (*x2, *y2), paint); - }, - ); - } else { - draw_stroke( - canvas, - &line_path(*x1, *y1, *x2, *y2), - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - ); + let paint_box = PaintBox::from_size(*paint_w, *paint_h); + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + if stroke.align == StrokeAlign::Center { + draw_native_centered_stroke( + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + |paint| { + canvas.draw_line((*x1, *y1), (*x2, *y2), paint); + }, + ); + } else { + draw_stroke( + canvas, + &line_path(*x1, *y1, *x2, *y2), + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ); + } + }); } - }); + StrokeSpace::Frame => draw_frame_space_stroke( + canvas, + view, + &item.world, + &line_path(*x1, *y1, *x2, *y2), + stroke, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ), + } } ItemKind::PathStroke { w, h, path, stroke, + space, dash_phase, post_paint_opacity, } => { - with_local_transform(canvas, view, &item.world, || { - let paint_box = PaintBox::from_size(*w, *h); - let geometry = backend_path(path); - if stroke.align != StrokeAlign::Center { - draw_stroke( - canvas, - &geometry, - stroke, - *dash_phase, - *post_paint_opacity, - paint_box, - ctx, - ); - return; + let paint_box = PaintBox::from_size(*w, *h); + let geometry = backend_path(path); + // One draw, so one composite pass. A solid closed contour's + // cap is inert; a dashed path keeps its authored cap. + let adjusted = if path.all_contours_closed && !any_contour_may_be_degenerate(path) { + stroke_cap_for_closed_contours(stroke) + } else { + stroke.clone() + }; + match effective_stroke_space(*space, &item.world) { + StrokeSpace::Local => { + with_local_transform(canvas, view, &item.world, || { + if adjusted.align != StrokeAlign::Center { + draw_stroke( + canvas, + &geometry, + &adjusted, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + ); + } else { + draw_native_centered_stroke( + &adjusted, + *dash_phase, + *post_paint_opacity, + paint_box, + ctx, + |paint| { + canvas.draw_path(&geometry, paint); + }, + ); + } + }); } - // One draw, so one composite pass. The cap a solid closed - // contour is stroked with is the *only* thing that varies - // here; a dashed path keeps its authored cap. The solid - // normalization can vary per path because - // `all_contours_closed` is a property of the whole - // artifact — a path that mixes open and closed contours - // under a non-butt cap refuses upstream rather than - // arriving here to be guessed at. - let stroke = if path.all_contours_closed && !any_contour_may_be_degenerate(path) - { - &stroke_cap_for_closed_contours(stroke) - } else { - stroke - }; - draw_native_centered_stroke( - stroke, + StrokeSpace::Frame => draw_frame_space_stroke( + canvas, + view, + &item.world, + &geometry, + &adjusted, *dash_phase, *post_paint_opacity, paint_box, ctx, - |paint| { - canvas.draw_path(&geometry, paint); - }, - ); - }); + ), + } } ItemKind::TextStroke { layout, paint_w, paint_h, stroke, + space, dash_phase, post_paint_opacity, } => { if layout.glyph_runs.is_empty() { continue; } + debug_assert_eq!(*space, StrokeSpace::Local); with_local_transform(canvas, view, &item.world, || { let paint_box = PaintBox::from_size(*paint_w, *paint_h); if stroke.align == StrokeAlign::Center { diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 3fb498e0..2ebfe795 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,10 +607,10 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,079 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,127 Chromium-baked cells plus 16 sampled frames; the text estate contains fifteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two - Bungee), and the named refusal register has 226 rows. `feFlood`, `feComposite`, + Bungee), and the named refusal register has 230 rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, `feDiffuseLighting`, `feDistantLight`, `fePointLight`, `feSpotLight`, @@ -665,6 +665,29 @@ cargo run -p n0_cli --bin n0 -- \ indirection and CSS escapes — all refuse by name. The SVG2-only join values `miter-clip` and `arcs` drop as invalid declarations exactly as Chromium drops them (measured, celled) — an agreement, not a hole. + The direct `vector-effect` presentation attribute carries the complete + standard-track grammar. `none` retains ordinary local-space stroking; + `non-scaling-stroke` maps the centerline into frame space before applying + the nominal width as one circular pen. Chromium 149 drops the at-risk + `non-scaling-size`, `non-rotation`, `fixed-position`, combination, + `viewport`, and `screen` members to `none`, and those drops are baked. + The source-neutral resolved stroke records only `Local | Frame` + construction space. Frame construction reaches every admitted geometry, + dash and `pathLength` route, affine and viewport mapping, paint server, + clip/mask/filter/opacity composition, pattern/mask/marker source program, + `` target, and marker-unit branch. `markerUnits="strokeWidth"` follows + Blink's RMS affine scale; `userSpaceOnUse` remains independent. Exact-linear + identity mappings retain the established local f32 execution order, while + a zero, non-finite, underflowed, or overflowed f32 determinant suppresses + both the frame-space stroke and its stroke-width marker. Forty-eight exact + Chromium cells guard the slice, including the rule that a substitution + function is inert when no drawable stroke exists. Marker scaling resolves + the same attribute on its separate live path. Direct `var()`, `env()` + fallback, typed `attr()`, and experimental `if()` forms remain stable + function-named refusals, and authored CSS `vector-effect` remains a separate + stable property refusal because the pinned Stylo build has no corresponding + longhand; no second matcher is added. Graphics elements outside this admitted + slice keep their own element-level refusals. `stroke-dasharray` is consumed in both spellings from that same cascade: numbers/lengths/percentages, comma or whitespace separators, CSS math, odd-list repetition, inheritance through containers and ``, and diff --git a/crates/rframe/src/lib.rs b/crates/rframe/src/lib.rs index e6a7b654..a932d35a 100644 --- a/crates/rframe/src/lib.rs +++ b/crates/rframe/src/lib.rs @@ -40,5 +40,5 @@ pub use path::{FillRule, PathCommand, PathData, PathDataError}; pub use scope::{Scope, ScopeEffect, ScopeOpacity, ScopeOpacityError}; pub use stroke::{ Stroke, StrokeCap, StrokeDash, StrokeDashError, StrokeDashIntervals, StrokeDashIntervalsError, - StrokeError, StrokeJoin, + StrokeError, StrokeJoin, StrokeSpace, }; diff --git a/crates/rframe/src/stroke.rs b/crates/rframe/src/stroke.rs index 52beb7cd..330678e9 100644 --- a/crates/rframe/src/stroke.rs +++ b/crates/rframe/src/stroke.rs @@ -1,8 +1,8 @@ //! The resolved stroke — a second painted fact per node, beside the fill. //! //! A stroke is carried, not derived: the producer resolves the paint, the width -//! in the node's local space, the three shapes a corner or an end can take, and -//! an optional dash pattern, and the consumer paints exactly that. +//! in one declared construction space, the three shapes a corner or an end can +//! take, and an optional dash pattern, and the consumer paints exactly that. //! //! **Centred, with no alignment field.** A Web stroke straddles its geometry: //! half the width falls inside the outline and half outside. That is the only @@ -10,11 +10,13 @@ //! an inside- or outside-aligned stroke would grow this type when a producer //! that needs it arrives. //! -//! **In local space.** The width is a length in the same space as the geometry, -//! so the node's transform scales it — including non-uniformly, which turns the -//! pen elliptical. That is what a browser does (measured: `scale(2,1)` on a -//! stroked rect yields a stroke twice as wide horizontally as vertically), and -//! it follows from transforming the stroke *outline* rather than the width. +//! **Construction space is explicit.** An ordinary stroke is widened and +//! dashed in the geometry's local space before the node transform, so a +//! non-uniform transform turns its pen elliptical. A frame-space stroke first +//! maps the centerline through the node transform, then applies the same scalar +//! width, dash intervals, and phase in resolved frame coordinates. The host +//! view remains downstream of both meanings; this contract never bakes a +//! camera into a frame. //! //! **An invisible stroke is not a stroke.** Construction *resolves* a width or //! a paint stack that cannot paint to `Ok(None)` — not an error, because @@ -28,6 +30,22 @@ use crate::frame::{PaintAlphaFactor, PaintStack}; +/// The coordinate system in which a stroke is dashed and widened. +/// +/// This is a source-neutral resolved fact. It names neither SVG syntax nor a +/// backend strategy: any producer may require a stroke whose construction is +/// fixed before or after the node's local-to-frame mapping. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StrokeSpace { + /// Construct the stroke around local geometry, then apply the node + /// transform to the complete outline. + #[default] + Local, + /// Apply the node transform to the centerline, then construct the stroke + /// in resolved frame coordinates. A later host view still applies. + Frame, +} + /// The shape of a stroked contour's open ends. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum StrokeCap { @@ -62,7 +80,7 @@ pub enum StrokeDashIntervalsError { /// A resolved cycle contains complete painted-gap pairs. Repetition of an /// odd authored list belongs to the producer and has already happened. OddIntervalCount { count: usize }, - /// Every interval is a finite local-space distance. + /// Every interval is a finite distance in its stroke's construction space. NonFiniteInterval { index: usize }, /// Every interval is non-negative. Zero-length intervals remain /// meaningful under round and square caps. @@ -100,11 +118,11 @@ impl std::error::Error for StrokeDashIntervalsError {} /// One checked stroke dash interval cycle. /// -/// The immutable intervals are local-space path distances before the node -/// transform. They alternate painted, unpainted, painted, unpainted, beginning -/// with paint, and the cycle restarts at the beginning of every contour. A -/// present cycle is non-empty and even-length; every interval is finite and -/// non-negative; and their `f32` sum is finite and positive. +/// The immutable intervals are path distances in their owning stroke's +/// declared construction space. They alternate painted, unpainted, painted, +/// unpainted, beginning with paint, and the cycle restarts at the beginning of +/// every contour. A present cycle is non-empty and even-length; every interval +/// is finite and non-negative; and their `f32` sum is finite and positive. /// /// Source syntax does not cross this type. A producer has already resolved /// units, percentages, and odd-list repetition. This type deliberately owns @@ -153,7 +171,7 @@ impl StrokeDashIntervals { })) } - /// The alternating painted and unpainted local-space distances. + /// The alternating painted and unpainted construction-space distances. #[must_use] pub fn as_slice(&self) -> &[f32] { &self.intervals @@ -173,8 +191,8 @@ impl StrokeDashIntervals { /// Why a checked dash cycle cannot be paired with one resolved phase. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StrokeDashError { - /// A resolved local-space phase must be finite before it can be reduced - /// into the finite cycle. + /// A resolved construction-space phase must be finite before it can be + /// reduced into the finite cycle. NonFinitePhase, } @@ -192,10 +210,10 @@ impl std::error::Error for StrokeDashError {} /// One checked, source-neutral stroke dash pattern. /// -/// The intervals and phase are local-space path distances before the node -/// transform. At contour distance `s`, the alternating cycle is observed at -/// `s + phase`: a positive phase advances into the cycle. The same phase -/// restarts at the beginning of every contour. +/// The intervals and phase are path distances in their owning stroke's +/// declared construction space. At contour distance `s`, the alternating +/// cycle is observed at `s + phase`: a positive phase advances into the cycle. +/// The same phase restarts at the beginning of every contour. /// /// Construction is the sole normalization owner. It reduces every finite /// phase modulo the positive cycle length into the canonical half-open range @@ -210,7 +228,7 @@ pub struct StrokeDash { } impl StrokeDash { - /// Pair a checked positive cycle with one finite local-space phase. + /// Pair a checked positive cycle with one finite construction-space phase. pub fn new(intervals: StrokeDashIntervals, phase: f32) -> Result { if !phase.is_finite() { return Err(StrokeDashError::NonFinitePhase); @@ -243,7 +261,7 @@ impl StrokeDash { &self.intervals } - /// The canonical local-space phase in `[0, cycle_length)`. + /// The canonical construction-space phase in `[0, cycle_length)`. #[must_use] pub const fn phase(&self) -> f32 { self.phase @@ -281,6 +299,7 @@ impl std::error::Error for StrokeError {} pub struct Stroke { paints: PaintStack, width: f32, + space: StrokeSpace, cap: StrokeCap, join: StrokeJoin, miter_limit: f32, @@ -333,8 +352,8 @@ impl Stroke { /// when nothing would be painted. /// /// Dash absence states a solid stroke. A present value pairs a checked - /// positive interval cycle with its canonical local-space phase, so phase - /// cannot exist without a cycle. + /// positive interval cycle with its canonical construction-space phase, so + /// phase cannot exist without a cycle. pub fn new_with_dash( paints: PaintStack, width: f32, @@ -362,6 +381,7 @@ impl Stroke { Ok(Some(Self { paints, width, + space: StrokeSpace::Local, cap, join, miter_limit, @@ -376,17 +396,33 @@ impl Stroke { /// Attach the factor applied after every stroke-paint entry's own alpha. /// - /// This changes only the stroke's [`PaintStack`]; width, cap, join, miter - /// limit, and dash remain exactly as resolved. A zero factor normalizes - /// the paint stack away, so the complete stroke becomes `None` rather than - /// retain an invisible stroke that violates this type's invariant. + /// This changes only the stroke's [`PaintStack`]; width, construction + /// space, cap, join, miter limit, and dash remain exactly as resolved. A + /// zero factor normalizes the paint stack away, so the complete stroke + /// becomes `None` rather than retain an invisible stroke that violates this + /// type's invariant. #[must_use] pub fn with_paint_alpha_factor(mut self, alpha_factor: PaintAlphaFactor) -> Option { self.paints = self.paints.with_alpha_factor(alpha_factor); (!self.paints.is_empty()).then_some(self) } - /// The stroke width, in the node's local space. + /// Select the coordinate system in which this stroke is dashed and + /// widened. Construction defaults to [`StrokeSpace::Local`]. + #[must_use] + pub fn with_space(mut self, space: StrokeSpace) -> Self { + self.space = space; + self + } + + /// The coordinate system in which width, dash intervals, and phase are + /// consumed around the centerline. + #[must_use] + pub const fn space(&self) -> StrokeSpace { + self.space + } + + /// The stroke width, in the stroke's declared construction space. #[must_use] pub const fn width(&self) -> f32 { self.width @@ -425,9 +461,10 @@ impl Stroke { } } - /// How far the stroke can reach outside the geometry it follows, in local - /// space. Consumers need this to know what a stroked node covers: the - /// node's `bounds` is its *geometry*, and ink lies outside it. + /// How far the stroke can reach outside the geometry it follows, in its + /// declared construction space. Consumers need this to know what a stroked + /// node covers: the node's `bounds` is its *geometry*, and ink lies outside + /// it. /// /// Two independent reaches, whichever is larger — a corner and an end are /// different places and either can be the farthest: @@ -543,6 +580,36 @@ mod tests { assert_eq!(original.with_paint_alpha_factor(zero), None); } + #[test] + fn construction_space_defaults_local_and_changes_no_other_stroke_fact() { + let intervals = StrokeDashIntervals::new(vec![3.0, 2.0]) + .expect("valid intervals") + .expect("positive cycle"); + let dash = StrokeDash::new(intervals, -7.0).expect("finite phase"); + let local = Stroke::new_with_dash( + black(), + 8.0, + StrokeCap::Square, + StrokeJoin::Bevel, + 2.5, + Some(dash), + ) + .expect("valid stroke") + .expect("visible stroke"); + assert_eq!(local.space(), StrokeSpace::Local); + + let frame = local.clone().with_space(StrokeSpace::Frame); + let mut expected = local.clone(); + expected.space = StrokeSpace::Frame; + assert_eq!(frame, expected); + assert_eq!(frame.space(), StrokeSpace::Frame); + assert_eq!( + local.space(), + StrokeSpace::Local, + "the builder is persistent" + ); + } + #[test] fn an_unpaintable_stroke_refuses_by_name() { assert_eq!( diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index 958645fe..b3333aa2 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -152,7 +152,7 @@ use rframe::{ FilterTurbulenceKind, Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, MAX_FILTER_CONVOLVE_KERNEL_VALUES, Mask, MaskMode, PaintAlphaFactor, PaintStack, PathData, PatternPaint, Provenance, Scope, ScopeEffect, ScopeOpacity, Stroke, StrokeCap, - StrokeDash, StrokeDashIntervals, StrokeDashIntervalsError, StrokeJoin, VisualRef, + StrokeDash, StrokeDashIntervals, StrokeDashIntervalsError, StrokeJoin, StrokeSpace, VisualRef, }; use std::sync::Arc; @@ -1112,7 +1112,6 @@ const RENDERING_ATTRIBUTES_NOT_CONSUMED: &[&str] = &[ "image-rendering", "color-rendering", "color-interpolation", - "vector-effect", "requiredFeatures", "requiredExtensions", "systemLanguage", @@ -5732,7 +5731,19 @@ impl<'a> ChildWalk<'a> { let marker_scale = match viewport.units { marker_resource::Units::UserSpaceOnUse => 1.0, marker_resource::Units::StrokeWidth => { - resolve_stroke_width(client, &client.local_name_string(), bases)? + let width = resolve_stroke_width(client, &client.local_name_string(), bases)?; + if resolve_vector_effect_space(client)? == StrokeSpace::Frame { + // Blink keeps the ordinary marker placement transform, + // including its anisotropy and orientation, but divides + // the `strokeWidth` marker scale by the RMS scale of the + // non-scaling-stroke transform. This is deliberately + // not the frame-space stroke painter's circular-pen + // rule; Chromium exposes the distinct marker formula + // even when the client stroke itself is transparent. + non_scaling_stroke_marker_width(width, client_to_frame) + } else { + width + } } }; if marker_scale == 0.0 { @@ -13235,6 +13246,124 @@ struct StrokeResolution { opacity_pass: bool, } +/// Blink's `markerUnits="strokeWidth"` scale for a non-scaling-stroke client. +/// +/// The affine values have already crossed the resolved f32 contract. Blink +/// suppresses the marker when that backend matrix cannot supply a finite, +/// nonzero f32 determinant; only the RMS accumulation widens to f64 before the +/// returned scale re-enters Blink's f32 fact. +fn non_scaling_stroke_marker_width(width: f32, transform: AffineTransform) -> f32 { + let [[a, c, _], [b, d, _]] = transform.matrix; + let determinant = a * d - b * c; + if determinant == 0.0 || !determinant.is_finite() { + return 0.0; + } + let sum = f64::from(a) * f64::from(a) + + f64::from(b) * f64::from(b) + + f64::from(c) * f64::from(c) + + f64::from(d) * f64::from(d); + let scale = (sum / 2.0).sqrt() as f32; + if scale > 0.0 && scale.is_finite() { + width / scale + } else { + 0.0 + } +} + +#[cfg(test)] +mod vector_effect_marker_tests { + use super::*; + + #[test] + fn marker_stroke_width_uses_blinks_rms_affine_scale() { + assert_eq!( + non_scaling_stroke_marker_width( + 8.0, + AffineTransform::from_acebdf(2.0, 0.0, 0.0, 0.0, 2.0, 0.0), + ), + 4.0 + ); + assert_eq!( + non_scaling_stroke_marker_width( + 8.0, + AffineTransform::from_acebdf(0.0, -1.0, 0.0, 1.0, 0.0, 0.0), + ), + 8.0, + "a rotation has unit RMS scale" + ); + let non_uniform = non_scaling_stroke_marker_width( + 8.0, + AffineTransform::from_acebdf(2.0, 0.0, 0.0, 0.0, 1.0, 0.0), + ); + assert_eq!(non_uniform, 8.0 / 2.5_f32.sqrt()); + } + + #[test] + fn marker_stroke_width_suppresses_backend_unusable_maps() { + for transform in [ + AffineTransform::from_acebdf(1.0, 2.0, 0.0, 2.0, 4.0, 0.0), + AffineTransform::from_acebdf(1.0e-30, 0.0, 0.0, 0.0, 1.0e-30, 0.0), + AffineTransform::from_acebdf(1.0e30, 0.0, 0.0, 0.0, 1.0e30, 0.0), + ] { + assert_eq!(non_scaling_stroke_marker_width(8.0, transform), 0.0); + } + } +} + +/// Resolve the direct `vector-effect` presentation hint at the Stylo boundary. +/// +/// The pinned cascade has no Servo `vector-effect` longhand, so there is no +/// computed value to read and no honest matcher to add around it. Authored CSS +/// stays in [`CASCADE_PROPERTIES_NOT_REPRESENTED`]; this function owns only the +/// direct attribute spelling. The property is not inherited, but an explicit +/// `inherit` exposes the direct parent computed value, so a missing or invalid +/// parent hint stops at the initial local-space member rather than skipping to +/// a more distant ancestor. +fn resolve_vector_effect_space(element: HtmlElement<'_>) -> Result { + use cssparser::{Parser, ParserInput, Token}; + + let mut candidate = Some(element); + while let Some(current) = candidate { + let Some(raw) = get_attr(current, "vector-effect") else { + return Ok(StrokeSpace::Local); + }; + let mut input = ParserInput::new(&raw); + let mut parser = Parser::new(&mut input); + let parsed: Result<_, cssparser::ParseError<'_, ()>> = + parser.parse_entirely(|input| input.expect_ident_cloned().map_err(Into::into)); + if let Ok(ident) = parsed { + match ident.to_ascii_lowercase().as_str() { + "non-scaling-stroke" => return Ok(StrokeSpace::Frame), + "inherit" => { + candidate = current.traversal_parent(); + continue; + } + // `none` is the initial member. `unset` therefore selects it + // for this non-inherited property; the two revert spellings + // remove the presentation hint and reach the same result. + "none" | "initial" | "unset" | "revert" | "revert-layer" => { + return Ok(StrokeSpace::Local); + } + // Chromium 149 drops malformed identifiers and every tested + // draft/at-risk grammar member to the initial `none` result. + _ => return Ok(StrokeSpace::Local), + } + } + + let mut input = ParserInput::new(&raw); + let mut parser = Parser::new(&mut input); + if let Ok(Token::Function(name)) = parser.next() { + let name = name.as_ref().to_ascii_lowercase(); + return Err(CompileError::UnsupportedStroke(format!( + "vector-effect presentation attribute uses {name}(), whose computed result is not represented at this Stylo pin" + ))); + } + // Every other malformed value contributes no presentation hint. + return Ok(StrokeSpace::Local); + } + Ok(StrokeSpace::Local) +} + impl StrokeResolution { fn none() -> Self { Self { @@ -13490,8 +13619,15 @@ fn resolve_stroke( // The cascade's non-negative types make a rejection here unreachable from a // document, so it would be this compiler's bug — named, never painted. + // Resolve construction space only after proving that this element owns a + // drawable stroke. An unsupported substitution function is inert when + // there is no stroke to construct; markerUnits="strokeWidth" performs its + // own resolution above because markers remain meaningful without stroke + // paint. + let vector_effect_space = resolve_vector_effect_space(el)?; let stroke = Stroke::new_with_dash(paints, width, cap, join, miter_limit, dash) - .map_err(|error| CompileError::UnsupportedStroke(error.to_string()))?; + .map_err(|error| CompileError::UnsupportedStroke(error.to_string()))? + .map(|stroke| stroke.with_space(vector_effect_space)); Ok(StrokeResolution { stroke, opacity_pass: true, diff --git a/crates/websem/tests/strokes_contract.rs b/crates/websem/tests/strokes_contract.rs index 7529827e..0d938d09 100644 --- a/crates/websem/tests/strokes_contract.rs +++ b/crates/websem/tests/strokes_contract.rs @@ -1,13 +1,12 @@ //! The stroke contract: what the one SVG compiler consumes for a stroke, how //! it resolves, and what still refuses by name. //! -//! A Web stroke is centred on its geometry, its width is a length in the same -//! space the geometry lives in, and everything about it — the paint, the width -//! with its units, the cap and join keywords, the miter limit — arrives as a -//! typed value from the one cascade. So inheritance through a ``, SVG2 -//! precedence, unit resolution and CSS keyword case-insensitivity are the -//! cascade's behaviour and not this compiler's, and the laws below pin the -//! resolved facts plus the refusals that keep the unconsumed half honest. +//! A Web stroke is centred on its geometry. Its paint, width, cap, join, miter +//! limit, dash, and construction space arrive as resolved facts; the consumer +//! never recovers their meaning from source syntax. So inheritance through a +//! ``, SVG2 precedence, unit resolution and CSS keyword case-insensitivity +//! are producer behaviour, and the laws below pin the resolved facts plus the +//! refusals that keep the unconsumed half honest. //! //! Every pixel claim here was measured in Chromium 149 first; the corpus bakes //! them (`fixtures/web-first/svg-stroke-*.svg`, 114 of 115 byte-exact — only @@ -17,7 +16,7 @@ #[allow(dead_code)] mod support; -use rframe::{Geometry, PathCommand, StrokeCap, StrokeJoin}; +use rframe::{Geometry, PathCommand, StrokeCap, StrokeJoin, StrokeSpace}; use support::render_through_n0; use websem::{CompileError, DegradationAction, InitialViewport, SvgFrameSource}; @@ -915,23 +914,152 @@ fn line_endpoint_css_value_families_refuse_by_exact_attribute() { // ─── what still refuses ────────────────────────────────────────────────── -/// The stroke properties this slice does not consume refuse by name through -/// both authored spellings. Dasharray and the guarded dashoffset capability -/// have left this list; paint ordering and non-scaling geometry have not. +/// The direct presentation attribute resolves one source-neutral stroke-space +/// fact. CSS tokenization supplies comments, case folding, and escapes even +/// though the pinned cascade has no computed `vector-effect` longhand. #[test] -fn the_unconsumed_stroke_properties_refuse_by_name() { - for attr in [ - r##"paint-order="stroke""##, - r##"vector-effect="non-scaling-stroke""##, +fn direct_vector_effect_resolves_the_stroke_construction_space() { + for (value, expected) in [ + ("none", StrokeSpace::Local), + ("initial", StrokeSpace::Local), + ("unset", StrokeSpace::Local), + ("revert", StrokeSpace::Local), + ("revert-layer", StrokeSpace::Local), + ("unknown", StrokeSpace::Local), + ("non-scaling-stroke trailing", StrokeSpace::Local), + (" non-scaling-stroke ", StrokeSpace::Frame), + ("NON-SCALING-STROKE", StrokeSpace::Frame), + ("/**/non-scaling-stroke/**/", StrokeSpace::Frame), + (r"non-scaling-str\6f ke", StrokeSpace::Frame), + ] { + let source = stroked_rect(&format!(r##"stroke-width="8" vector-effect="{value}""##)); + assert_eq!( + stroke_of(&admit_both(&source), 0).space(), + expected, + "{value:?}" + ); + } + + let inherited = admit_both(&document( + r##" + + "##, + )); + assert_eq!(stroke_of(&inherited, 0).space(), StrokeSpace::Frame); +} + +/// Chromium drops the SVG 2 at-risk members rather than applying a partial +/// vector effect. Carry the initial `none` result; never reinterpret one as +/// non-scaling-stroke merely because it appears in the same listed grammar. +#[test] +fn chromium_dropped_vector_effect_members_resolve_to_none() { + for value in [ + "non-scaling-size", + "non-rotation", + "fixed-position", + "non-scaling-size non-rotation fixed-position", + "non-scaling-stroke non-scaling-stroke", + "non-scaling-stroke viewport", + "non-scaling-stroke screen", + ] { + let source = stroked_rect(&format!(r##"stroke-width="8" vector-effect="{value}""##)); + assert_eq!( + stroke_of(&admit_both(&source), 0).space(), + StrokeSpace::Local, + "Chromium-dropped {value:?}" + ); + } +} + +/// Presentation-attribute substitution functions can hide a live keyword +/// from this raw-attribute route. Chromium resolves `var()`, `env()` fallback, +/// typed `attr()`, and `if()` to non-scaling-stroke in the measured cases. +/// Both policies name each function instead of silently painting the initial +/// value. The patrol is deliberately function-generic so a later substitution +/// function cannot become the same wrong pixel unnoticed. +#[test] +fn vector_effect_attribute_functions_refuse_by_name_in_both_policies() { + for (function, attributes) in [ + ( + "var()", + r##"stroke-width="8" vector-effect="var(--ve)" style="--ve: non-scaling-stroke""##, + ), + ( + "env()", + r##"stroke-width="8" vector-effect="env(--missing-ve, non-scaling-stroke)""##, + ), + ( + "attr()", + r##"stroke-width="8" data-ve="non-scaling-stroke" vector-effect="attr(data-ve type(<custom-ident>), none)""##, + ), + ( + "if()", + r##"stroke-width="8" style="--ve-on: yes" vector-effect="if(style(--ve-on): non-scaling-stroke; else: none)""##, + ), ] { - let source = stroked_rect(&format!(r##"stroke-width="8" {attr}"##)); + let source = stroked_rect(attributes); + let error = refusal(&source); assert!( - matches!(refusal(&source), CompileError::UnsupportedAttribute { .. }), - "{attr} must refuse by name" + matches!(&error, CompileError::UnsupportedStroke(reason) + if reason.contains("vector-effect presentation attribute") + && reason.contains(function)), + "{function}: unexpected strict refusal: {error}" ); + let best = + SvgFrameSource::from_standalone_svg_best_effort(source.as_str(), viewport(64.0, 64.0)) + .expect("best effort compiles the remaining document"); + let skipped: Vec<_> = best + .degradations() + .iter() + .filter(|d| d.action() == DegradationAction::Skipped) + .collect(); + assert_eq!(skipped.len(), 1, "{function}"); + assert_eq!(skipped[0].reason(), error.to_string(), "{function}"); } - // The same values through a stylesheet, where only a computed-level read or - // the CSS-name patrol can catch them. +} + +/// A substitution function matters only when construction space can affect +/// pixels. Chromium 149 keeps a fill-only shape and a zero-width stroke exact +/// to their controls; inspecting the function before those stroke exits made +/// best effort skip both otherwise valid fills (measured: 1,024 pixels at +/// maximum channel delta 233). Marker scaling resolves the attribute on its +/// separate live path, even when the client's stroke paint is `none`. +#[test] +fn vector_effect_functions_are_inert_without_a_drawable_stroke() { + let source = document( + r##" + "##, + ); + let frame = admit_both(&source); + assert_eq!(frame.nodes().len(), 2); + assert!(frame.nodes().iter().all(|node| node.stroke.is_none())); + let pixels = render_through_n0(&frame, 64, 64); + assert_eq!(at(&pixels, 16, 32), [0x16, 0xa3, 0x4a, 255]); + assert_eq!(at(&pixels, 24, 16), [0x25, 0x63, 0xeb, 255]); + + let marker = document( + r##" + "##, + ); + let error = refusal(&marker); + assert!( + matches!(&error, CompileError::UnsupportedStroke(reason) + if reason.contains("vector-effect presentation attribute uses var()")), + "the live marker branch still refuses the hidden member: {error}" + ); +} + +/// Paint ordering still refuses through either spelling. The CSS property +/// twin of the now-admitted direct `vector-effect` attribute also remains +/// quarantined because this Stylo build exposes no computed longhand. +#[test] +fn unrepresented_stroke_properties_refuse_by_name() { + let source = stroked_rect(r##"stroke-width="8" paint-order="stroke""##); + assert!( + matches!(refusal(&source), CompileError::UnsupportedAttribute { .. }), + "paint-order attribute must refuse by name" + ); + for css in ["paint-order: stroke", "vector-effect: non-scaling-stroke"] { let source = document(&format!( r##" diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index d6047867..a0e8d3c6 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -879,11 +879,6 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "stroke-width in", ), - ( - "svg-stroke-vector-effect", - DeclaredByBestEffort, - "vector-effect", - ), ( "svg-stroke-width-calc-mixed", DeclaredByBestEffort, @@ -1062,6 +1057,31 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "unsupported element ", ), + ( + "svg-vector-effect-attribute-attr", + DeclaredByBestEffort, + "vector-effect presentation attribute uses attr()", + ), + ( + "svg-vector-effect-attribute-env", + DeclaredByBestEffort, + "vector-effect presentation attribute uses env()", + ), + ( + "svg-vector-effect-attribute-if", + DeclaredByBestEffort, + "vector-effect presentation attribute uses if()", + ), + ( + "svg-vector-effect-attribute-var", + DeclaredByBestEffort, + "vector-effect presentation attribute uses var()", + ), + ( + "svg-vector-effect-css-property", + DeclaredByBestEffort, + "vector-effect", + ), ("svg-viewbox-invalid-token", BothRefuse, "viewBox"), ("svg-viewbox-repeated-comma", BothRefuse, "viewBox"), ("svg-viewbox-trailing-comma", BothRefuse, "viewBox"), diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 9adaaee1..6cb8c5f0 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -49,9 +49,11 @@ from the dated addenda below: including `context-fill`/`context-stroke` selected through same-document use instances and fully resolved before the frame; with - centred stroke geometry, the closed cap/join family, opacity, and resolved - dash patterns with a checked cycle, signed local-space phase, and - `pathLength` source calibration; + centred local- or frame-space stroke construction, including the direct + `vector-effect="non-scaling-stroke"` presentation hint, the closed cap/join + family, opacity, and resolved dash patterns with a checked cycle, signed + phase in the selected construction space, and `pathLength` source + calibration; `` and `` containers, visibility, isolated element/group/root opacity, and HTML-ancestor opacity around the selected inline SVG; the whole `transform` grammar in both spellings (the attribute is a @@ -109,7 +111,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,079 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,127 Chromium-baked primitive cells plus 16 sampled frames, with a separate fifteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -121,7 +123,7 @@ from the dated addenda below: against Chromium's Skia; 18 knife-edge pixels between this engine's own macOS and Linux Skia builds; 336 ramp pixels under an isolated layer's restore; 576 after a masked ramp becomes luminance alpha). The named refusal - register has 226 rows. + register has 230 rows. - **Not claimed:** no conformance score exists or may be computed — FLIP is unratified. The FLIP record and identity-changing review are prepared, but only the owner act on gridaco/nothing#49 may authorize them and the first @@ -5365,3 +5367,107 @@ The primitive corpus moves from 1,051 to 1,079 exact Chromium cells plus the unchanged 16 sampled frames; the named register moves to 226 rows. No tolerance was added, no conformance score was produced, and no FLIP record, rule, or baseline changed. + +## Rung: SVG direct `vector-effect` presentation attribute (2026-09-05) + +The verdict is VE1 CLOSE/SPLIT. The direct SVG presentation-attribute row +closes at the complete standard-track +[`vector-effect` grammar](https://www.w3.org/TR/SVG2/coords.html#VectorEffects). +The CSS property row stays open: the pinned Stylo revision has no Servo +`vector-effect` longhand, so inline and stylesheet declarations retain a +stable property refusal instead of acquiring a matcher beside the cascade. +Direct substitution functions remain independently tracked function gaps. +This is the same attribute/property and own-row split ratified by +gridaco/nothing#75, #80, #92, and #94. Graphics elements outside the admitted +slice remain bounded by their own unchecked element rows; every admitted +stroke-bearing geometry route is included here. + +Chromium 149.0.7827.55 was measured first through the shared pinned capture +module. Missing and `none` are ordinary local stroking. +`non-scaling-stroke` transforms the centerline into frame space and then +applies the nominal width as one circular pen. Whitespace, CSS comments, +ASCII-insensitive case, and an escaped identifier are exact aliases. The +property is not inherited, but explicit `inherit` takes the direct parent's +computed value; the other CSS-wide spellings reach the initial result. A +use-site spelling is inert in current Chromium, while a spelling on referenced +geometry stays active through the instance transform. Chromium drops the +at-risk `non-scaling-size`, `non-rotation`, `fixed-position`, combination, +repeated-member, `viewport`, and `screen` forms to the initial result. Those +valid listed drops are committed evidence under the gridaco/nothing#77 +precedent, not an engine invention. + +The implementation adds one checked source-neutral fact to the resolved +stroke: `StrokeSpace::Local | Frame`. `websem` owns the direct-attribute parse +because no computed longhand exists at this pin; no SVG syntax crosses the +contract. `n0` projects frame-space geometry before dash and stroke +construction, maps gradient/image/pattern paint coordinates through the same +affine, computes conservative frame coverage from transformed geometry plus +the nominal pen, and otherwise reuses the existing native stroke operations. +Ordinary local strokes keep their old route. Dash lengths and phase are +nominal frame distances after `pathLength` has calibrated them in the original +local metric. Marker expansion remains producer-side: +`markerUnits="userSpaceOnUse"` is independent, while +`markerUnits="strokeWidth"` divides the client width by Blink's root-mean- +square affine scale and retains ordinary marker anisotropy and orientation. + +The scratch law pass found two silent pixel defects before close. First, +explicitly transforming a large source-space centerline changed f32 +cancellation under a pure translation, although translation cannot alter +stroke construction. Frame strokes whose linear map is exactly identity now +take the equivalent established local execution route; exact reflection and +quarter-turn deliberately do not. Second, classifying invertibility in f64 +painted an extra stroke dot and marker for mathematically invertible matrices +whose determinant underflows or overflows f32. Chromium and the pinned backend +suppress those alongside singular maps. Stroke projection and stroke-width +marker scaling therefore share the finite, nonzero f32-determinant boundary. +Focused unit laws guard both causes. + +The final independent LAW pass found three more silent ingress classes before +landing. Chromium resolves a custom `env()` fallback and typed `attr()` to +`non-scaling-stroke`; it also resolves a true CSS `if()` style-query branch to +that member. The old raw parser treated each function as invalid and painted +the initial `none`, differing from Chromium by 512 pixels at maximum channel +delta 255 in every case (measured, not celled). Together with the already +measured live `var()` route, a function-generic patrol now refuses by exact +function name in strict and best-effort modes. `var()`, `env()`, and `attr()` +retain their own unchecked checklist rows. `if()` is defined by +[CSS Values 5](https://www.w3.org/TR/css-values-5/), whose status declares the +module early-exploration draft work; under the checklist header it does not +raise the standard-track grammar bar, but the named refusal still prevents a +silent pixel. + +Hosted review then found one avoidable over-refusal in that patrol. It ran +before the stroke resolver had proved that a stroke would be constructed. +Pinned Chromium makes a fill-only shape and a zero-width stroke carrying a +live `var()` fallback exact to controls. The old strict path refused, while +best effort skipped the otherwise valid fills and differed by 1,024 pixels at +maximum channel delta 233. Construction-space resolution now follows the +no-paint, zero-width, and empty-paint exits. The separate marker consumer still +resolves it when marker scaling is live: with stroke paint `none`, ordinary and +non-scaling `markerUnits="strokeWidth"` differ by 768 pixels at delta 255, and +the `var()` form equals the non-scaling result (measured, not celled). +`svg-vector-effect-inert-function` commits the two inert stroke branches. + +Forty-eight new exact Chromium cells cover token grammar and browser drops, +CSS-wide behavior and applicability, both source entries, ``, every +admitted geometry, curves and zero-length caps, dash/phase/`pathLength`, +computed stroke lengths, general and nested transforms, root and nested +`viewBox`, the two precision boundaries, all admitted paint-server classes, +clip/mask/filter/opacity composition, pattern/mask/marker source programs, +and marker units/orientation. Every source was rendered through strict and +best-effort `n0` during the scratch audit; every admitted result was exact. +No tolerance was added. + +Gate sensitivity was proved three ways. Replacing frame construction with +ordinary local stroking failed forty new cells, up to maximum channel delta +255. Removing the identity-linear execution route failed the large-viewBox +translation cell by 288 pixels at delta 255. Widening the determinant +classifier failed the stroke and marker boundary cells by 120 and 248 pixels, +at deltas 255 and 218. Restoring each law returned the complete fixture gate +to green. Moving the function patrol back ahead of the no-stroke exits made +the review-added inert-function cell fail at strict admission; restoring the +ordering regated the complete corpus. Replacing the former broad vector-effect +patrol with four direct function rows and the CSS-property row moves the named +refusal register from 226 to 230. The primitive corpus moves from 1,079 to 1,127 +exact Chromium cells plus the unchanged 16 sampled frames. No conformance score +was produced, and no FLIP record, rule, or baseline changed. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 4d5e1bd4..5058edb4 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -1105,6 +1105,12 @@ excluded. - [ ] `text-rendering` - [ ] `text-anchor` - [ ] `vector-effect` + +> **2026-09-05 attribute/property split:** the CSS property remains open at +> the pinned Stylo cap: this build has no Servo `vector-effect` longhand, and +> authored inline or stylesheet declarations refuse by stable property name +> instead of entering a second matcher. The direct presentation-attribute +> spelling is an independent row and closes below. - [ ] `stop-color` - [ ] `stop-opacity` @@ -2269,7 +2275,51 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `transform` - [ ] `transform-origin` - [ ] `unicode-bidi` -- [ ] `vector-effect` +- [x] `vector-effect` + +> **2026-09-05 close/split:** forty-eight exact Chromium cells carry the +> complete standard-track direct-attribute grammar. `none` and missing select +> ordinary local stroking; `non-scaling-stroke` transforms the centerline to +> frame space before applying one nominal circular pen. Whitespace, comments, +> case-insensitive and escaped identifiers, CSS-wide values, explicit +> inheritance, non-inheritance, invalid fallback, applicability, ``, all +> admitted geometry and dash routes, root/nested `viewBox`, general affine +> transforms, paint servers, clips, masks, filters, opacity, marker content, +> and both marker-unit systems are Chromium-baked. Chromium 149 drops the +> at-risk `non-scaling-size`, `non-rotation`, `fixed-position`, combination, +> `viewport`, and `screen` members to the initial result; the drop is celled +> under the gridaco/nothing#77 precedent. +> +> The checked resolved fact is source-neutral stroke construction space. +> Two probe-discovered precision laws are permanent cells: exact-translation +> mappings retain the established local f32 execution order, while singular +> and mathematically invertible mappings whose determinant is zero, non-finite, +> underflows, or overflows in f32 suppress the non-scaling stroke and its +> `markerUnits="strokeWidth"` marker exactly as Blink/Skia do. Replacing the +> feature with ordinary stroking failed forty cells up to delta 255; removing +> the translation route failed its cell by 288 pixels; widening determinant +> classification failed the stroke and marker cells by 120 and 248 pixels. +> Restoration returned all 1,127 primitive cells plus 16 sampled frames to +> green. A final function patrol found that Chromium resolves direct `var()` +> fallback, custom `env()` fallback, typed `attr()`, and `if()` to the live +> member; the former raw parse silently painted `none`, differing by 512 +> pixels at delta 255 for each case (measured, not celled). A generic +> function-name patrol now refuses those in both policies. `var()`, `env()`, +> and `attr()` retain their own unchecked function rows. `if()` belongs to +> [CSS Values 5](https://www.w3.org/TR/css-values-5/), which declares itself +> early-exploration draft work, so it does not raise the standard-track bar +> under this checklist's header rule. The CSS property twin above remains +> open. Replacing one broad patrol with four direct-function rows and the +> property row moves the refusal register from 226 to 230. Graphics elements +> outside the admitted slice retain their own +> unchecked element rows; every admitted stroke-bearing geometry route is +> covered here. Hosted review found that the function patrol initially ran +> before proving a drawable stroke: Chromium keeps fill-only and zero-width +> cases exact, while old best effort skipped 1,024 valid pixels at delta 233 +> and strict admission refused. The call now follows the no-stroke exits and a +> dedicated cell guards that ordering. The separate marker consumer still +> resolves the attribute when marker scaling is live (measured, not celled). +> No tolerance was added. - [x] `visibility` - [ ] `white-space` - [ ] `width` diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 0116ec9a..4abf2c1d 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,8 +19,8 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): fifteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,079 primitive cells plus 16 sampled frames, those twenty-three text -witnesses, and 226 named refusal rows. Pixel cells use byte equality: what each +estate is 1,127 primitive cells plus 16 sampled frames, those twenty-three text +witnesses, and 230 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The real-font witness grades geometry before rasterization and makes no Chromium @@ -135,6 +135,15 @@ pixel claim. | `svg-stroke-join-css-miter-clip.svg` · `svg-stroke-join-css-arcs.svg` | The declaration-level proof the drop happens at parse: CSS `miter-clip` or `arcs` over a `round` **attribute** paints round — the invalid declaration ceases to exist and the hint survives, where an implemented value would have changed the corner. | | `svg-stroke-miter-limit-css.svg` · `svg-stroke-miter-limit-css-below-one.svg` | The miter limit's CSS twin at the same forced bevel, and a below-one limit — valid in SVG2 where SVG 1.1 forbade it — is not dropped: no miter can satisfy it, so it bevels identically. A negative limit instead drops as invalid and the initial 4 miters, and a CSS limit beats the attribute spelling (both measured, neither celled). | | `svg-stroke-scaled-group.svg` · `svg-stroke-nonuniform-scale.svg` | The width is a length in local space, so a group's `scale(2)` doubles it and `scale(2,1)` makes the pen elliptical — the stroke *outline* is transformed, not the width. | +| `svg-vector-effect-{basic,token-grammar,browser-dropped-grammar,css-wide-inherit}.svg` | The direct `vector-effect` presentation-attribute grammar. Missing/`none` retain ordinary local stroking; canonical, whitespace-padded, commented, case-insensitive, and escaped `non-scaling-stroke` spellings select frame-space construction. The CSS-wide cell proves non-inheritance plus explicit `inherit`. Chromium 149 drops invalid text and the at-risk `non-scaling-size`, `non-rotation`, `fixed-position`, combination, repeated-member, `viewport`, and `screen` forms to the initial result. Each aggregate is exact to its separately measured hand-built control; the controls are not cells. | +| `svg-vector-effect-{nonuniform,rotate,skew,reflect,ancestor}-transform.svg` · `svg-vector-effect-{root-viewbox-up,root-viewbox-down,nested-viewbox}.svg` | Transform semantics: Blink maps the centerline into frame space, then applies a nominal circular pen. Uniform/non-uniform scale, rotation, skew, reflection, non-commuting ancestor/element maps, and both directions of root and nested `viewBox` mapping are exact; no scalar-width approximation can share these oracles. | +| `svg-vector-effect-{backend-determinant,large-viewbox-translation,large-reflection,large-quarter-turn}.svg` | The backend precision laws discovered before close. A pure translation retains the established local f32 draw order, avoiding large-coordinate cancellation; exact reflection and quarter-turn stay on the transformed-centerline route. Singular maps and invertible maps whose determinant underflows or overflows f32 suppress the stroke, while a visible sibling survives. Removing the translation route changes 288 pixels. Widening determinant classification paints 120 wrong pixels. | +| `svg-vector-effect-{shape-elements,path-vocabulary,zero-length-caps,circle,rounded-rect,dash-pathlength,computed-stroke-values,no-stroke}.svg` | Geometry and stroke interaction: all seven admitted geometry elements, arcs, curves, multiple contours, joins, zero-length round/square caps, percentage width, negative dash phase, odd cycles, and `pathLength` are exact. Dash lengths and phase are nominal in frame space after `pathLength` calibrates in the original local metric. Admitted computed width/dash math crosses unchanged; absent/zero stroke remains nothing. | +| `svg-vector-effect-inert-function.svg` | A live `var()` fallback is not inspected when there is no stroke to construct: one fill-only shape and one zero-width stroke retain their fills exactly. Before the review fix, strict admission refused and best effort skipped both shapes; the latter differed from Chromium by 1,024 pixels at Δ233. Marker scaling remains a separate live consumer even when stroke paint is `none` (measured, not celled): `markerUnits="strokeWidth"` under ordinary versus non-scaling construction differs by 768 pixels at Δ255, and the `var()` form equals the non-scaling result. | +| `svg-vector-effect-{group-applicability,link-applicability,root-applicability,use-element,use-target}.svg` · `html-inline-svg-vector-effect.html` | Applicability and entry routing. Container/root hints do not inherit, a use-site hint is inert in Chromium, and the referenced geometry's hint remains active through the instance transform. The direct standalone and first-inline-SVG entries share the same exact result. | +| `svg-vector-effect-marker-{stroke-width,user-space,rms-auto,fixed-angle,reflection,backend-determinant,content}.svg` | Marker interaction. `userSpaceOnUse` is unaffected. `strokeWidth` uses Blink's RMS affine scale while retaining auto/fixed orientation and reflection; a marker-source child may itself own a non-scaling stroke. The same f32 determinant boundary suppresses extreme marker instances, and widening it paints 248 wrong pixels. | +| `svg-vector-effect-{user-gradient,object-gradient,user-pattern,object-pattern,pattern-content,clip,mask-target,mask-source,filter-opacity,currentcolor}.svg` | Paint and effect composition. User-space and object-box gradient/pattern paints follow the transformed source coordinate system while only stroke construction changes. Pattern and mask source children may own the effect; target clip/mask/filter, fill, element/stroke opacity, and `currentColor` retain their established ordering. All are exact without a new tolerance. | +| *(measured, not celled — vector-effect remainder)* | Scratch Chromium matrices also covered identity and pure translation, unit reflection, near-cancellation, separate dash/phase controls, manual transformed-geometry controls, singular sibling isolation, marker-only controls, and ordinary/non-scaling pairs for every committed branch; every candidate rendered through strict and best-effort `n0`. A final function patrol found that direct `var()` fallback, custom `env()` fallback, typed `attr()`, and CSS `if()` each resolve to `non-scaling-stroke`; the old raw parser instead painted `none`, differing by 512 pixels at Δ255. They now refuse by exact function name in both policies. The first three retain their own unchecked function rows; `if()` belongs to the early-exploration CSS Values 5 draft and does not raise this checklist's standard-track bar. Authored CSS `vector-effect` is likewise live in Chromium and remains the separate stable property refusal. The old broad patrol was replaced by five focused refusal rows. | | `svg-stroke-zero-extent-rect.svg` | A zero-extent `` or `r="0"` `` renders nothing **including its stroke** (SVG2 §10.1) — baked as proof, since a naive stroke of a zero-extent box would draw a line. | | `svg-polygon-fill.svg` · `svg-points-trailing-comma.svg` | The points rung's fill cells: a triangle authored with mixed comma/whitespace separators, and the same shape with a trailing separator — which Blink accepts in `points` (measured), unlike the `viewBox` grammar. | | `svg-polygon-fill-rule-evenodd.svg` | A self-intersecting star under `fill-rule="evenodd"`: the hollow core is the cell's proof that the points shapes read the cascaded fill rule. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 82401a7d..48953120 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1079) +## Chromium-baked cells (1127) Each renders byte-exact against its committed Chromium oracle (seven curved cells and four gradient ramps carry a declared, bounded @@ -32,6 +32,7 @@ to its fixture source. No new image is committed for this view. html-inline-svg-currentcolor-rect html-inline-svg-nested-viewport html-inline-svg-pattern +html-inline-svg-vector-effect html-webpage-mockup svg-anchor-container svg-circle-defaults-clip @@ -1100,6 +1101,53 @@ to its fixture source. No new image is committed for this view. svg-use-wh-inert svg-use-xlink-href svg-use-xy +svg-vector-effect-ancestor-transform +svg-vector-effect-backend-determinant +svg-vector-effect-basic +svg-vector-effect-browser-dropped-grammar +svg-vector-effect-circle +svg-vector-effect-clip +svg-vector-effect-computed-stroke-values +svg-vector-effect-css-wide-inherit +svg-vector-effect-currentcolor +svg-vector-effect-dash-pathlength +svg-vector-effect-filter-opacity +svg-vector-effect-group-applicability +svg-vector-effect-inert-function +svg-vector-effect-large-quarter-turn +svg-vector-effect-large-reflection +svg-vector-effect-large-viewbox-translation +svg-vector-effect-link-applicability +svg-vector-effect-marker-backend-determinant +svg-vector-effect-marker-content +svg-vector-effect-marker-fixed-angle +svg-vector-effect-marker-reflection +svg-vector-effect-marker-rms-auto +svg-vector-effect-marker-stroke-width +svg-vector-effect-marker-user-space +svg-vector-effect-mask-source +svg-vector-effect-mask-target +svg-vector-effect-nested-viewbox +svg-vector-effect-no-stroke +svg-vector-effect-nonuniform-transform +svg-vector-effect-object-gradient +svg-vector-effect-object-pattern +svg-vector-effect-path-vocabulary +svg-vector-effect-pattern-content +svg-vector-effect-reflect-transform +svg-vector-effect-root-applicability +svg-vector-effect-root-viewbox-down +svg-vector-effect-root-viewbox-up +svg-vector-effect-rotate-transform +svg-vector-effect-rounded-rect +svg-vector-effect-shape-elements +svg-vector-effect-skew-transform +svg-vector-effect-token-grammar +svg-vector-effect-use-element +svg-vector-effect-use-target +svg-vector-effect-user-gradient +svg-vector-effect-user-pattern +svg-vector-effect-zero-length-caps svg-viewbox-only-sizing-rect svg-viewbox-unequal-default svg-viewbox-uniform-offset-rect @@ -1108,7 +1156,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (226) +## The refusal register (230) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1300,7 +1348,6 @@ its row into the cells above. | `svg-stroke-dashoffset-var` | declared | skipped svg/path[1]: unsupported stroke value "a stroke-dashoffset on resolves through var(), an indirection this patrol cannot follow" | | `svg-stroke-paint-order` | declared | skipped svg/rect[1]: unsupported rendering attribute paint-order on (not yet consumed) | | `svg-stroke-sheet-unit-width` | declared | declaration ignored at svg/style[1]: a stylesheet declares a stroke-width in ex, which needs a basis this cascade does not have; elements it matches render at the wrong width | -| `svg-stroke-vector-effect` | declared | skipped svg/g[1]/rect[1]: unsupported rendering attribute vector-effect on (not yet consumed) | | `svg-stroke-width-calc-mixed` | declared | skipped svg/rect[1]: unsupported stroke value "a calc() stroke-width mixing lengths and percentages is not consumed" | | `svg-stroke-width-font-basis` | declared | skipped svg/rect[1]: unsupported stroke value "a stroke-width in em on under an authored font-size carrying vw needs a basis this cascade does not have" | | `svg-stroke-width-percentage-precision-alias` | declared | skipped svg/path[1]: unsupported stroke value "stroke-width percentage precision alias loses Chromium used-value provenance" | @@ -1339,6 +1386,11 @@ its row into the cells above. | `svg-use-stylesheet` | declared | skipped svg/use[1]: unsupported : the document carries author CSS, and shadow-scoped selector matching is not yet consumed (selectors must match inside the cloned subtree alone — measured) | | `svg-use-svg-viewport` | declared | skipped svg/use[1]: unsupported : its referenced root needs the instance-sized viewport contract | | `svg-use-symbol` | declared | skipped svg/symbol[1]: unsupported element ; skipped svg/use[1]/symbol[1]: unsupported element | +| `svg-vector-effect-attribute-attr` | declared | skipped svg/rect[1]: unsupported stroke value "vector-effect presentation attribute uses attr(), whose computed result is not represented at this Stylo pin" | +| `svg-vector-effect-attribute-env` | declared | skipped svg/rect[1]: unsupported stroke value "vector-effect presentation attribute uses env(), whose computed result is not represented at this Stylo pin" | +| `svg-vector-effect-attribute-if` | declared | skipped svg/rect[1]: unsupported stroke value "vector-effect presentation attribute uses if(), whose computed result is not represented at this Stylo pin" | +| `svg-vector-effect-attribute-var` | declared | skipped svg/rect[1]: unsupported stroke value "vector-effect presentation attribute uses var(), whose computed result is not represented at this Stylo pin" | +| `svg-vector-effect-css-property` | declared | declaration ignored at svg/style[1]: a stylesheet declares vector-effect, which this cascade does not represent; elements it matches render without it | | `svg-viewbox-invalid-token` | **both refuse** | viewBox "0 0 invalid 64 64" is invalid | | `svg-viewbox-repeated-comma` | **both refuse** | viewBox "0 0,,64 64" is invalid | | `svg-viewbox-trailing-comma` | **both refuse** | viewBox "0 0 64 64," is invalid | diff --git a/fixtures/web-first/chromium/html-inline-svg-vector-effect.png b/fixtures/web-first/chromium/html-inline-svg-vector-effect.png new file mode 100644 index 00000000..f56308f0 Binary files /dev/null and b/fixtures/web-first/chromium/html-inline-svg-vector-effect.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-ancestor-transform.png b/fixtures/web-first/chromium/svg-vector-effect-ancestor-transform.png new file mode 100644 index 00000000..8829207c Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-ancestor-transform.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-backend-determinant.png b/fixtures/web-first/chromium/svg-vector-effect-backend-determinant.png new file mode 100644 index 00000000..297cfd59 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-backend-determinant.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-basic.png b/fixtures/web-first/chromium/svg-vector-effect-basic.png new file mode 100644 index 00000000..f56308f0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-basic.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-browser-dropped-grammar.png b/fixtures/web-first/chromium/svg-vector-effect-browser-dropped-grammar.png new file mode 100644 index 00000000..22f98e70 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-browser-dropped-grammar.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-circle.png b/fixtures/web-first/chromium/svg-vector-effect-circle.png new file mode 100644 index 00000000..4ef330a6 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-circle.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-clip.png b/fixtures/web-first/chromium/svg-vector-effect-clip.png new file mode 100644 index 00000000..3b26e30d Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-clip.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-computed-stroke-values.png b/fixtures/web-first/chromium/svg-vector-effect-computed-stroke-values.png new file mode 100644 index 00000000..cdea0fbf Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-computed-stroke-values.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-css-wide-inherit.png b/fixtures/web-first/chromium/svg-vector-effect-css-wide-inherit.png new file mode 100644 index 00000000..0fd36486 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-css-wide-inherit.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-currentcolor.png b/fixtures/web-first/chromium/svg-vector-effect-currentcolor.png new file mode 100644 index 00000000..c1389985 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-currentcolor.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-dash-pathlength.png b/fixtures/web-first/chromium/svg-vector-effect-dash-pathlength.png new file mode 100644 index 00000000..82f83c9c Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-dash-pathlength.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-filter-opacity.png b/fixtures/web-first/chromium/svg-vector-effect-filter-opacity.png new file mode 100644 index 00000000..99ba4140 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-filter-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-group-applicability.png b/fixtures/web-first/chromium/svg-vector-effect-group-applicability.png new file mode 100644 index 00000000..d974ab71 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-group-applicability.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-inert-function.png b/fixtures/web-first/chromium/svg-vector-effect-inert-function.png new file mode 100644 index 00000000..4a25971e Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-inert-function.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-large-quarter-turn.png b/fixtures/web-first/chromium/svg-vector-effect-large-quarter-turn.png new file mode 100644 index 00000000..f4fdbbe2 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-large-quarter-turn.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-large-reflection.png b/fixtures/web-first/chromium/svg-vector-effect-large-reflection.png new file mode 100644 index 00000000..f54092d5 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-large-reflection.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-large-viewbox-translation.png b/fixtures/web-first/chromium/svg-vector-effect-large-viewbox-translation.png new file mode 100644 index 00000000..d9b4b216 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-large-viewbox-translation.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-link-applicability.png b/fixtures/web-first/chromium/svg-vector-effect-link-applicability.png new file mode 100644 index 00000000..d974ab71 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-link-applicability.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-backend-determinant.png b/fixtures/web-first/chromium/svg-vector-effect-marker-backend-determinant.png new file mode 100644 index 00000000..b6e2f006 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-backend-determinant.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-content.png b/fixtures/web-first/chromium/svg-vector-effect-marker-content.png new file mode 100644 index 00000000..725647b8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-content.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-fixed-angle.png b/fixtures/web-first/chromium/svg-vector-effect-marker-fixed-angle.png new file mode 100644 index 00000000..17484726 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-fixed-angle.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-reflection.png b/fixtures/web-first/chromium/svg-vector-effect-marker-reflection.png new file mode 100644 index 00000000..12c8c2d1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-reflection.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-rms-auto.png b/fixtures/web-first/chromium/svg-vector-effect-marker-rms-auto.png new file mode 100644 index 00000000..0e44a030 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-rms-auto.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-stroke-width.png b/fixtures/web-first/chromium/svg-vector-effect-marker-stroke-width.png new file mode 100644 index 00000000..2170daa8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-stroke-width.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-marker-user-space.png b/fixtures/web-first/chromium/svg-vector-effect-marker-user-space.png new file mode 100644 index 00000000..16970ce0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-marker-user-space.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-mask-source.png b/fixtures/web-first/chromium/svg-vector-effect-mask-source.png new file mode 100644 index 00000000..c1389985 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-mask-source.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-mask-target.png b/fixtures/web-first/chromium/svg-vector-effect-mask-target.png new file mode 100644 index 00000000..a7b5b388 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-mask-target.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-nested-viewbox.png b/fixtures/web-first/chromium/svg-vector-effect-nested-viewbox.png new file mode 100644 index 00000000..44511253 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-nested-viewbox.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-no-stroke.png b/fixtures/web-first/chromium/svg-vector-effect-no-stroke.png new file mode 100644 index 00000000..b62a5fc8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-no-stroke.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-nonuniform-transform.png b/fixtures/web-first/chromium/svg-vector-effect-nonuniform-transform.png new file mode 100644 index 00000000..84eb697f Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-nonuniform-transform.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-object-gradient.png b/fixtures/web-first/chromium/svg-vector-effect-object-gradient.png new file mode 100644 index 00000000..6fec400e Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-object-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-object-pattern.png b/fixtures/web-first/chromium/svg-vector-effect-object-pattern.png new file mode 100644 index 00000000..f1c36fa2 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-object-pattern.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-path-vocabulary.png b/fixtures/web-first/chromium/svg-vector-effect-path-vocabulary.png new file mode 100644 index 00000000..1628f81c Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-path-vocabulary.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-pattern-content.png b/fixtures/web-first/chromium/svg-vector-effect-pattern-content.png new file mode 100644 index 00000000..e680527b Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-pattern-content.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-reflect-transform.png b/fixtures/web-first/chromium/svg-vector-effect-reflect-transform.png new file mode 100644 index 00000000..cd2b5700 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-reflect-transform.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-root-applicability.png b/fixtures/web-first/chromium/svg-vector-effect-root-applicability.png new file mode 100644 index 00000000..d974ab71 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-root-applicability.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-down.png b/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-down.png new file mode 100644 index 00000000..d974ab71 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-down.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-up.png b/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-up.png new file mode 100644 index 00000000..f56308f0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-root-viewbox-up.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-rotate-transform.png b/fixtures/web-first/chromium/svg-vector-effect-rotate-transform.png new file mode 100644 index 00000000..f032841d Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-rotate-transform.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-rounded-rect.png b/fixtures/web-first/chromium/svg-vector-effect-rounded-rect.png new file mode 100644 index 00000000..6f971955 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-rounded-rect.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-shape-elements.png b/fixtures/web-first/chromium/svg-vector-effect-shape-elements.png new file mode 100644 index 00000000..ec83190e Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-shape-elements.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-skew-transform.png b/fixtures/web-first/chromium/svg-vector-effect-skew-transform.png new file mode 100644 index 00000000..ba1674c5 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-skew-transform.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-token-grammar.png b/fixtures/web-first/chromium/svg-vector-effect-token-grammar.png new file mode 100644 index 00000000..6a9c488f Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-token-grammar.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-use-element.png b/fixtures/web-first/chromium/svg-vector-effect-use-element.png new file mode 100644 index 00000000..d974ab71 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-use-element.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-use-target.png b/fixtures/web-first/chromium/svg-vector-effect-use-target.png new file mode 100644 index 00000000..f56308f0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-use-target.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-user-gradient.png b/fixtures/web-first/chromium/svg-vector-effect-user-gradient.png new file mode 100644 index 00000000..4aa31466 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-user-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-user-pattern.png b/fixtures/web-first/chromium/svg-vector-effect-user-pattern.png new file mode 100644 index 00000000..7a8c18c0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-user-pattern.png differ diff --git a/fixtures/web-first/chromium/svg-vector-effect-zero-length-caps.png b/fixtures/web-first/chromium/svg-vector-effect-zero-length-caps.png new file mode 100644 index 00000000..4df26909 Binary files /dev/null and b/fixtures/web-first/chromium/svg-vector-effect-zero-length-caps.png differ diff --git a/fixtures/web-first/html-inline-svg-vector-effect.html b/fixtures/web-first/html-inline-svg-vector-effect.html new file mode 100644 index 00000000..f1fd0e11 --- /dev/null +++ b/fixtures/web-first/html-inline-svg-vector-effect.html @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index ef10ac3f..43666b4e 100644 --- a/fixtures/web-first/oracle-bake.json +++ b/fixtures/web-first/oracle-bake.json @@ -5,7 +5,7 @@ "bake_script_sha256": "2bdb5f933d072a1e87c9a675c3342fcf506c0955c0f5c26e2988f4e8fa37c4f2", "capture_module_sha256": "069296201718c43d29efe356fbea893781b73250dca51de3b6d47468d74027b0", "suite": "primitives.json", - "suite_sha256": "16e1e70bca9b18db72693142eaa6c7d11951cda13ff3b29e73869b80cba1acc6", + "suite_sha256": "51e5b49c419c78a8e0fbf0fcd37bceeb6d8aaf57421e996bd99c17ce912165ab", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -55,6 +55,15 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-vector-effect", + "source": "html-inline-svg-vector-effect.html", + "source_sha256": "ec1c0a018912518c948a453544128efbb1dd33aba1c6fc614df787f1d2c70764", + "oracle": "chromium/html-inline-svg-vector-effect.png", + "oracle_sha256": "73d2abaaf6de678790857eb4d21d641434d5164d4357ffd27fb8a6a38fa4f583", + "width": 64, + "height": 64 + }, { "id": "html-webpage-mockup", "source": "html-webpage-mockup.html", @@ -9667,6 +9676,429 @@ "width": 64, "height": 64 }, + { + "id": "svg-vector-effect-ancestor-transform", + "source": "svg-vector-effect-ancestor-transform.svg", + "source_sha256": "6c8dca00487c68eb7d441234390003e75b5e95550a3fa96d541165514c2a9fcb", + "oracle": "chromium/svg-vector-effect-ancestor-transform.png", + "oracle_sha256": "11452350ddeaea765cff86162d8f1ad1019675299dac06b83743f261016f3c1a", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-backend-determinant", + "source": "svg-vector-effect-backend-determinant.svg", + "source_sha256": "75c0a5a14b411e8281c23a546c313466d35568fae88710365f124ae32da4341c", + "oracle": "chromium/svg-vector-effect-backend-determinant.png", + "oracle_sha256": "8818c342b6b753835b57d7dfc798ce392db7fb28871efc5e6b8b32e7bc659429", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-basic", + "source": "svg-vector-effect-basic.svg", + "source_sha256": "af83df23d4e4d01be9f5222e0fca0a74da7c77bf4e790d2eb1de8894ce054a5e", + "oracle": "chromium/svg-vector-effect-basic.png", + "oracle_sha256": "73d2abaaf6de678790857eb4d21d641434d5164d4357ffd27fb8a6a38fa4f583", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-browser-dropped-grammar", + "source": "svg-vector-effect-browser-dropped-grammar.svg", + "source_sha256": "c89fdf124c1021516509379185faa54092e348632938f3c287e1f451f9ef4da8", + "oracle": "chromium/svg-vector-effect-browser-dropped-grammar.png", + "oracle_sha256": "439a5f0f7418587d0a8ac2bf5e33c755c1258db1220a02280366836fdc2ff2e1", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-circle", + "source": "svg-vector-effect-circle.svg", + "source_sha256": "8512604d12d9d68bd331d625d223f768dd8f5128275340127638ffe53e1cb045", + "oracle": "chromium/svg-vector-effect-circle.png", + "oracle_sha256": "4fefb0fca48159765bfdb461b7c844c51b87298a159ad418a5eeaa598da9f5df", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-clip", + "source": "svg-vector-effect-clip.svg", + "source_sha256": "4ad917682ebd7e6ffddca0592a249003543b25e504b5754d761a0d12b9a80d9b", + "oracle": "chromium/svg-vector-effect-clip.png", + "oracle_sha256": "46b7512ea23a785a8ce4c7e26c6669f1171653d5da13d333be2a2a32a15a21f8", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-computed-stroke-values", + "source": "svg-vector-effect-computed-stroke-values.svg", + "source_sha256": "5a1986efe7a72a0fd9be77661c4f47a9e6fac5d9c796a9fc7e07bf8e1cce13ed", + "oracle": "chromium/svg-vector-effect-computed-stroke-values.png", + "oracle_sha256": "ab952e5aaede4f58475cadcc8c96911ad040dd388a00c3902d55f816ec1d46f0", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-css-wide-inherit", + "source": "svg-vector-effect-css-wide-inherit.svg", + "source_sha256": "c66f30e193405406b3326388822900b062a901c6555ed833664a4c212eb100f9", + "oracle": "chromium/svg-vector-effect-css-wide-inherit.png", + "oracle_sha256": "9a395714bfa5c59c67e779f252b6e9ed9812e53410f7994db4955c21ac3f6a75", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-currentcolor", + "source": "svg-vector-effect-currentcolor.svg", + "source_sha256": "dc94905f00ea13c160df78db06ef955e89a36e447f7f39c12cdf9eedcd482af9", + "oracle": "chromium/svg-vector-effect-currentcolor.png", + "oracle_sha256": "237c4424619b6ebed2c004a84090ce68fdf7d188910a57d4564be6470249bfae", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-dash-pathlength", + "source": "svg-vector-effect-dash-pathlength.svg", + "source_sha256": "f1f7624def8ebb53cd18c8ae12bf5259e4e8ece6ded8592aa7aa42d75018f9cb", + "oracle": "chromium/svg-vector-effect-dash-pathlength.png", + "oracle_sha256": "871e4131e457af15bd387f5979c49bc8a2a106f21b001332a86394b76c60cb9e", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-filter-opacity", + "source": "svg-vector-effect-filter-opacity.svg", + "source_sha256": "7bf402f45869ba5c91d44a892b3eb39ed8dfc74b7b500d406b30c0d5725990f9", + "oracle": "chromium/svg-vector-effect-filter-opacity.png", + "oracle_sha256": "2d9410313ad26b53ef1b29c13d1ab89b755efd9461f0f499501e21f8ff2093be", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-group-applicability", + "source": "svg-vector-effect-group-applicability.svg", + "source_sha256": "55b78a702edfa59934ddde78adff83930bef6578d6f06ee57badf68bf1f134a0", + "oracle": "chromium/svg-vector-effect-group-applicability.png", + "oracle_sha256": "2c97c6ee9aff9c0f3ce4ed3abe9109bf31381fdbfc519b17583384d8c9a8d7cb", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-inert-function", + "source": "svg-vector-effect-inert-function.svg", + "source_sha256": "aade6e7d7850fd8d4a6941086da72826ac56a566bc2378258f96e149532b8fff", + "oracle": "chromium/svg-vector-effect-inert-function.png", + "oracle_sha256": "48188d916ce96e4fd6edb992d753b4e5f3c776175fe8e3e30647fb1e6ba946dd", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-quarter-turn", + "source": "svg-vector-effect-large-quarter-turn.svg", + "source_sha256": "f0e9ebe8b8905f1c63727a9e8c082062494efad87b4dac923617b321be7c3f87", + "oracle": "chromium/svg-vector-effect-large-quarter-turn.png", + "oracle_sha256": "e6ef99915ec7c83ce8f6eab147476a27411f02ad05c481f3b61c95163d2b54c1", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-reflection", + "source": "svg-vector-effect-large-reflection.svg", + "source_sha256": "7259375515ac89f74c0da144cf3c1427d69299243c7035338f8258d582d5d410", + "oracle": "chromium/svg-vector-effect-large-reflection.png", + "oracle_sha256": "9f740ac1b5183a4832d905ffda11e27efeed38bf7e59b415da0d48e582c338c8", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-viewbox-translation", + "source": "svg-vector-effect-large-viewbox-translation.svg", + "source_sha256": "c1192dd183e92a48e3e6b5af8212feef2722f613185907873795667af65c0d71", + "oracle": "chromium/svg-vector-effect-large-viewbox-translation.png", + "oracle_sha256": "4f852cfe622067e2872424c8add5d3977db6b7d6ad84a6329f288f6d7da566fa", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-link-applicability", + "source": "svg-vector-effect-link-applicability.svg", + "source_sha256": "525f4d183f5e46114506a9f8fc5d1c1c92533e4d2a75a097269f4aba1e73863b", + "oracle": "chromium/svg-vector-effect-link-applicability.png", + "oracle_sha256": "2c97c6ee9aff9c0f3ce4ed3abe9109bf31381fdbfc519b17583384d8c9a8d7cb", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-backend-determinant", + "source": "svg-vector-effect-marker-backend-determinant.svg", + "source_sha256": "2e2ebf9daa21d3ed5e19d1c9115fffdc2d67a195a26f8efa1f0a89a52853a518", + "oracle": "chromium/svg-vector-effect-marker-backend-determinant.png", + "oracle_sha256": "740b5698e360e76b82ba357a209aabb05517481ff154c697c9b6451f5827f16a", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-content", + "source": "svg-vector-effect-marker-content.svg", + "source_sha256": "9c564893da7dd5e5b1429175664ab964dda8675b4e95874b45181cc96f8ca9a0", + "oracle": "chromium/svg-vector-effect-marker-content.png", + "oracle_sha256": "a6f843066e76a38fe1e860fb7716bad9e823c74fbeb847450ce3b6712ea02919", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-fixed-angle", + "source": "svg-vector-effect-marker-fixed-angle.svg", + "source_sha256": "ab1205a6b25a76fd5ff2041a2030d7cb25faafe20142f2c33917cbcfefc88624", + "oracle": "chromium/svg-vector-effect-marker-fixed-angle.png", + "oracle_sha256": "8ec33bbc9fd288f77e9968bc9e5fbfc02451bc32862b2afa8d9c3e1d59c28b53", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-reflection", + "source": "svg-vector-effect-marker-reflection.svg", + "source_sha256": "1e4cb10ae678c7d322403f6f063630a6e498cc2183cf7237ffc4a96b03f61b55", + "oracle": "chromium/svg-vector-effect-marker-reflection.png", + "oracle_sha256": "8b90366f323038a9d1fb43327233c20962ceae2ffd2c199c4e6a9a53719e53cc", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-rms-auto", + "source": "svg-vector-effect-marker-rms-auto.svg", + "source_sha256": "130c4b5d408e78c4b51fb95bf9063f095f0a96a3040df4b757d7b718a335ed21", + "oracle": "chromium/svg-vector-effect-marker-rms-auto.png", + "oracle_sha256": "3248492423f300d672064c0d8d95afbdaa78ac1c563abaa1334cd9ae07c3f43c", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-stroke-width", + "source": "svg-vector-effect-marker-stroke-width.svg", + "source_sha256": "1356040d1f5f826d669c999f290b1407aaee904d7c0e17c0d22bc5dcf097d494", + "oracle": "chromium/svg-vector-effect-marker-stroke-width.png", + "oracle_sha256": "08e39a385ca232f459a7c4483d53ddfafa658ac1b6888515a8b653941e59c7d4", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-user-space", + "source": "svg-vector-effect-marker-user-space.svg", + "source_sha256": "4169b9fa297413ef822abb7531c304888358bbd51627d33f8ddf354a1534aa49", + "oracle": "chromium/svg-vector-effect-marker-user-space.png", + "oracle_sha256": "fa359463b627a06cf7a4b0f8fd0fe3c5276c0103f74a16ef139568a1a0b843ef", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-mask-source", + "source": "svg-vector-effect-mask-source.svg", + "source_sha256": "ec22fe8e5bd8bf777712f271491215237a0d5512e176566bf2de53f3cbb6deac", + "oracle": "chromium/svg-vector-effect-mask-source.png", + "oracle_sha256": "237c4424619b6ebed2c004a84090ce68fdf7d188910a57d4564be6470249bfae", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-mask-target", + "source": "svg-vector-effect-mask-target.svg", + "source_sha256": "362b8832905dc9293a0e327f7d0a4bb95c89ca8fb09cb628ec8ba6c9520b9693", + "oracle": "chromium/svg-vector-effect-mask-target.png", + "oracle_sha256": "70d85ff803bf834e1cb2d68412e96fa07e29465b6a3734d748e5944d023a1e3f", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-nested-viewbox", + "source": "svg-vector-effect-nested-viewbox.svg", + "source_sha256": "0b5393cadba1c9d72f8b768ceb020bec0882da2d36996ce7306f073cf77bb24c", + "oracle": "chromium/svg-vector-effect-nested-viewbox.png", + "oracle_sha256": "f492498246ae6c264aa3f6d350da375014eb3c52ecb293de79c9c7346f0fdf2e", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-no-stroke", + "source": "svg-vector-effect-no-stroke.svg", + "source_sha256": "08bed18f41fc742d6d96fb9ba70c3b0356d4df382f4409458e624718799ae1b3", + "oracle": "chromium/svg-vector-effect-no-stroke.png", + "oracle_sha256": "173b81ef1926c613e7a78552c09357e10c0bac7e36968b2d6e339a6e0a40251d", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-nonuniform-transform", + "source": "svg-vector-effect-nonuniform-transform.svg", + "source_sha256": "47048f60f61c17b94437d8e82c8b2a493bc1ab407b7e1f8abe6a61690f9ac489", + "oracle": "chromium/svg-vector-effect-nonuniform-transform.png", + "oracle_sha256": "8bb60f152207fdb11b3a0707c106cbbf97e11862fd8a413a1ca17e737e9b8416", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-object-gradient", + "source": "svg-vector-effect-object-gradient.svg", + "source_sha256": "33daa0c411b94c0fe8cac745d6888d5e6fb6c45d674debb4ae074551f8c5eecc", + "oracle": "chromium/svg-vector-effect-object-gradient.png", + "oracle_sha256": "8082e699d3597aab6249ad6e117aab0f67a9ba1029f633d06fbdbaee105907fd", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-object-pattern", + "source": "svg-vector-effect-object-pattern.svg", + "source_sha256": "fb9bfc81a9a67843b9c7048dc0fcf73e0ee6775a3e365ba0181bb86cbe959e21", + "oracle": "chromium/svg-vector-effect-object-pattern.png", + "oracle_sha256": "e0b902ad5e13a30ceaed1e94df7d7ab936884ef7ff5671cb47d387c918f85a2e", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-path-vocabulary", + "source": "svg-vector-effect-path-vocabulary.svg", + "source_sha256": "1cf94ae62fb141e51be58b61a3b22c6d58f7db8184db618bb1eaf17d6a0262aa", + "oracle": "chromium/svg-vector-effect-path-vocabulary.png", + "oracle_sha256": "be5fb1e4773ba6e7f68ff03cf7531df4160d01fdb3ba8a93c22ec41d3f239814", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-pattern-content", + "source": "svg-vector-effect-pattern-content.svg", + "source_sha256": "bfd5e238c188857fdd1d818c2afeb1b3e552d4ad8b7e16e9ce51493b6c46027b", + "oracle": "chromium/svg-vector-effect-pattern-content.png", + "oracle_sha256": "277971a9c9a8d4c0a9ad5149757822fff149dca6503e129599deed61249a97b8", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-reflect-transform", + "source": "svg-vector-effect-reflect-transform.svg", + "source_sha256": "b30f1bf4b52bd004b350905742b066a202e4fad5982aee3cec1f4532a29a3965", + "oracle": "chromium/svg-vector-effect-reflect-transform.png", + "oracle_sha256": "7162ac99ddde7321b2285a838619718d14f17ee8a45dc6d8adb9eca5024eb9f0", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-applicability", + "source": "svg-vector-effect-root-applicability.svg", + "source_sha256": "1146a665022d970974bcf5be31505f46abfffe2e3d730be2d3d6b2876070ded2", + "oracle": "chromium/svg-vector-effect-root-applicability.png", + "oracle_sha256": "2c97c6ee9aff9c0f3ce4ed3abe9109bf31381fdbfc519b17583384d8c9a8d7cb", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-viewbox-down", + "source": "svg-vector-effect-root-viewbox-down.svg", + "source_sha256": "b80d4223f131832bebbc65202d8333088b7cbaabddb82591d9866491c709ddd8", + "oracle": "chromium/svg-vector-effect-root-viewbox-down.png", + "oracle_sha256": "2c97c6ee9aff9c0f3ce4ed3abe9109bf31381fdbfc519b17583384d8c9a8d7cb", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-viewbox-up", + "source": "svg-vector-effect-root-viewbox-up.svg", + "source_sha256": "dea283c45db6dd806e4d7836764f706698507ae5ef5d3403143c6c28d7eac459", + "oracle": "chromium/svg-vector-effect-root-viewbox-up.png", + "oracle_sha256": "73d2abaaf6de678790857eb4d21d641434d5164d4357ffd27fb8a6a38fa4f583", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-rotate-transform", + "source": "svg-vector-effect-rotate-transform.svg", + "source_sha256": "2b6fd94d2adfee69094ab215ab9ec35d2fa225b33e840a1712cf542d6083f339", + "oracle": "chromium/svg-vector-effect-rotate-transform.png", + "oracle_sha256": "1e2656f3ae6f85772208e251790d14ba4dff3fcdc7bcaa52f8bc865ef2ef3ee1", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-rounded-rect", + "source": "svg-vector-effect-rounded-rect.svg", + "source_sha256": "14e53b4a074cce2a799241025760c05ef05e60fc363ebec65d46dd21dfae4bb8", + "oracle": "chromium/svg-vector-effect-rounded-rect.png", + "oracle_sha256": "72df7993a1a92fdab274e1110e9ea922867cf6d77d8ef6cdb828278951e6f03f", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-shape-elements", + "source": "svg-vector-effect-shape-elements.svg", + "source_sha256": "4f9876bd2a2a143abde77ca7fa8f19b6edac1d1c5475b4a8a1be86c6cd8853a3", + "oracle": "chromium/svg-vector-effect-shape-elements.png", + "oracle_sha256": "5a45038b73370030ec6ee019090f6ff2eb82300dae09a3d283b4619204e59664", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-skew-transform", + "source": "svg-vector-effect-skew-transform.svg", + "source_sha256": "463bc0b9d822c884b9d5dbb7b90cb76e14186a2cf2719f8e1667eb7a9936187c", + "oracle": "chromium/svg-vector-effect-skew-transform.png", + "oracle_sha256": "d6df7c01a9495bfe7eb575c6821b4ac4fa98f190b3597ae01f2016edfa3d0155", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-token-grammar", + "source": "svg-vector-effect-token-grammar.svg", + "source_sha256": "dc8c6e76226cebd5654eaac49a4dd99675a6b665a764f12f3fc8ea4986f314a4", + "oracle": "chromium/svg-vector-effect-token-grammar.png", + "oracle_sha256": "51d16aa4c8303bc0d7dcc0fb924187e073cc63c8d7071be8c29a93edc849e86c", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-use-element", + "source": "svg-vector-effect-use-element.svg", + "source_sha256": "9338eb013ddc1e250c50da71efc7bc5d525eabd0d310137f739b61c2dd783443", + "oracle": "chromium/svg-vector-effect-use-element.png", + "oracle_sha256": "2c97c6ee9aff9c0f3ce4ed3abe9109bf31381fdbfc519b17583384d8c9a8d7cb", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-use-target", + "source": "svg-vector-effect-use-target.svg", + "source_sha256": "c34b8a2dbfb29df23528b749b718e53ee85116df76895632f4075d8d533b077b", + "oracle": "chromium/svg-vector-effect-use-target.png", + "oracle_sha256": "73d2abaaf6de678790857eb4d21d641434d5164d4357ffd27fb8a6a38fa4f583", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-user-gradient", + "source": "svg-vector-effect-user-gradient.svg", + "source_sha256": "6170da5e0bec86536db98db4e41ea7225b626a651fd954fdaf92110aa07cac72", + "oracle": "chromium/svg-vector-effect-user-gradient.png", + "oracle_sha256": "570d358a4d1e6b1beb6426eb89a09297bf2ccbf5371679d2de591a796a39027c", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-user-pattern", + "source": "svg-vector-effect-user-pattern.svg", + "source_sha256": "5b549ad13e44099b7d6cee81967f62c73be2cf76856b4a6d6526528c8ced413f", + "oracle": "chromium/svg-vector-effect-user-pattern.png", + "oracle_sha256": "f07e575ebdd7624c1b61671866063c57c202d50d8ef0a0626b83d4dfc930f3e6", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-zero-length-caps", + "source": "svg-vector-effect-zero-length-caps.svg", + "source_sha256": "62df07b5c50379ae919fa5453a27ea7b1dc60f99376c2332345b0075900ece77", + "oracle": "chromium/svg-vector-effect-zero-length-caps.png", + "oracle_sha256": "191ea9073f2d7becd68ac65eef74d6c62ad8c82e5f4a8591f1e478691b9b9c90", + "width": 64, + "height": 64 + }, { "id": "svg-viewbox-only-sizing-rect", "source": "svg-viewbox-only-sizing-rect.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index 9ce27321..944d5155 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -33,6 +33,14 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-vector-effect", + "source": "html-inline-svg-vector-effect.html", + "entry": "html-inline-svg", + "oracle": "chromium/html-inline-svg-vector-effect.png", + "width": 64, + "height": 64 + }, { "id": "html-webpage-mockup", "source": "html-webpage-mockup.html", @@ -8688,6 +8696,382 @@ "width": 64, "height": 64 }, + { + "id": "svg-vector-effect-ancestor-transform", + "source": "svg-vector-effect-ancestor-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-ancestor-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-backend-determinant", + "source": "svg-vector-effect-backend-determinant.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-backend-determinant.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-basic", + "source": "svg-vector-effect-basic.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-basic.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-browser-dropped-grammar", + "source": "svg-vector-effect-browser-dropped-grammar.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-browser-dropped-grammar.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-circle", + "source": "svg-vector-effect-circle.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-circle.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-clip", + "source": "svg-vector-effect-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-computed-stroke-values", + "source": "svg-vector-effect-computed-stroke-values.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-computed-stroke-values.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-css-wide-inherit", + "source": "svg-vector-effect-css-wide-inherit.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-css-wide-inherit.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-currentcolor", + "source": "svg-vector-effect-currentcolor.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-currentcolor.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-dash-pathlength", + "source": "svg-vector-effect-dash-pathlength.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-dash-pathlength.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-filter-opacity", + "source": "svg-vector-effect-filter-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-filter-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-group-applicability", + "source": "svg-vector-effect-group-applicability.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-group-applicability.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-inert-function", + "source": "svg-vector-effect-inert-function.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-inert-function.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-quarter-turn", + "source": "svg-vector-effect-large-quarter-turn.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-large-quarter-turn.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-reflection", + "source": "svg-vector-effect-large-reflection.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-large-reflection.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-large-viewbox-translation", + "source": "svg-vector-effect-large-viewbox-translation.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-large-viewbox-translation.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-link-applicability", + "source": "svg-vector-effect-link-applicability.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-link-applicability.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-backend-determinant", + "source": "svg-vector-effect-marker-backend-determinant.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-backend-determinant.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-content", + "source": "svg-vector-effect-marker-content.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-content.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-fixed-angle", + "source": "svg-vector-effect-marker-fixed-angle.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-fixed-angle.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-reflection", + "source": "svg-vector-effect-marker-reflection.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-reflection.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-rms-auto", + "source": "svg-vector-effect-marker-rms-auto.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-rms-auto.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-stroke-width", + "source": "svg-vector-effect-marker-stroke-width.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-stroke-width.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-marker-user-space", + "source": "svg-vector-effect-marker-user-space.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-marker-user-space.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-mask-source", + "source": "svg-vector-effect-mask-source.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-mask-source.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-mask-target", + "source": "svg-vector-effect-mask-target.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-mask-target.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-nested-viewbox", + "source": "svg-vector-effect-nested-viewbox.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-nested-viewbox.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-no-stroke", + "source": "svg-vector-effect-no-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-no-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-nonuniform-transform", + "source": "svg-vector-effect-nonuniform-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-nonuniform-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-object-gradient", + "source": "svg-vector-effect-object-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-object-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-object-pattern", + "source": "svg-vector-effect-object-pattern.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-object-pattern.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-path-vocabulary", + "source": "svg-vector-effect-path-vocabulary.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-path-vocabulary.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-pattern-content", + "source": "svg-vector-effect-pattern-content.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-pattern-content.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-reflect-transform", + "source": "svg-vector-effect-reflect-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-reflect-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-applicability", + "source": "svg-vector-effect-root-applicability.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-root-applicability.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-viewbox-down", + "source": "svg-vector-effect-root-viewbox-down.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-root-viewbox-down.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-root-viewbox-up", + "source": "svg-vector-effect-root-viewbox-up.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-root-viewbox-up.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-rotate-transform", + "source": "svg-vector-effect-rotate-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-rotate-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-rounded-rect", + "source": "svg-vector-effect-rounded-rect.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-rounded-rect.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-shape-elements", + "source": "svg-vector-effect-shape-elements.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-shape-elements.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-skew-transform", + "source": "svg-vector-effect-skew-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-skew-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-token-grammar", + "source": "svg-vector-effect-token-grammar.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-token-grammar.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-use-element", + "source": "svg-vector-effect-use-element.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-use-element.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-use-target", + "source": "svg-vector-effect-use-target.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-use-target.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-user-gradient", + "source": "svg-vector-effect-user-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-user-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-user-pattern", + "source": "svg-vector-effect-user-pattern.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-user-pattern.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-vector-effect-zero-length-caps", + "source": "svg-vector-effect-zero-length-caps.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-vector-effect-zero-length-caps.png", + "width": 64, + "height": 64 + }, { "id": "svg-viewbox-only-sizing-rect", "source": "svg-viewbox-only-sizing-rect.svg", diff --git a/fixtures/web-first/svg-vector-effect-ancestor-transform.svg b/fixtures/web-first/svg-vector-effect-ancestor-transform.svg new file mode 100644 index 00000000..5ac1410a --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-ancestor-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-backend-determinant.svg b/fixtures/web-first/svg-vector-effect-backend-determinant.svg new file mode 100644 index 00000000..6ce8bdb3 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-backend-determinant.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-basic.svg b/fixtures/web-first/svg-vector-effect-basic.svg new file mode 100644 index 00000000..d98deb9b --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-basic.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-browser-dropped-grammar.svg b/fixtures/web-first/svg-vector-effect-browser-dropped-grammar.svg new file mode 100644 index 00000000..b504acbf --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-browser-dropped-grammar.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-circle.svg b/fixtures/web-first/svg-vector-effect-circle.svg new file mode 100644 index 00000000..2efc3691 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-circle.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-clip.svg b/fixtures/web-first/svg-vector-effect-clip.svg new file mode 100644 index 00000000..c884ac8f --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-computed-stroke-values.svg b/fixtures/web-first/svg-vector-effect-computed-stroke-values.svg new file mode 100644 index 00000000..3cd78570 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-computed-stroke-values.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-css-wide-inherit.svg b/fixtures/web-first/svg-vector-effect-css-wide-inherit.svg new file mode 100644 index 00000000..c6264958 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-css-wide-inherit.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-currentcolor.svg b/fixtures/web-first/svg-vector-effect-currentcolor.svg new file mode 100644 index 00000000..fec9f90b --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-currentcolor.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-dash-pathlength.svg b/fixtures/web-first/svg-vector-effect-dash-pathlength.svg new file mode 100644 index 00000000..df23179d --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-dash-pathlength.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-filter-opacity.svg b/fixtures/web-first/svg-vector-effect-filter-opacity.svg new file mode 100644 index 00000000..19d3fd6f --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-filter-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-group-applicability.svg b/fixtures/web-first/svg-vector-effect-group-applicability.svg new file mode 100644 index 00000000..05b56ec1 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-group-applicability.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-inert-function.svg b/fixtures/web-first/svg-vector-effect-inert-function.svg new file mode 100644 index 00000000..f02d8f0d --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-inert-function.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-large-quarter-turn.svg b/fixtures/web-first/svg-vector-effect-large-quarter-turn.svg new file mode 100644 index 00000000..a4f6a84c --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-large-quarter-turn.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-large-reflection.svg b/fixtures/web-first/svg-vector-effect-large-reflection.svg new file mode 100644 index 00000000..dc844104 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-large-reflection.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-large-viewbox-translation.svg b/fixtures/web-first/svg-vector-effect-large-viewbox-translation.svg new file mode 100644 index 00000000..fc1e952e --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-large-viewbox-translation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-link-applicability.svg b/fixtures/web-first/svg-vector-effect-link-applicability.svg new file mode 100644 index 00000000..52cff2d1 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-link-applicability.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-backend-determinant.svg b/fixtures/web-first/svg-vector-effect-marker-backend-determinant.svg new file mode 100644 index 00000000..bbc24ae9 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-backend-determinant.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-content.svg b/fixtures/web-first/svg-vector-effect-marker-content.svg new file mode 100644 index 00000000..df45bfc7 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-content.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-fixed-angle.svg b/fixtures/web-first/svg-vector-effect-marker-fixed-angle.svg new file mode 100644 index 00000000..88385934 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-fixed-angle.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-reflection.svg b/fixtures/web-first/svg-vector-effect-marker-reflection.svg new file mode 100644 index 00000000..60f1480b --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-reflection.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-rms-auto.svg b/fixtures/web-first/svg-vector-effect-marker-rms-auto.svg new file mode 100644 index 00000000..a44bc26c --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-rms-auto.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-stroke-width.svg b/fixtures/web-first/svg-vector-effect-marker-stroke-width.svg new file mode 100644 index 00000000..1ebad807 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-stroke-width.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-marker-user-space.svg b/fixtures/web-first/svg-vector-effect-marker-user-space.svg new file mode 100644 index 00000000..3633615f --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-marker-user-space.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-mask-source.svg b/fixtures/web-first/svg-vector-effect-mask-source.svg new file mode 100644 index 00000000..9bd813f8 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-mask-source.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-mask-target.svg b/fixtures/web-first/svg-vector-effect-mask-target.svg new file mode 100644 index 00000000..871aff29 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-mask-target.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-nested-viewbox.svg b/fixtures/web-first/svg-vector-effect-nested-viewbox.svg new file mode 100644 index 00000000..67a76856 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-nested-viewbox.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-no-stroke.svg b/fixtures/web-first/svg-vector-effect-no-stroke.svg new file mode 100644 index 00000000..71b9b139 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-no-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-nonuniform-transform.svg b/fixtures/web-first/svg-vector-effect-nonuniform-transform.svg new file mode 100644 index 00000000..3ce3c531 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-nonuniform-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-object-gradient.svg b/fixtures/web-first/svg-vector-effect-object-gradient.svg new file mode 100644 index 00000000..21a438f9 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-object-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-object-pattern.svg b/fixtures/web-first/svg-vector-effect-object-pattern.svg new file mode 100644 index 00000000..032ac96b --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-object-pattern.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-path-vocabulary.svg b/fixtures/web-first/svg-vector-effect-path-vocabulary.svg new file mode 100644 index 00000000..3f955871 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-path-vocabulary.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-pattern-content.svg b/fixtures/web-first/svg-vector-effect-pattern-content.svg new file mode 100644 index 00000000..78e54a7e --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-pattern-content.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-reflect-transform.svg b/fixtures/web-first/svg-vector-effect-reflect-transform.svg new file mode 100644 index 00000000..28b7be54 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-reflect-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-root-applicability.svg b/fixtures/web-first/svg-vector-effect-root-applicability.svg new file mode 100644 index 00000000..f240cd44 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-root-applicability.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-root-viewbox-down.svg b/fixtures/web-first/svg-vector-effect-root-viewbox-down.svg new file mode 100644 index 00000000..fa398006 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-root-viewbox-down.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-root-viewbox-up.svg b/fixtures/web-first/svg-vector-effect-root-viewbox-up.svg new file mode 100644 index 00000000..89677586 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-root-viewbox-up.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-rotate-transform.svg b/fixtures/web-first/svg-vector-effect-rotate-transform.svg new file mode 100644 index 00000000..db278cd4 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-rotate-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-rounded-rect.svg b/fixtures/web-first/svg-vector-effect-rounded-rect.svg new file mode 100644 index 00000000..26870484 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-rounded-rect.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-shape-elements.svg b/fixtures/web-first/svg-vector-effect-shape-elements.svg new file mode 100644 index 00000000..c0efa6f0 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-shape-elements.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-skew-transform.svg b/fixtures/web-first/svg-vector-effect-skew-transform.svg new file mode 100644 index 00000000..ff22fc4c --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-skew-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-token-grammar.svg b/fixtures/web-first/svg-vector-effect-token-grammar.svg new file mode 100644 index 00000000..c6913439 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-token-grammar.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-use-element.svg b/fixtures/web-first/svg-vector-effect-use-element.svg new file mode 100644 index 00000000..0c78b590 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-use-element.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-use-target.svg b/fixtures/web-first/svg-vector-effect-use-target.svg new file mode 100644 index 00000000..81a16576 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-use-target.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-user-gradient.svg b/fixtures/web-first/svg-vector-effect-user-gradient.svg new file mode 100644 index 00000000..9c38f4e8 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-user-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-user-pattern.svg b/fixtures/web-first/svg-vector-effect-user-pattern.svg new file mode 100644 index 00000000..72948597 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-user-pattern.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-vector-effect-zero-length-caps.svg b/fixtures/web-first/svg-vector-effect-zero-length-caps.svg new file mode 100644 index 00000000..ec4c7608 --- /dev/null +++ b/fixtures/web-first/svg-vector-effect-zero-length-caps.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index 9adbfcf7..83aad270 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -44,12 +44,17 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-stroke-dashoffset-percentage-precision-alias.svg` | Refuse valid percentages whose Chromium used phase cannot be recovered from the pinned Stylo computed f32. `57384.265625%` and `57384.267578125007%` collapse into the same computed bucket but Chromium's rasters differ by 120 pixels; their negative mirrors differ by 142. The last-finite/first-overflow pair exposes the same loss, and percentage-bearing math can lose its operation history before this compiler reads it. The stable refusal covers attribute, inline-style, stylesheet, and inherited spellings. Because this honored valid class has no independent checklist row, both `stroke-dashoffset` twins remain open under the gridaco/nothing#81 split precedent. | | `svg-stroke-dashoffset-sheet-unit.svg` | Refuse by name — the stylesheet spelling of a phase in a viewport-, container-, font-metric-, or root-font-metric-relative unit whose basis this build lacks. Those unit families retain their own checklist rows under the gridaco/nothing#80 guarded-gap precedent. | | `svg-stroke-dashoffset-var.svg` | Refuse by name — `var()` hides the phase's authored unit provenance. Chromium substitutes it in both presentation and CSS spellings; this patrol cannot follow that indirection, so even a benign fixed-length substitution over-refuses rather than risking a silent wrong phase. | -| `svg-stroke-vector-effect.svg` · `svg-stroke-paint-order.svg` | Refuse by name. Both were provably inert while strokes refused; consuming strokes made them load-bearing, which is exactly the trap the earlier patrol was kept for. | +| `svg-stroke-paint-order.svg` | Refuse by name. Paint ordering was provably inert while strokes refused; consuming strokes made it load-bearing, which is exactly the trap the earlier patrol was kept for. | | `svg-stroke-sheet-unit-width.svg` | Refuse by name — the third spelling of the basis-less unit. The attribute patrol walks ancestors, so it never saw a `