diff --git a/crates/rustmotion-core/src/engine/paint_pass.rs b/crates/rustmotion-core/src/engine/paint_pass.rs index 048b3c81..4b93fe75 100644 --- a/crates/rustmotion-core/src/engine/paint_pass.rs +++ b/crates/rustmotion-core/src/engine/paint_pass.rs @@ -1337,11 +1337,16 @@ fn gradient_stops(stops: &[crate::css::style::GradientStop]) -> (Vec, V (colors, positions) } +/// Gradient-line endpoints for a CSS ``: `0deg` points the line "to +/// top" (first stop at the bottom, travelling up to the last stop), and the +/// angle increases clockwise, so `90deg` is "to right" and `180deg` (the +/// default) is "to bottom" (first stop at the top). Skia's +/// `linear_gradient` places `colors[0]` at `p0`, so `p0` is always the end +/// the angle points *away from*. fn gradient_endpoints(bounds: Rect, angle_deg: f32) -> (Point, Point) { - // CSS angle: 0deg = bottom→top, increasing clockwise. let cx = bounds.left + bounds.width() / 2.0; let cy = bounds.top + bounds.height() / 2.0; - let rad = (angle_deg - 180.0).to_radians(); + let rad = angle_deg.to_radians(); let (sin_a, cos_a) = (rad.sin(), -rad.cos()); let len = (bounds.width().abs() * sin_a.abs() + bounds.height().abs() * cos_a.abs()) / 2.0; let p0 = Point::new(cx - sin_a * len, cy - cos_a * len); diff --git a/crates/rustmotion-core/tests/audit_ws_a.rs b/crates/rustmotion-core/tests/audit_ws_a.rs index d42a55bf..dbf4160d 100644 --- a/crates/rustmotion-core/tests/audit_ws_a.rs +++ b/crates/rustmotion-core/tests/audit_ws_a.rs @@ -2,8 +2,8 @@ //! tracked in issue #220. use rustmotion_core::css::style::{ - Background, BoxShadow, Color as CssColor, CssStyle, Display, FlexDirection, Position, - Size as CSize, + Background, BackgroundLayer, BoxShadow, Color as CssColor, CssStyle, Display, FlexDirection, + GradientStop, Position, Size as CSize, }; use rustmotion_core::css::taffy_bridge::ConversionContext; use rustmotion_core::css::units::{Length, LengthPercentage as CLP}; @@ -191,3 +191,54 @@ fn underdamped_spring_matches_the_analytic_closed_form() { ); } } + +// ---- linear-gradient(180deg, ...) must put the first stop at the top ---- + +fn gradient_card(w: f32, h: f32, angle: f32) -> BoxNode { + let css = CssStyle { + width: Some(CSize::Length(CLP::Px(w))), + height: Some(CSize::Length(CLP::Px(h))), + background: Some(Background::Single(BackgroundLayer::LinearGradient { + angle: Some(angle), + stops: vec![ + GradientStop { + color: CssColor::String("#ffffff".into()), + offset: Some(0.0), + }, + GradientStop { + color: CssColor::String("#000000".into()), + offset: Some(1.0), + }, + ], + })), + ..Default::default() + }; + BoxNode { + id: 0, + kind: BoxKind::Container, + css, + children: vec![], + intrinsic: None, + source_path: None, + window: None, + } +} + +#[test] +fn linear_gradient_180deg_puts_the_first_stop_at_the_top() { + // CSS: `angle: 180` ("to bottom") points the gradient line downward, so + // the first stop lands at the top and the last stop at the bottom. + let mut root = gradient_card(100.0, 100.0, 180.0); + let buf = render_pixels(&mut root, 100, 100); + + let top = probe(&buf, 100, 50, 2); + let bottom = probe(&buf, 100, 50, 97); + assert!( + top.0 > 200 && top.1 > 200 && top.2 > 200, + "angle: 180 must put the white first stop at the top, got {top:?}" + ); + assert!( + bottom.0 < 50 && bottom.1 < 50 && bottom.2 < 50, + "angle: 180 must put the black last stop at the bottom, got {bottom:?}" + ); +}