mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Render: an unfilled shape's glow follows its line
A glow always filled the shape's whole silhouette, so a frame with no fill (a rounded-rectangle border with a soft glow, common in corporate decks) came out as a solid tinted panel over the slide. PowerPoint glows such a shape along its outline only and leaves the inside clear; a filled shape's glow is unchanged. Checked against PowerPoint's own PDF export of 31 real decks (597 slides): the affected slides' mean pixel difference drops from ~29 to ~4 (of 255).
This commit is contained in:
@@ -668,7 +668,9 @@ impl Renderer {
|
||||
let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill };
|
||||
// Effects below the shape.
|
||||
if let Some(e) = &effects {
|
||||
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, m, w, h);
|
||||
// A shape without a fill (an outline, a frame) glows along its line only.
|
||||
let filled = is_pic || fill.as_ref().is_some_and(|f| !matches!(f, Fill::None));
|
||||
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, filled, m, w, h);
|
||||
}
|
||||
let soft = effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
|
||||
if let Some(r) = soft {
|
||||
@@ -801,6 +803,7 @@ impl Renderer {
|
||||
ph: Option<Rgba>,
|
||||
geo: &Geometry,
|
||||
fill: &Option<Fill>,
|
||||
filled: bool,
|
||||
m: Affine,
|
||||
w: f64,
|
||||
h: f64,
|
||||
@@ -837,9 +840,15 @@ impl Renderer {
|
||||
ctx.push_layer(None, None, Some((c.a as f32) / 255.0), None, Some(blur(g.radius * 0.35 * k)));
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(c.with_alpha(255), 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 1.2).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(&silhouette);
|
||||
ctx.fill_path(&silhouette);
|
||||
if filled {
|
||||
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 1.2).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(&silhouette);
|
||||
ctx.fill_path(&silhouette);
|
||||
} else {
|
||||
// The glow spreads its radius to both sides of the line; the inside stays clear.
|
||||
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 2.0).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(&silhouette);
|
||||
}
|
||||
ctx.pop_layer();
|
||||
}
|
||||
if let Some(rf) = &e.reflection {
|
||||
|
||||
@@ -124,6 +124,34 @@ fn effects_tables_charts_render() {
|
||||
assert!(px[0] < 250, "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_of_an_unfilled_shape_follows_its_line() {
|
||||
// A frame (no fill, thin line) with a strong glow: the glow lines its edge, the inside stays
|
||||
// the slide's white, as PowerPoint draws it.
|
||||
let glow = |fill: Option<Fill>| {
|
||||
let mut sh = Shape {
|
||||
id: ShapeId(610),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 300.0)),
|
||||
fill,
|
||||
line: Some(deckcraft_model::style::Line::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 176, 240)), 0.25)),
|
||||
..Default::default()
|
||||
};
|
||||
sh.effects = Some(deckcraft_model::Effects {
|
||||
glow: Some(deckcraft_model::style::Glow { color: ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 0, 255)), radius: 8.0 }),
|
||||
..Default::default()
|
||||
});
|
||||
render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() })
|
||||
};
|
||||
let img = glow(Some(Fill::None));
|
||||
let inside = img.pixel(300, 250);
|
||||
assert!(inside[0] > 245 && inside[1] > 245, "{inside:?}");
|
||||
let edge = img.pixel(300, 97);
|
||||
assert!(edge[0] < 230, "{edge:?}");
|
||||
// A filled shape's glow is still the whole silhouette's (the fill covers its inside).
|
||||
let img = glow(Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 255, 255)))));
|
||||
assert!(img.pixel(300, 94)[0] < 240);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_prompts_only_in_edit_view() {
|
||||
let p = Presentation::default();
|
||||
|
||||
Reference in New Issue
Block a user