Gradients: read, write and draw a:tileRect
Gradient tile rectangles were dropped on import. PowerPoint's "from corner" radial presets depend on them: fillToRect puts the focus on a corner and tileRect stretches the gradient's box a whole size past it, so the circle is centred on that corner and reaches the far one. Without it (after the two-point radial change) such fills washed out. A linear gradient now spans its tile rectangle; a radial one ends on the circle around it, with the focus still placed in the shape's own box (fitted to PowerPoint's export of the corpus deck tdf114848: mean diff over the filled box 0.77). Round-trips through pptx. Wild synthetic set: tile-rect gradients 16.74 -> 2.48. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
38793f13c7
commit
a718a9c3da
|
|
@ -261,7 +261,7 @@ pub(crate) fn gradient_param(v: &Value) -> Option<Gradient> {
|
|||
scaled: false,
|
||||
},
|
||||
};
|
||||
Some(Gradient { stops, shape, rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true) })
|
||||
Some(Gradient { tile: [0.0; 4], stops, shape, rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true) })
|
||||
}
|
||||
|
||||
fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,15 @@ pub struct Gradient {
|
|||
pub shape: GradientShape,
|
||||
#[serde(default = "crate::yes")]
|
||||
pub rotate_with_shape: bool,
|
||||
/// `a:tileRect`: the box the gradient is laid out in, as insets from the shape's box
|
||||
/// (fractions; negative reaches outside it). PowerPoint's "from corner" radial presets use it
|
||||
/// to centre the gradient on the corner.
|
||||
#[serde(default, skip_serializing_if = "is_zero4")]
|
||||
pub tile: [f64; 4],
|
||||
}
|
||||
|
||||
fn is_zero4(v: &[f64; 4]) -> bool {
|
||||
v.iter().all(|x| *x == 0.0)
|
||||
}
|
||||
|
||||
/// How a picture fills its box: stretched into a (fractional) inset rect, or tiled.
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ fn a_gradient_without_stops_takes_them_from_its_style() {
|
|||
let p = Presentation::default();
|
||||
let s = &p.slides[0];
|
||||
let ctx = Ctx::for_slide(&p, s).unwrap();
|
||||
let own = style::Gradient { stops: vec![], shape: style::GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
let own =
|
||||
style::Gradient { tile: [0.0; 4], stops: vec![], shape: style::GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
let mut st = ShapeStyle::accent(SchemeSlot::Accent2);
|
||||
st.fill_ref.0 = 3;
|
||||
let sh = Shape { style: Some(st.clone()), fill: Some(Fill::Gradient(own.clone())), ..Default::default() };
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ fn ph() -> ColorRef {
|
|||
pub fn default_format() -> FormatScheme {
|
||||
let grad = |a: i32, b: i32| {
|
||||
Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: ph().with(ColorTransform::LumMod(a)).with(ColorTransform::SatMod(103000)) },
|
||||
GradientStop { pos: 1.0, color: ph().with(ColorTransform::LumMod(b)) },
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ pub fn gradient(f: &El) -> Gradient {
|
|||
} else {
|
||||
GradientShape::Linear { angle: 90.0, scaled: false }
|
||||
};
|
||||
Gradient { stops, shape, rotate_with_shape: f.bool("rotWithShape").unwrap_or(true) }
|
||||
let tile = f.child("tileRect").map(|r| [pct(r, "l"), pct(r, "t"), pct(r, "r"), pct(r, "b")].map(|v| v.unwrap_or(0.0).clamp(-10.0, 10.0)));
|
||||
Gradient { tile: tile.unwrap_or_default(), stops, shape, rotate_with_shape: f.bool("rotWithShape").unwrap_or(true) }
|
||||
}
|
||||
|
||||
/// `blipFill` (either `a:` or `p:`) → picture fill, registering the image as media.
|
||||
|
|
|
|||
|
|
@ -272,6 +272,10 @@ fn gradient(w: &mut W, g: &deckcraft_model::style::Gradient) {
|
|||
w.close("a:path");
|
||||
}
|
||||
}
|
||||
if g.tile.iter().any(|v| *v != 0.0) {
|
||||
let [l, t, r, b] = g.tile;
|
||||
w.empty("a:tileRect", A::new().a("l", pct(l)).a("t", pct(t)).a("r", pct(r)).a("b", pct(b)));
|
||||
}
|
||||
w.close("a:gradFill");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ fn a_gradient_without_stops_is_saved_without_stops() {
|
|||
let mut p = defaults::new_presentation(None);
|
||||
let id = ShapeId(p.alloc_id());
|
||||
let first = p.slides[0].id;
|
||||
let own = Gradient { stops: vec![], shape: GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
let own = Gradient { tile: [0.0; 4], stops: vec![], shape: GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
p.slide_mut(first).expect("slide").shapes.push(Shape {
|
||||
id,
|
||||
name: "Styled".into(),
|
||||
|
|
@ -112,6 +112,7 @@ fn a_fill_overlay_round_trips() {
|
|||
let overlay = FillOverlay {
|
||||
blend: "mult".into(),
|
||||
fill: Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent1) },
|
||||
GradientStop { pos: 1.0, color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(10, 20, 30)) },
|
||||
|
|
@ -133,3 +134,39 @@ fn a_fill_overlay_round_trips() {
|
|||
let s = q.slides[0].shapes.iter().find(|s| s.name == "Overlaid").expect("shape");
|
||||
assert_eq!(s.effects.as_ref().and_then(|e| e.fill_overlay.clone()), Some(overlay));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_tile_rect_is_read_and_round_trips() {
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
|
||||
// PowerPoint's "from top-left corner" radial: focus on the corner, tile reaching a box up and
|
||||
// to the left so it centres on that corner.
|
||||
let g = Gradient {
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 128, 255)) },
|
||||
GradientStop { pos: 1.0, color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::WHITE) },
|
||||
],
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.0, 0.0, 1.0, 1.0] },
|
||||
rotate_with_shape: true,
|
||||
tile: [-1.0, -1.0, 0.0, 0.0],
|
||||
};
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let id = ShapeId(p.alloc_id());
|
||||
let first = p.slides[0].id;
|
||||
p.slide_mut(first).expect("slide").shapes.push(Shape {
|
||||
id,
|
||||
name: "Corner".into(),
|
||||
xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)),
|
||||
fill: Some(Fill::Gradient(g.clone())),
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = deckcraft_pptx::export(&p).expect("export");
|
||||
let q = deckcraft_pptx::import(&bytes).expect("import");
|
||||
let s = q.slides[0].shapes.iter().find(|s| s.name == "Corner").expect("shape");
|
||||
assert_eq!(s.fill, Some(Fill::Gradient(g)));
|
||||
// As written by PowerPoint (attributes it leaves out are 0).
|
||||
let edited =
|
||||
edit_slides(&bytes, |x| x.replace("<a:tileRect l=\"-100000\" t=\"-100000\" r=\"0\" b=\"0\"/>", "<a:tileRect l=\"-100000\" t=\"-100000\"/>"));
|
||||
let q = deckcraft_pptx::import(&edited).expect("import");
|
||||
let s = q.slides[0].shapes.iter().find(|s| s.name == "Corner").expect("shape");
|
||||
assert!(matches!(&s.fill, Some(Fill::Gradient(g)) if g.tile == [-1.0, -1.0, 0.0, 0.0]), "{:?}", s.fill);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ pub fn rich_deck() -> Presentation {
|
|||
let mut a = shape(&mut p, "Gradient Star", 100.0, 120.0, 150.0, 150.0);
|
||||
a.geom = Geom::Preset { name: "star5".into(), adj: vec![20000.0] };
|
||||
a.fill = Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: ColorRef::scheme(SchemeSlot::Accent1) },
|
||||
GradientStop { pos: 1.0, color: ColorRef::scheme(SchemeSlot::Accent2).with(ColorTransform::LumMod(75000)) },
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ pub fn fill_path(
|
|||
if g.stops.is_empty() {
|
||||
return;
|
||||
}
|
||||
// A linear gradient spans its tile rectangle (the shape's box unless `tileRect` moves
|
||||
// its edges); a radial one ends on the circle around it, from a focus placed in the
|
||||
// shape's own box (PowerPoint's "from corner" presets centre the tile on the corner).
|
||||
let tile = tile_box(bounds, g.tile);
|
||||
let stops: Vec<(f64, Rgba)> = g.stops.iter().map(|s| (s.pos, shade(rctx.color(&s.color, ph), mode))).collect();
|
||||
ctx.set_paint(gradient(&g.shape, stops, bounds, alpha));
|
||||
|
||||
|
|
@ -55,12 +59,12 @@ pub fn fill_path(
|
|||
// Angle measured clockwise from left→right; span the bounds along that direction.
|
||||
let a = angle.to_radians();
|
||||
let (dx, dy) = (a.cos(), a.sin());
|
||||
let c = bounds.center();
|
||||
let half = (bounds.width() * dx.abs() + bounds.height() * dy.abs()) / 2.0;
|
||||
let c = tile.center();
|
||||
let half = (tile.width() * dx.abs() + tile.height() * dy.abs()) / 2.0;
|
||||
peniko::Gradient::new_linear(Point::new(c.x - dx * half, c.y - dy * half), Point::new(c.x + dx * half, c.y + dy * half))
|
||||
.with_stops(stops.as_slice())
|
||||
}
|
||||
GradientShape::Path { focus, .. } => circle_gradient(*focus, bounds).with_stops(stops.as_slice()),
|
||||
GradientShape::Path { focus, .. } => circle_gradient(*focus, bounds, tile).with_stops(stops.as_slice()),
|
||||
};
|
||||
ctx.set_paint(grad);
|
||||
ctx.fill_path(path);
|
||||
|
|
@ -88,6 +92,22 @@ pub fn resolved<'a>(rctx: &Ctx, pf: &'a PictureFill, ph: Option<Rgba>) -> std::b
|
|||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
|
||||
/// `bounds` with its edges moved in by `tile` (left, top, right, bottom insets as fractions of its
|
||||
/// size; negative moves them out), as `a:tileRect` and `a:fillRect` lay out a fill.
|
||||
pub fn tile_box(bounds: Rect, tile: [f64; 4]) -> Rect {
|
||||
if tile.iter().all(|v| *v == 0.0) || tile.iter().any(|v| !v.is_finite()) {
|
||||
return bounds;
|
||||
}
|
||||
let (w, h) = (bounds.width(), bounds.height());
|
||||
let [l, t, r, b] = tile;
|
||||
let x0 = bounds.x0 + l * w;
|
||||
let x1 = bounds.x1 - r * w;
|
||||
let y0 = bounds.y0 + t * h;
|
||||
let y1 = bounds.y1 - b * h;
|
||||
// An inverted tile (insets past each other) lays out nothing sensible: keep the box.
|
||||
if x1 - x0 <= 1e-9 || y1 - y0 <= 1e-9 { bounds } else { Rect::new(x0, y0, x1, y1) }
|
||||
}
|
||||
|
||||
/// The paint of a gradient of `shape` with `stops` (position, colour) spanning `bounds`, faded
|
||||
/// by `alpha`, ramping between stops as PowerPoint does (see `ramp`).
|
||||
pub fn gradient(shape: &GradientShape, mut stops: Vec<(f64, Rgba)>, bounds: Rect, alpha: f64) -> peniko::Gradient {
|
||||
|
|
@ -107,7 +127,7 @@ pub fn gradient(shape: &GradientShape, mut stops: Vec<(f64, Rgba)>, bounds: Rect
|
|||
peniko::Gradient::new_linear(Point::new(c.x - dx * half, c.y - dy * half), Point::new(c.x + dx * half, c.y + dy * half))
|
||||
.with_stops(stops.as_slice())
|
||||
}
|
||||
GradientShape::Path { focus, .. } => circle_gradient(*focus, bounds).with_stops(stops.as_slice()),
|
||||
GradientShape::Path { focus, .. } => circle_gradient(*focus, bounds, bounds).with_stops(stops.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,11 +137,14 @@ pub fn gradient(shape: &GradientShape, mut stops: Vec<(f64, Rgba)>, bounds: Rect
|
|||
/// centre, half its diagonal), with circles in between moving from one to the other. A focus off
|
||||
/// the centre so bunches the colours on its near side; a gradient "from a corner" reaches its
|
||||
/// last colour at that corner too, not only at the far one.
|
||||
pub fn circle_gradient(focus: [f64; 4], bounds: Rect) -> peniko::Gradient {
|
||||
///
|
||||
/// With a tile rectangle (`tile`, otherwise `bounds` itself) the end circle is the one around
|
||||
/// the tile, while the focus stays placed in `bounds`.
|
||||
pub fn circle_gradient(focus: [f64; 4], bounds: Rect, tile: Rect) -> peniko::Gradient {
|
||||
let fx = bounds.x0 + bounds.width() * (focus[0] + (1.0 - focus[2])) / 2.0;
|
||||
let fy = bounds.y0 + bounds.height() * (focus[1] + (1.0 - focus[3])) / 2.0;
|
||||
let c = bounds.center();
|
||||
let r = (bounds.width().hypot(bounds.height()) / 2.0).max(1e-6);
|
||||
let c = tile.center();
|
||||
let r = (tile.width().hypot(tile.height()) / 2.0).max(1e-6);
|
||||
let f = Point::new(fx, fy);
|
||||
if !(f.is_finite() && c.is_finite() && r.is_finite()) {
|
||||
return peniko::Gradient::new_radial(c, r as f32);
|
||||
|
|
|
|||
|
|
@ -565,6 +565,7 @@ fn a_linear_gradient_ramps_like_powerpoint() {
|
|||
id: ShapeId(700),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, 800.0, 100.0)),
|
||||
fill: Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![stop(0.0, deckcraft_color::Rgba::rgb(0, 0, 0)), stop(1.0, deckcraft_color::Rgba::rgb(255, 255, 255))],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
|
@ -589,6 +590,7 @@ fn a_gradient_with_a_stop_off_the_end_stays_linear() {
|
|||
id: ShapeId(701),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, 800.0, 100.0)),
|
||||
fill: Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![stop(0.2, deckcraft_color::Rgba::rgb(0, 0, 0)), stop(1.0, deckcraft_color::Rgba::rgb(255, 255, 255))],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
|
@ -629,6 +631,7 @@ fn a_shape_filled_with_the_background_shows_the_slide_background() {
|
|||
let stop = |pos, v| GradientStop { pos, color: ColorRef::rgb(deckcraft_color::Rgba::rgb(v, v, v)) };
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).background = Some(deckcraft_model::Background::Fill {
|
||||
fill: Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![stop(0.0, 0), stop(0.5, 0), stop(1.0, 200)],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
|
@ -702,6 +705,7 @@ fn a_text_gradient_starts_over_on_every_line() {
|
|||
let props = &mut p.runs[0].props;
|
||||
props.size = Some(80.0);
|
||||
props.fill = Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![stop(0.0, 0), stop(1.0, 255)],
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
|
@ -862,6 +866,7 @@ fn rect_path_gradients_draw_x_shaped_contours() {
|
|||
id: ShapeId(770),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, 280.0, 200.0)),
|
||||
fill: Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![stop(0.0, 0), stop(0.5, 128), stop(1.0, 255)],
|
||||
shape: GradientShape::Path { path: "rect".into(), focus: [0.5, 0.5, 0.5, 0.5] },
|
||||
rotate_with_shape: true,
|
||||
|
|
|
|||
|
|
@ -168,7 +168,12 @@ fn radial(focus: [f64; 4]) -> Shape {
|
|||
Shape {
|
||||
id: ShapeId(13),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)),
|
||||
fill: Some(Fill::Gradient(Gradient { stops, shape: GradientShape::Path { path: "circle".into(), focus }, rotate_with_shape: true })),
|
||||
fill: Some(Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops,
|
||||
shape: GradientShape::Path { path: "circle".into(), focus },
|
||||
rotate_with_shape: true,
|
||||
})),
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -292,7 +297,8 @@ fn a_pictures_border_lies_outside_the_picture() {
|
|||
fn children_filled_with_the_group_fill_show_one_fill_across_the_group() {
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
|
||||
let stops = vec![GradientStop { pos: 0.0, color: ColorRef::rgb(RED) }, GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::rgb(0, 0, 255)) }];
|
||||
let grad = Fill::Gradient(Gradient { stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true });
|
||||
let grad =
|
||||
Fill::Gradient(Gradient { tile: [0.0; 4], stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true });
|
||||
let child = |id: u32, x: f64| Shape {
|
||||
id: ShapeId(id),
|
||||
xfrm: Some(Xfrm::new(x, 0.0, 90.0, 100.0)),
|
||||
|
|
@ -331,3 +337,37 @@ fn open_freeforms_take_arrowheads_even_when_filled() {
|
|||
// (295.7, 187.3), 3.5 pt off the line's axis: outside the 4 pt line and outside the fill.
|
||||
assert!(close(img.pixel(294, 189), RED, 60), "arrowhead drawn {:?}", img.pixel(294, 189));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_corner_radial_centres_on_the_corner_through_its_tile() {
|
||||
// fillToRect on the top-left corner, tileRect reaching a whole box up and left: the circle is
|
||||
// centred on the corner and reaches the far corner (as PowerPoint's "from corner" presets).
|
||||
let mut sh = radial([0.0, 0.0, 1.0, 1.0]);
|
||||
if let Some(Fill::Gradient(g)) = &mut sh.fill {
|
||||
g.tile = [-1.0, -1.0, 0.0, 0.0];
|
||||
}
|
||||
let img = render(vec![sh]);
|
||||
assert!(img.pixel(102, 102)[0] > 240, "white at the corner {:?}", img.pixel(102, 102));
|
||||
let (top_right, bottom_left) = (img.pixel(498, 101)[0], img.pixel(101, 298)[0]);
|
||||
assert!(top_right < bottom_left, "farther is darker {top_right} {bottom_left}");
|
||||
assert!(bottom_left > 120, "the near edge stays light {bottom_left}");
|
||||
assert!(img.pixel(497, 297)[0] < 40, "dark at the far corner {:?}", img.pixel(497, 297));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linear_gradient_spans_its_tile_rect() {
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
|
||||
// Tile rect the left half of the box: the ramp ends half way and stays black after.
|
||||
let stops = vec![GradientStop { pos: 0.0, color: ColorRef::rgb(WHITE) }, GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::BLACK) }];
|
||||
let g = Gradient { stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true, tile: [0.0, 0.0, 0.5, 0.0] };
|
||||
let sh = Shape {
|
||||
id: ShapeId(20),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 100.0)),
|
||||
fill: Some(Fill::Gradient(g)),
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
};
|
||||
let img = render(vec![sh]);
|
||||
assert!(img.pixel(102, 150)[0] > 240);
|
||||
assert!(img.pixel(305, 150)[0] < 15 && img.pixel(495, 150)[0] < 15, "{:?}", img.pixel(305, 150));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub fn default_gradient(base: ColorRef, angle: f64) -> Gradient {
|
|||
let mut dark = base.clone();
|
||||
dark.mods.push(ColorTransform::LumMod(60_000));
|
||||
Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![GradientStop { pos: 0.0, color: base }, GradientStop { pos: 1.0, color: dark }],
|
||||
shape: GradientShape::Linear { angle, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
|
@ -136,6 +137,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) ->
|
|||
..default_gradient(base.clone(), 45.0)
|
||||
},
|
||||
Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![GradientStop { pos: 0.0, color: light }, GradientStop { pos: 1.0, color: dark }],
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.5, 0.5, 0.5, 0.5] },
|
||||
rotate_with_shape: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue