Refactor render for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 06:02:27 +00:00
parent 8e4d0e6b4c
commit 7f1ebc2a5c
9 changed files with 943 additions and 471 deletions
+593 -343
View File
File diff suppressed because it is too large Load Diff
+79 -23
View File
@@ -62,9 +62,23 @@ fn funnel(
ctx.set_paint(color(series_color(rctx, c, 0), 1.0));
ctx.fill_rect(&r);
if let Some(name) = c.categories.get(i) {
label(ctx, rctx, m, name, Rect::new(area.x0, y, plot.x0 - base * 0.4, y + band), base, Align::Right, Anchor::Middle, text_col, false);
label(
ctx,
rctx,
m,
name,
Rect::new(area.x0, y, plot.x0 - base * 0.4, y + band),
crate::chart::LabelStyle { size: base, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
);
}
label(ctx, rctx, m, &value_label(*v), Rect::new(cx - 60.0, y, cx + 60.0, y + band), base, Align::Center, Anchor::Middle, Rgba::WHITE, false);
label(
ctx,
rctx,
m,
&value_label(*v),
Rect::new(cx - 60.0, y, cx + 60.0, y + band),
crate::chart::LabelStyle { size: base, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
);
}
}
@@ -118,7 +132,20 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
}
let inner = t.inset(-base * 0.25);
if inner.width() > base && inner.height() > base * 0.8 {
label(ctx, rctx, m, &text, inner, base * 0.9, Align::Center, Anchor::Middle, if dark { Rgba::WHITE } else { text_col }, false);
label(
ctx,
rctx,
m,
&text,
inner,
crate::chart::LabelStyle {
size: base * 0.9,
align: Align::Center,
anchor: Anchor::Middle,
col: if dark { Rgba::WHITE } else { text_col },
bold: false,
},
);
}
}
}
@@ -136,9 +163,23 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
ctx.fill_rect(&Rect::new(x0, bar.y0, x1.min(bar.x1), bar.y1));
}
let ly = Rect::new(bar.x0 - base * 8.0, bar.y0 - base * 0.2, bar.x0 - base * 0.4, bar.y1 + base * 0.2);
label(ctx, rctx, m, &c.data_label(s, min), ly, base, Align::Right, Anchor::Middle, text_col, false);
label(
ctx,
rctx,
m,
&c.data_label(s, min),
ly,
crate::chart::LabelStyle { size: base, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
);
let ry = Rect::new(bar.x1 + base * 0.4, bar.y0 - base * 0.2, bar.x1 + base * 8.0, bar.y1 + base * 0.2);
label(ctx, rctx, m, &c.data_label(s, max), ry, base, Align::Left, Anchor::Middle, text_col, false);
label(
ctx,
rctx,
m,
&c.data_label(s, max),
ry,
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Middle, col: text_col, bold: false },
);
}
let note = Rect::new(area.x0, area.y1 - note_h, area.x1, area.y1);
label(
@@ -147,11 +188,13 @@ fn region_tiles(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
m,
"Map preview unavailable: regions shown as tiles",
note,
base * 0.85,
Align::Center,
Anchor::Middle,
mix(text_col, Rgba::WHITE, 0.4),
false,
crate::chart::LabelStyle {
size: base * 0.85,
align: Align::Center,
anchor: Anchor::Middle,
col: mix(text_col, Rgba::WHITE, 0.4),
bold: false,
},
);
}
@@ -248,7 +291,14 @@ fn treemap(
}
let inner = t.inset(-base * 0.3);
if inner.width() > base && inner.height() > base {
label(ctx, rctx, m, &text, inner, base, Align::Left, Anchor::Top, Rgba::WHITE, false);
label(
ctx,
rctx,
m,
&text,
inner,
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Top, col: Rgba::WHITE, bold: false },
);
}
}
}
@@ -289,11 +339,7 @@ fn sunburst(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
m,
name,
Rect::new(lp.x - 50.0, lp.y - base, lp.x + 50.0, lp.y + base),
base,
Align::Center,
Anchor::Middle,
Rgba::WHITE,
false,
crate::chart::LabelStyle { size: base, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
);
}
a0 = a1;
@@ -344,7 +390,14 @@ fn tree_treemap(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
}
let inner = t.inset(-base * 0.3);
if inner.width() > base && inner.height() > base {
label(ctx, rctx, m, &text, inner, base * 0.9, Align::Left, Anchor::Top, Rgba::WHITE, false);
label(
ctx,
rctx,
m,
&text,
inner,
crate::chart::LabelStyle { size: base * 0.9, align: Align::Left, anchor: Anchor::Top, col: Rgba::WHITE, bold: false },
);
}
continue;
}
@@ -355,7 +408,14 @@ fn tree_treemap(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area:
ctx.fill_rect(&t);
if t.width() > base * 2.0 && banner > base * 0.8 {
let head = Rect::new(t.x0 + base * 0.3, t.y0, t.x1 - base * 0.3, t.y0 + banner);
label(ctx, rctx, m, &n.name, head, base, Align::Left, Anchor::Middle, Rgba::WHITE, true);
label(
ctx,
rctx,
m,
&n.name,
head,
crate::chart::LabelStyle { size: base, align: Align::Left, anchor: Anchor::Middle, col: Rgba::WHITE, bold: true },
);
}
let inner = Rect::new(t.x0 + 1.0, t.y0 + banner, t.x1 - 1.0, t.y1 - 1.0);
let k = lighten(fill, 0.12 * (depth + 1) as f64);
@@ -437,11 +497,7 @@ fn tree_sunburst(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area
m,
&n.name,
Rect::new(lp.x - half, lp.y - base, lp.x + half, lp.y + base),
base * 0.9,
Align::Center,
Anchor::Middle,
Rgba::WHITE,
false,
crate::chart::LabelStyle { size: base * 0.9, align: Align::Center, anchor: Anchor::Middle, col: Rgba::WHITE, bold: false },
);
}
if !n.children.is_empty() {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{DateTime, Fields, Rgba, ShapeId, Vec2, datetime};
/// Animation state of one shape during a slide show or preview.
#[derive(Clone, Debug)]
+10 -11
View File
@@ -67,7 +67,14 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
Align::Right => q.x - w + base * 0.3,
_ => q.x - w / 2.0,
};
label(ctx, rctx, m, name, Rect::new(x0, q.y - base * 0.7, x0 + w, q.y + base * 0.7), base, align, Anchor::Middle, text_col, false);
label(
ctx,
rctx,
m,
name,
Rect::new(x0, q.y - base * 0.7, x0 + w, q.y + base * 0.7),
crate::chart::LabelStyle { size: base, align, anchor: Anchor::Middle, col: text_col, bold: false },
);
}
}
// Value labels up the first spoke.
@@ -83,11 +90,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
m,
&t,
Rect::new(q.x - w - base * 0.3, q.y - base * 0.6, q.x - base * 0.3, q.y + base * 0.6),
base * 0.9,
Align::Right,
Anchor::Middle,
text_col,
false,
crate::chart::LabelStyle { size: base * 0.9, align: Align::Right, anchor: Anchor::Middle, col: text_col, bold: false },
);
}
}
@@ -130,11 +133,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, area: Rec
m,
&t,
Rect::new(q.x - 40.0, q.y - base * 1.4, q.x + 40.0, q.y - base * 0.2),
base * 0.9,
Align::Center,
Anchor::Bottom,
text_col,
false,
crate::chart::LabelStyle { size: base * 0.9, align: Align::Center, anchor: Anchor::Bottom, col: text_col, bold: false },
);
}
}
+242 -76
View File
@@ -60,6 +60,50 @@ pub(crate) struct Frame<'a> {
pub(crate) fields: SlideFields,
}
/// A shape being drawn: its resolved geometry, fill, line and effects, in shape space `m` at size
/// `w` x `h`, shared by the stages of `Renderer::geometry_shape`.
struct ShapeDraw<'a, 'ctx> {
f: &'a Frame<'a>,
rctx: &'a Ctx<'ctx>,
s: &'a Shape,
st: &'a ShapeState,
m: Affine,
w: f64,
h: f64,
depth: usize,
geo: Geometry,
fades: Vec<deckcraft_geom::morph::Fade>,
fill: Option<Fill>,
fill_ph: Option<Rgba>,
line: Line,
line_ph: Option<Rgba>,
effects: Option<Effects>,
fx_ph: Option<Rgba>,
outline: BezPath,
is_pic: bool,
empty_ph: bool,
}
impl ShapeDraw<'_, '_> {
/// How much of the fill and stroke of path `i` shows (morphing outlines fade).
fn fade(&self, i: usize) -> deckcraft_geom::morph::Fade {
self.fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 })
}
}
/// What casts a shape's shadow, glow and reflection: its geometry, fill and outline, in shape
/// space `m` at size `w` x `h`.
struct FxSource<'a> {
geo: &'a Geometry,
fill: &'a Option<Fill>,
fill_alpha: Option<f64>,
outline: Option<Caster<'a>>,
content: Option<&'a deckcraft_model::style::PictureFill>,
m: Affine,
w: f64,
h: f64,
}
impl Renderer {
fn canvas(pres: &Presentation, opts: &RenderOpts, filters: bool) -> (RenderContext, u16, u16, Affine) {
let (w, h) = output_size(pres, opts);
@@ -541,7 +585,6 @@ impl Renderer {
Some(pm) => self.morphs.geometry(pm, s, w, h),
None => (self.scaled_geometry(s, m, depth, w, h), Vec::new()),
};
let fade = |i: usize| fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 });
let (fill, fill_ph) = resolve::fill(rctx, s);
let (line, line_ph) = resolve::line(rctx, s);
let (effects, fx_ph) = resolve::effects(rctx, s);
@@ -552,6 +595,56 @@ impl Renderer {
// Placeholders in the editor: dashed outline and prompt, nothing else when empty.
let empty_ph = s.ph.is_some() && s.text.as_ref().is_none_or(TextBody::is_empty) && !is_pic && !matches!(s.kind, ShapeKind::Media(_));
let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill };
let d = ShapeDraw { f, rctx, s, st, m, w, h, depth, geo, fades, fill, fill_ph, line, line_ph, effects, fx_ph, outline, is_pic, empty_ph };
self.cast_effects(ctx, &d);
let soft = d.effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
if let Some(r) = soft {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, None, None, None, None);
let _ = r;
}
let pt = self.point_scale(&d);
self.draw_extrusion(ctx, &d, pt);
self.draw_contour(ctx, &d, pt);
self.draw_fill(ctx, &d);
self.draw_fill_overlay(ctx, &d);
self.draw_bevel(ctx, &d, pt);
self.draw_outline(ctx, &d);
if let Some(r) = soft {
self.draw_feather(ctx, &d, r);
}
self.draw_inner_shadow(ctx, &d);
self.draw_text_body(ctx, &d);
}
/// Shape-space units per point for the 3-D sizes, measured without the camera's turn (which
/// `shape_at` puts in `m`): its foreshortening is not a child-coordinate scale.
fn point_scale(&self, d: &ShapeDraw) -> f64 {
let turn = d
.effects
.as_ref()
.and_then(|e| e.raw3d.as_deref())
.and_then(camera::from_raw)
.and_then(|c| camera::affine(&c, d.w, d.h))
.filter(|t| t.determinant().abs() > 1e-9);
self.per_point(turn.map_or(d.m, |t| d.m * t.inverse()), d.depth)
}
fn cast_effects(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let depth = d.depth;
let fill_ph = d.fill_ph;
let line_ph = d.line_ph;
let fx_ph = d.fx_ph;
let is_pic = d.is_pic;
let s = d.s;
let rctx = d.rctx;
let geo = &d.geo;
let fill = &d.fill;
let effects = &d.effects;
let line = &d.line;
// Effects below the shape.
if let Some(e) = &effects {
// A picture (or picture fill) casts its shadow from its pixels, not its frame.
@@ -560,7 +653,7 @@ impl Renderer {
_ => None,
};
// What casts the shadow: the fill as see-through as it is, and the outline.
let fill_alpha = fill_opacity(rctx, &fill, fill_ph);
let fill_alpha = fill_opacity(rctx, fill, fill_ph);
let outline = line.fill.as_ref().filter(|f| !f.is_none()).map(|lf| {
let a = match lf {
Fill::Solid { color: c } => rctx.color(c, line_ph).a as f64 / 255.0,
@@ -568,28 +661,22 @@ impl Renderer {
};
let (base, rel) = self.line_space(m, depth);
let lw = line.width.unwrap_or(0.75).max(0.0);
let outside = if border_outside(is_pic, effects.as_ref()) { outside_line(&line, lw) } else { None };
Caster { line: outside.map_or(std::borrow::Cow::Borrowed(&line), std::borrow::Cow::Owned), ph: line_ph, alpha: a, base, rel }
let outside = if border_outside(is_pic, effects.as_ref()) { outside_line(line, lw) } else { None };
Caster { line: outside.map_or(std::borrow::Cow::Borrowed(line), std::borrow::Cow::Owned), ph: line_ph, alpha: a, base, rel }
});
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, fill_alpha, outline, content.as_ref(), m, w, h);
self.effects_below(ctx, rctx, e, fx_ph, FxSource { geo, fill, fill_alpha, outline, content: content.as_ref(), m, w, h });
}
let soft = effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
if let Some(r) = soft {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, None, None, None, None);
let _ = r;
}
// Shape-space units per point for the 3-D sizes, measured without the camera's turn (which
// `shape_at` puts in `m`): its foreshortening is not a child-coordinate scale.
let pt = {
let turn = effects
.as_ref()
.and_then(|e| e.raw3d.as_deref())
.and_then(camera::from_raw)
.and_then(|c| camera::affine(&c, w, h))
.filter(|t| t.determinant().abs() > 1e-9);
self.per_point(turn.map_or(m, |t| m * t.inverse()), depth)
};
}
fn draw_extrusion(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
let m = d.m;
let w = d.w;
let h = d.h;
let fill_ph = d.fill_ph;
let rctx = d.rctx;
let fill = &d.fill;
let effects = &d.effects;
let outline = &d.outline;
// A 3-D extrusion under a turned camera: the sides, swept behind the face.
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
&& let Some(ex) = extrude::from_raw(raw)
@@ -604,8 +691,17 @@ impl Renderer {
let pre = m * turn.inverse();
let lit = light::Lighting::from_raw(raw);
let d = ex.depth * pt;
extrude::draw(ctx, &outline, w, h, &cam, d, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
extrude::draw(ctx, outline, w, h, &cam, d, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
}
}
fn draw_contour(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
let m = d.m;
let fill_ph = d.fill_ph;
let rctx = d.rctx;
let fill = &d.fill;
let effects = &d.effects;
let outline = &d.outline;
// A 3-D contour: a band around the outline, behind the face (and in front of any sides).
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
&& let Some(c) = extrude::contour_from_raw(raw)
@@ -615,8 +711,23 @@ impl Renderer {
_ => None,
};
let col = c.color.as_ref().map(|k| rctx.color(k, face)).or(face).unwrap_or(Rgba::rgb(0xff, 0xff, 0xff));
extrude::draw_contour(ctx, &outline, m, c.width * pt, col, &light::Lighting::from_raw(raw));
extrude::draw_contour(ctx, outline, m, c.width * pt, col, &light::Lighting::from_raw(raw));
}
}
fn draw_fill(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let fill_ph = d.fill_ph;
let empty_ph = d.empty_ph;
let s = d.s;
let f = d.f;
let rctx = d.rctx;
let geo = &d.geo;
let fill = &d.fill;
let outline = &d.outline;
let fade = |i: usize| d.fade(i);
// Fill.
match &s.kind {
ShapeKind::Picture { fill: pf } => {
@@ -625,11 +736,11 @@ impl Renderer {
if s.fill.as_ref().is_some_and(|f| !f.is_none())
&& let Some(fl) = &fill
{
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
}
ctx.set_transform(m);
let bounds = Rect::new(0.0, 0.0, w, h);
ctx.push_clip_layer(&outline);
ctx.push_clip_layer(outline);
let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t);
ctx.pop_layer();
@@ -642,7 +753,7 @@ impl Renderer {
if s.fill.as_ref().is_some_and(|f| !f.is_none())
&& let Some(fl) = &fill
{
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
}
ctx.set_transform(m);
let pf = deckcraft_model::style::PictureFill { media: *media, ..Default::default() };
@@ -653,7 +764,7 @@ impl Renderer {
let bounds = Rect::new(0.0, 0.0, w, h);
if let Some(p) = mc.poster {
let pf = deckcraft_model::style::PictureFill { media: p, crop: mc.crop, ..Default::default() };
ctx.push_clip_layer(&outline);
ctx.push_clip_layer(outline);
paint::picture(ctx, f.pres, &pf, bounds, m, None);
ctx.pop_layer();
} else if mc.video {
@@ -668,10 +779,23 @@ impl Renderer {
if let Some(fl) = &fill
&& !(empty_ph && f.opts.edit && s.fill.is_none() && s.line.is_none() && s.style.is_none())
{
self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade);
self.fill_geometry(ctx, f, rctx, fl, fill_ph, geo, (w, h), m, &fade);
}
}
}
}
fn draw_fill_overlay(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let fx_ph = d.fx_ph;
let f = d.f;
let rctx = d.rctx;
let geo = &d.geo;
let fill = &d.fill;
let effects = &d.effects;
let fade = |i: usize| d.fade(i);
// `a:fillOverlay`: a fill blended over the shape's fill (PowerPoint's "Flow" themes multiply
// a gradient over a tiled picture), inside the filled outline, under the line.
if let Some(ov) = effects.as_ref().and_then(|e| e.fill_overlay.as_ref())
@@ -691,13 +815,19 @@ impl Renderer {
ctx.pop_layer();
}
}
// Some presets' outlines default to sharp corners (PowerPoint's own per-shape default).
let line = match (&s.geom, line.join) {
(Geom::Preset { name, .. }, None) if preset::mitred_by_default(name) => {
Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..line }
}
_ => line,
};
}
fn draw_bevel(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, pt: f64) {
let m = d.m;
let w = d.w;
let h = d.h;
let fill_ph = d.fill_ph;
let is_pic = d.is_pic;
let rctx = d.rctx;
let geo = &d.geo;
let fill = &d.fill;
let effects = &d.effects;
let outline = &d.outline;
// A 3-D bevel: the face and a band inside the outline lit as surfaces.
if let Some(bv) = effects.as_ref().and_then(|e| e.raw3d.as_deref()).and_then(bevel::from_raw) {
let bv = bevel::Bevel { w: bv.w * pt, h: bv.h * pt, ..bv };
@@ -717,6 +847,29 @@ impl Renderer {
bevel::draw(ctx, &bv, if is_pic { None } else { solid }, &area, Rect::new(0.0, 0.0, w, h), m, 1.0);
}
}
}
fn draw_outline(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let depth = d.depth;
let line_ph = d.line_ph;
let is_pic = d.is_pic;
let s = d.s;
let rctx = d.rctx;
let geo = &d.geo;
let effects = &d.effects;
let outline = &d.outline;
let line = d.line.clone();
let fade = |i: usize| d.fade(i);
// Some presets' outlines default to sharp corners (PowerPoint's own per-shape default).
let line = match (&s.geom, line.join) {
(Geom::Preset { name, .. }, None) if preset::mitred_by_default(name) => {
Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..line }
}
_ => line,
};
// Outline.
if line.fill.as_ref().is_some_and(|f| !f.is_none()) {
let lw = line.width.unwrap_or(0.75).max(0.0);
@@ -779,7 +932,7 @@ impl Renderer {
stroke(ctx, rctx, &line, line_ph, &path, Some(&whole), base, lw, fade(i).stroke);
}
if ends {
arrowheads(ctx, rctx, &line, line_ph, &geo, rel, base, lw);
arrowheads(ctx, rctx, &line, line_ph, geo, rel, base, lw);
}
if clipped {
ctx.pop_layer();
@@ -788,33 +941,46 @@ impl Renderer {
ctx.pop_layer();
}
}
if let Some(r) = soft {
// Feather: the shape (fill and line) fades to nothing at its outline. Measured on
// PowerPoint's export: the mask is the outline shrunk by 0.85 r, blurred by a third of
// r, so it's half opaque 0.85 r inside the edge and transparent at the edge itself.
ctx.set_transform(m);
ctx.push_layer(
None,
Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)),
None,
None,
Some(blur(r / 3.0 * scale_of(m))),
);
ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 255));
ctx.fill_path(&outline);
ctx.push_blend_layer(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut));
ctx.set_stroke(kurbo::Stroke::new(r * 1.7).with_join(kurbo::Join::Round));
ctx.stroke_path(&outline);
ctx.pop_layer();
ctx.pop_layer();
ctx.pop_layer();
}
}
fn draw_feather(&mut self, ctx: &mut RenderContext, d: &ShapeDraw, r: f64) {
let m = d.m;
let outline = &d.outline;
// Feather: the shape (fill and line) fades to nothing at its outline. Measured on
// PowerPoint's export: the mask is the outline shrunk by 0.85 r, blurred by a third of
// r, so it's half opaque 0.85 r inside the edge and transparent at the edge itself.
ctx.set_transform(m);
ctx.push_layer(
None,
Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)),
None,
None,
Some(blur(r / 3.0 * scale_of(m))),
);
ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 255));
ctx.fill_path(outline);
ctx.push_blend_layer(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut));
ctx.set_stroke(kurbo::Stroke::new(r * 1.7).with_join(kurbo::Join::Round));
ctx.stroke_path(outline);
ctx.pop_layer();
ctx.pop_layer();
ctx.pop_layer();
}
fn draw_inner_shadow(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let fx_ph = d.fx_ph;
let rctx = d.rctx;
let effects = &d.effects;
let outline = &d.outline;
// Inner shadow.
if let Some(e) = &effects
&& let Some(sh) = &e.inner_shadow
{
ctx.set_transform(m);
ctx.push_clip_layer(&outline);
ctx.push_clip_layer(outline);
let c = rctx.color(&sh.color, fx_ph);
let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist;
// As PowerPoint draws it (measured on its export): the shadow falls inside along the
@@ -832,6 +998,19 @@ impl Renderer {
ctx.pop_layer();
ctx.pop_layer();
}
}
fn draw_text_body(&mut self, ctx: &mut RenderContext, d: &ShapeDraw) {
let m = d.m;
let w = d.w;
let h = d.h;
let depth = d.depth;
let empty_ph = d.empty_ph;
let s = d.s;
let f = d.f;
let rctx = d.rctx;
let st = d.st;
let geo = &d.geo;
// Text (or the placeholder prompt in the editor).
let skip_text = f.opts.skip_text == Some(s.id);
if let Some(body) = &s.text
@@ -842,7 +1021,7 @@ impl Renderer {
let (tw, th, m) = self.text_space(m, depth, w, h);
let text_geo;
let geo = if (tw, th) == (w, h) {
&geo
geo
} else {
text_geo = shape_geometry(s, tw, th);
&text_geo
@@ -878,7 +1057,7 @@ impl Renderer {
let prompt_body = prompt_body(rctx, s, kind);
if !prompt_body.is_empty() {
let grey = Rgba::rgb(0x59, 0x59, 0x59);
draw_text(ctx, rctx, s, &prompt_body, tr, m, &f.fields, Some(grey));
draw_text(ctx, rctx, s, &prompt_body, m, &Opts { rect: tr, fields: &f.fields, prompt_color: Some(grey), no_shrink: false });
}
}
}
@@ -895,21 +1074,8 @@ impl Renderer {
}
}
fn effects_below(
&mut self,
ctx: &mut RenderContext,
rctx: &Ctx,
e: &Effects,
ph: Option<Rgba>,
geo: &Geometry,
fill: &Option<Fill>,
fill_alpha: Option<f64>,
outline: Option<Caster>,
content: Option<&deckcraft_model::style::PictureFill>,
m: Affine,
w: f64,
h: f64,
) {
fn effects_below(&mut self, ctx: &mut RenderContext, rctx: &Ctx, e: &Effects, ph: Option<Rgba>, src: FxSource) {
let FxSource { geo, fill, fill_alpha, outline, content, m, w, h } = src;
let silhouette = {
let mut p = BezPath::new();
for sp in &geo.paths {
+2 -3
View File
@@ -1,7 +1,7 @@
use super::opts::{RenderOpts, SlideFields};
use super::raster::{Image, MAX_SIDE};
use super::renderer::{Frame, RENDERER, Renderer};
use super::*;
use super::{Affine, Ctx, PathMorph, Presentation, Rect, RenderContext, Shape, ShapeId, Slide, TextMorph, resolve};
static PANICS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
@@ -125,8 +125,7 @@ pub fn render_slide_part(pres: &Presentation, index: usize, part: SlidePart, opt
/// to `shapes` only.
pub fn render_blend(
pres: &Presentation,
old: usize,
new: usize,
(old, new): (usize, usize),
mix: f64,
shapes: &[(Shape, bool)],
text: &[TextMorph],
+8 -8
View File
@@ -616,14 +616,14 @@ fn blend_ends_match_the_slides_and_mixes_backdrops() {
let o = RenderOpts { scale: 0.5, ..Default::default() };
let shapes = vec![(sh, false)];
// At the ends the frame is the slide itself; the shape stays opaque throughout.
assert_eq!(render_blend(&p, 0, 1, 1.0, &shapes, &[], &[], &o).pixels, render_slide(&p, 1, &o).pixels);
let mid = render_blend(&p, 0, 1, 0.5, &shapes, &[], &[], &o);
assert_eq!(render_blend(&p, (0, 1), 1.0, &shapes, &[], &[], &o).pixels, render_slide(&p, 1, &o).pixels);
let mid = render_blend(&p, (0, 1), 0.5, &shapes, &[], &[], &o);
let px = mid.pixel(5, 5);
assert!((px[0] as i32 - 128).abs() <= 3, "{px:?}");
assert_eq!(mid.pixel(100, 75), render_slide(&p, 1, &o).pixel(100, 75));
// Hostile input: bad indices and NaN never panic.
assert_eq!(render_blend(&p, 0, 9, 0.5, &shapes, &[], &[], &o).width, 0);
let _ = render_blend(&p, 0, 1, f64::NAN, &shapes, &[], &[], &o);
assert_eq!(render_blend(&p, (0, 9), 0.5, &shapes, &[], &[], &o).width, 0);
let _ = render_blend(&p, (0, 1), f64::NAN, &shapes, &[], &[], &o);
}
fn text_shape(id: u32, text: &str, x: f64, y: f64) -> Shape {
@@ -668,7 +668,7 @@ fn morph_text_ends_match_the_slides() {
sb.xfrm = Some(x);
sa.xfrm = Some(x);
let tm = TextMorph { old: ShapeId(11), new: ShapeId(10), from, to, chars: false, t };
render_blend(&p, 0, 1, t, &[(sb, false), (sa, true)], &[tm], &[], &o)
render_blend(&p, (0, 1), t, &[(sb, false), (sa, true)], &[tm], &[], &o)
};
// Warm up: faces load lazily (a background scan runs), so the first render may still fall back.
let _ = (render_slide(&p, 0, &o), render_slide(&p, 1, &o), frame(0.5));
@@ -781,7 +781,7 @@ fn morph_path_ends_match_the_slides() {
let lerp = |a: f64, b: f64| a + (b - a) * t;
let x = Xfrm::new(lerp(from.x, to.x), lerp(from.y, to.y), lerp(from.w, to.w), lerp(from.h, to.h));
let pm = PathMorph { id: ShapeId(10), from: Geom::preset("ellipse"), from_box: from, to_box: to, t };
render_blend(&p, 0, 1, t, &[(form(10, "star5", x), false)], &[], &[pm], &o)
render_blend(&p, (0, 1), t, &[(form(10, "star5", x), false)], &[], &[pm], &o)
};
// The morph splits the outlines into more curves, which flatten slightly differently: edge pixels
// may differ a little, a different shape differs a lot.
@@ -792,7 +792,7 @@ fn morph_path_ends_match_the_slides() {
let d1 = differ(&frame(1.0), &render_slide(&p, 1, &o));
assert!(d1 < 40, "end differs {d1}");
// Without the morph the start would show the star.
let star = render_blend(&p, 0, 1, 0.0, &[(form(10, "star5", from), false)], &[], &[], &o);
let star = render_blend(&p, (0, 1), 0.0, &[(form(10, "star5", from), false)], &[], &[], &o);
assert!(differ(&star, &render_slide(&p, 0, &o)) > 400);
// Halfway the shape is filled around the middle of its box.
let mid = frame(0.5);
@@ -818,7 +818,7 @@ fn morph_path_fades_a_fill_only_one_side_has() {
let at = |t: f64| {
let pm = PathMorph { id: ShapeId(10), from: Geom::preset("rect"), from_box: bx, to_box: bx, t };
// A point near the line but not on it: inside the shape all the way.
render_blend(&p, 0, 1, t, &[(form(10, "line"), false)], &[], &[pm], &o).pixel(110, 90)
render_blend(&p, (0, 1), t, &[(form(10, "line"), false)], &[], &[pm], &o).pixel(110, 90)
};
let (full, half, none) = (at(0.0), at(0.5), at(1.0));
assert_eq!(none, [255, 255, 255, 255]);
+7 -5
View File
@@ -1,6 +1,8 @@
use super::geometry::{blur, color, pre_translate, scale_of};
use super::opts::ParaState;
use super::*;
use super::{
Affine, Arc, BezPath, Ctx, FontDb, HashMap, Opts, PhType, Rect, RenderContext, Rgba, Shape, TextBody, Vec2, colr, paint, peniko, resolve,
};
/// The prompt shown in an empty placeholder, formatted like its content would be.
pub(crate) fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody {
@@ -53,10 +55,10 @@ pub(crate) fn glyph(face: &deckcraft_fonts::FontFace, gid: u32) -> Arc<BezPath>
p
}
/// Draw a text body into shape space `m` within the text rect.
pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, tr: Rect, m: Affine, fields: &dyn Fields, prompt: Option<Rgba>) {
let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false });
draw_layout(ctx, &l, tr, m);
/// Draw a text body into shape space `m` within the text rect `opts.rect`.
pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, m: Affine, opts: &Opts) {
let l = deckcraft_text::layout(rctx, s, body, opts);
draw_layout(ctx, &l, opts.rect, m);
}
/// Add transient list markers to an editor layout, using the document's own bullet glyphs.
+1 -1
View File
@@ -87,7 +87,7 @@ pub fn render_transition(pres: &Presentation, index: usize, t: f64, opts: &Rende
.iter()
.map(|x| crate::PathMorph { id: x.id, from: x.from.clone(), from_box: x.from_box, to_box: x.to_box, t: m.mix })
.collect();
return render_blend(pres, from, index, m.mix, &m.shapes, &text, &paths, &o);
return render_blend(pres, (from, index), m.mix, &m.shapes, &text, &paths, &o);
}
}
let new = render_slide(pres, index, opts);