Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/rustmotion-core/src/engine/paint_pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1337,11 +1337,16 @@ fn gradient_stops(stops: &[crate::css::style::GradientStop]) -> (Vec<Color4f>, V
(colors, positions)
}

/// Gradient-line endpoints for a CSS `<angle>`: `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);
Expand Down
55 changes: 53 additions & 2 deletions crates/rustmotion-core/tests/audit_ws_a.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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:?}"
);
}
Loading