mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 19:56:51 -06:00
A linear gradient across a straight line runs along its length, on screen and in PDF alike
Review fix from aiongg (72d7ea2e, with the line tests of6db40bdb), ported to this branch: Fill::linear_axis gives screen and PDF one gradient axis, and a box with no extent along the gradient's angle takes its longer side. Co-authored-by: aiongg <aiongtaigi@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
aiongg
Claude Opus 5.5
parent
5f00aa7bb3
commit
f69601b0ff
@@ -32,6 +32,21 @@ impl Default for Fill {
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// A linear gradient's axis on an object whose bounds (in its own space) are `b`: the
|
||||
/// Gradient Swatch tool's vector, or the box's extent along the gradient's angle through its
|
||||
/// centre. A box with no extent along that angle (a straight line, across it) takes its longer
|
||||
/// side.
|
||||
pub fn linear_axis(&self, b: Rect) -> (Point, Point) {
|
||||
if let Some([x0, y0, x1, y1]) = self.gradient_vector {
|
||||
return (Point::new(x0, y0), Point::new(x1, y1));
|
||||
}
|
||||
let a = self.gradient_angle.unwrap_or(0.0).to_radians();
|
||||
let half = (b.width() * a.cos().abs() + b.height() * a.sin().abs()) / 2.0;
|
||||
let half = if half > 1e-9 { half } else { b.width().max(b.height()).max(1e-3) / 2.0 };
|
||||
let d = designcraft_geom::Vec2::new(a.cos(), -a.sin()) * half;
|
||||
(b.center() - d, b.center() + d)
|
||||
}
|
||||
|
||||
pub fn none() -> Self {
|
||||
Fill { swatch: designcraft_color::swatch::NONE.into(), tint: 1.0, gradient_angle: None, gradient_vector: None, overprint: false }
|
||||
}
|
||||
|
||||
@@ -1900,6 +1900,51 @@ mod gradient_stroke_tests {
|
||||
assert!(a > 215 && b < 40, "dragged: {a} {b}");
|
||||
assert!(s.execute("object.gradient", &json!({"ids": [id], "target": "gap"})).is_err());
|
||||
}
|
||||
|
||||
/// A 20 pt horizontal line with a gradient on its stroke, drawn on screen and from its PDF.
|
||||
fn line_with_stroke_gradient(g: serde_json::Value) -> (designcraft_render::Rendered, designcraft_render::Rendered) {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
let id = s.execute("line.create", &json!({"a": [20, 60], "b": [180, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [id], "weight": 20})).unwrap();
|
||||
let mut g = g;
|
||||
g["ids"] = json!([id]);
|
||||
g["target"] = json!("stroke");
|
||||
s.execute("object.gradient", &g).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
let screen = rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
let pdf = designcraft_pdf::export_pdf(&d, &s.cache, &Default::default()).unwrap();
|
||||
let mut p = crate::Session::new();
|
||||
p.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
p.execute("file.place", &json!({"base64": crate::cmd::file::base64_encode(&pdf), "name": "line.pdf", "x": 0, "y": 0, "width": 200})).unwrap();
|
||||
let pd = p.doc().unwrap().doc.clone();
|
||||
let placed = rr.render_page(&pd, &p.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
(screen, placed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_radial_gradient_on_a_straight_lines_stroke_shows_all_its_stops() {
|
||||
// The line's box has no height: the gradient is a circle across the line's length.
|
||||
let (screen, pdf) = line_with_stroke_gradient(
|
||||
json!({"kind": "radial", "stops": [{"location": 0, "color": "#000000"}, {"location": 100, "color": "#ffffff"}]}),
|
||||
);
|
||||
for (what, img) in [("screen", &screen), ("pdf", &pdf)] {
|
||||
let (a, m, b) = along(img);
|
||||
assert!(m < 40 && a > 180 && b > 180, "{what}: {a} {m} {b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linear_gradient_across_a_straight_line_paints_its_stroke_alike_on_screen_and_in_pdf() {
|
||||
let (screen, pdf) =
|
||||
line_with_stroke_gradient(json!({"angle": 90, "stops": [{"location": 0, "color": "#ff0000"}, {"location": 100, "color": "#0000ff"}]}));
|
||||
let (s, p) = (screen.pixel(100, 60), pdf.pixel(100, 60));
|
||||
// The line's box has no height: the gradient runs across the line's length, so the stroke
|
||||
// shows its middle.
|
||||
assert!(s[1] < 60 && s[0] > 80 && s[2] > 80, "screen: the stroke is painted, {s:?}");
|
||||
assert!((0..3).all(|i| s[i].abs_diff(p[i]) < 40), "screen {s:?}, pdf {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -978,7 +978,7 @@ impl Exporter<'_> {
|
||||
|
||||
/// A swatch fill (solid or gradient) in the item's inner space. `None` for [None]/unknown.
|
||||
fn fill_paint(&mut self, fill: &designcraft_doc::Fill, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
let (swatch, tint, angle) = (fill.swatch.as_str(), fill.tint, fill.gradient_angle);
|
||||
let (swatch, tint) = (fill.swatch.as_str(), fill.tint);
|
||||
if let Some(g) = designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch) {
|
||||
let kind = g.kind;
|
||||
let expanded = g.expanded_stops();
|
||||
@@ -1036,13 +1036,7 @@ impl Exporter<'_> {
|
||||
.into()
|
||||
}
|
||||
_ => {
|
||||
let a = angle.unwrap_or(0.0).to_radians();
|
||||
let half = ((bounds.width() * a.cos().abs() + bounds.height() * a.sin().abs()) / 2.0).max(1e-3);
|
||||
let d = Vec2::new(a.cos(), -a.sin()) * half;
|
||||
let (p0, p1) = match v {
|
||||
Some([x0, y0, x1, y1]) => (designcraft_geom::Point::new(x0, y0), designcraft_geom::Point::new(x1, y1)),
|
||||
None => (c - d, c + d),
|
||||
};
|
||||
let (p0, p1) = fill.linear_axis(bounds);
|
||||
LinearGradient {
|
||||
x1: p0.x as f32,
|
||||
y1: p0.y as f32,
|
||||
|
||||
@@ -889,7 +889,7 @@ pub fn color_of(c: &designcraft_color::Color, alpha: f32) -> peniko::Color {
|
||||
|
||||
/// Set a swatch fill (solid or gradient) as the paint. Returns false for [None]/unknown.
|
||||
fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_doc::Fill, bounds: Rect) -> bool {
|
||||
let (swatch, tint, angle) = (fill.swatch.as_str(), fill.tint, fill.gradient_angle);
|
||||
let (swatch, tint) = (fill.swatch.as_str(), fill.tint);
|
||||
if let Some(g) = designcraft_color::swatch::resolve_gradient(&doc.swatches, swatch) {
|
||||
let stops: Vec<peniko::ColorStop> = g.expanded_stops().iter().map(|(o, c, a)| peniko::ColorStop::from((*o, color_of(c, *a)))).collect();
|
||||
if stops.is_empty() {
|
||||
@@ -901,16 +901,13 @@ fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_do
|
||||
let r = Vec2::new(x1 - x0, y1 - y0).hypot().max(1e-3);
|
||||
peniko::Gradient::new_radial((x0, y0), r as f32).with_stops(stops.as_slice())
|
||||
}
|
||||
(_, Some([x0, y0, x1, y1])) => peniko::Gradient::new_linear((x0, y0), (x1, y1)).with_stops(stops.as_slice()),
|
||||
(designcraft_color::GradientKind::Radial, None) => {
|
||||
let r = bounds.width().max(bounds.height()) / 2.0;
|
||||
peniko::Gradient::new_radial(c, r as f32).with_stops(stops.as_slice())
|
||||
}
|
||||
_ => {
|
||||
let a = angle.unwrap_or(0.0).to_radians();
|
||||
let half = (bounds.width() * a.cos().abs() + bounds.height() * a.sin().abs()) / 2.0;
|
||||
let d = Vec2::new(a.cos(), -a.sin()) * half;
|
||||
peniko::Gradient::new_linear(c - d, c + d).with_stops(stops.as_slice())
|
||||
let (a, b) = fill.linear_axis(bounds);
|
||||
peniko::Gradient::new_linear(a, b).with_stops(stops.as_slice())
|
||||
}
|
||||
};
|
||||
ctx.set_paint(grad);
|
||||
|
||||
Reference in New Issue
Block a user