mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
refactor(render): move fx.rs items to fx/{bounds,entries,pixels,predicates,raster,renderer}.rs
This commit is contained in:
+17
-2396
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,374 @@
|
||||
use super::predicates::{has_fx, has_object_fx};
|
||||
use super::raster::doc_raster_ppi;
|
||||
use super::*;
|
||||
|
||||
fn node_bezpath(n: &Node) -> Option<BezPath> {
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, .. } => Some(path.to_bezpath()),
|
||||
NodeKind::Compound { children, .. } => {
|
||||
let mut bp = BezPath::new();
|
||||
for c in children {
|
||||
if let Some(p) = c.path_data() {
|
||||
bp.extend(p.to_bezpath());
|
||||
}
|
||||
}
|
||||
Some(bp)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply(effects: &[Effect], bp: &BezPath, ctx: &GeomContext) -> BezPath {
|
||||
if !effects::has_geometry(effects) || bp.elements().is_empty() {
|
||||
return bp.clone();
|
||||
}
|
||||
effects::apply_geometry_bez(effects, bp, bp.bounding_box(), ctx)
|
||||
}
|
||||
|
||||
/// The object-level effected geometry of `n` (base path `bp`).
|
||||
pub(crate) fn effected_path(n: &Node, bp: &BezPath) -> BezPath {
|
||||
apply(&n.appearance.effects, bp, &GeomContext::of(n))
|
||||
}
|
||||
|
||||
pub(super) fn item_effects(item: &AppearanceItem) -> &[Effect] {
|
||||
match item {
|
||||
AppearanceItem::Fill(f) => &f.effects,
|
||||
AppearanceItem::Stroke(s) => &s.effects,
|
||||
}
|
||||
}
|
||||
|
||||
/// Visual bounds including geometry effects, stroke outsets and shadows/glows (symbol instances
|
||||
/// measured by their art from `symbols`).
|
||||
pub(crate) fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
if let Some(art) = effects::revolve_art(n).or_else(|| effects::solid_art(n)) {
|
||||
return cull_bounds(symbols, &art).map(|b| {
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
b.inflate(o, o)
|
||||
});
|
||||
}
|
||||
let bp = node_bezpath(n)?;
|
||||
let ctx = GeomContext::of(n);
|
||||
let g = effected_path(n, &bp);
|
||||
let mut r: Option<Rect> = (!g.elements().is_empty()).then(|| g.bounding_box());
|
||||
for item in &n.appearance.items {
|
||||
let fx = item_effects(item);
|
||||
if effects::has_geometry(fx) {
|
||||
let ig = apply(fx, &g, &ctx.item(item));
|
||||
if !ig.elements().is_empty() {
|
||||
let b = ig.bounding_box();
|
||||
r = Some(r.map_or(b, |r| r.union(b)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let s = n.appearance.outset();
|
||||
r.map(|r| {
|
||||
let r = r.inflate(s, s);
|
||||
// The fills' and strokes' own raster effects, then the object's.
|
||||
let r = n.appearance.items.iter().filter(|i| paints(i)).map(|i| spread(symbols, item_effects(i), r)).fold(r, |acc, b| acc.union(b));
|
||||
let o = outset(symbols, &n.appearance.effects, r);
|
||||
r.inflate(o, o)
|
||||
})
|
||||
}
|
||||
|
||||
/// Does `item` paint (as [`Renderer::paint_items`] draws it)?
|
||||
fn paints(item: &AppearanceItem) -> bool {
|
||||
item.visible() && !item.paint().is_none()
|
||||
}
|
||||
|
||||
/// The bounds of what `n` draws, its own and its members' geometry effects applied and the raster
|
||||
/// effects of its members and of its fills and strokes included (each fill's and stroke's
|
||||
/// spreading what it paints, [`item_bounds`]), with strokes measured as
|
||||
/// [`Node::visual_bounds_with`] measures them, without the reach of `n`'s own raster effects
|
||||
/// (symbol instances measured by their art from `symbols`): what Rasterize makes its image over.
|
||||
pub(crate) fn drawn_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
let b = art_bounds(symbols, n)?;
|
||||
let items = n.appearance.items.iter().filter(|i| paints(i) && !effects::raster_effects(item_effects(i)).is_empty());
|
||||
Some(items.map(|i| spread(symbols, item_effects(i), item_bounds(n, i).unwrap_or(b))).fold(b, |acc, r| acc.union(r)))
|
||||
}
|
||||
|
||||
/// The bounds of what `item`, a fill or stroke of the path or compound path `n`, paints: the path
|
||||
/// with `n`'s and `item`'s geometry effects, and for a stroke its width. `None` for other objects,
|
||||
/// 3D art, a brushed stroke and an item that draws no path.
|
||||
fn item_bounds(n: &Node, item: &AppearanceItem) -> Option<Rect> {
|
||||
let stroke = matches!(item, AppearanceItem::Stroke(_));
|
||||
if !has_fx(n) || effects::has_revolve(n) || effects::has_solid(n) || (stroke && vectorcraft_brush::has_brush(n)) {
|
||||
return None;
|
||||
}
|
||||
let g = effected_path(n, &node_bezpath(n)?);
|
||||
let fx = item_effects(item);
|
||||
if effects::has_geometry(fx) {
|
||||
let ig = apply(fx, &g, &GeomContext::of(n).item(item));
|
||||
let o = if let AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 };
|
||||
return (!ig.elements().is_empty()).then(|| ig.bounding_box().inflate(o, o));
|
||||
}
|
||||
let one = vectorcraft_doc::Appearance { items: vec![item.clone()], ..Default::default() };
|
||||
(!g.elements().is_empty()).then(|| one.stroked_bounds(g.bounding_box(), || g.clone()))
|
||||
}
|
||||
|
||||
/// `b` grown by how far raster effects `fx` paint past it, each spreading what the ones before it
|
||||
/// paint (Rasterize at the document's raster effects resolution where it takes it).
|
||||
fn spread(symbols: &SymbolBoxes, fx: &[Effect], b: Rect) -> Rect {
|
||||
let ppi = doc_raster_ppi(symbols.document());
|
||||
effects::raster_effects(fx).iter().fold(b, |r, e| e.at_document_ppi(ppi).spread(r, 72.0 / ppi))
|
||||
}
|
||||
|
||||
/// [`drawn_bounds`] before the raster effects of `n`'s fills and strokes: its art, and the art of
|
||||
/// its brushed strokes (a symbol's brushed art as far as [`crate::brush_fx::pad`] reaches).
|
||||
fn art_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
let b = shape_bounds(symbols, n);
|
||||
let brushed = vectorcraft_brush::has_brush(n)
|
||||
.then(|| vectorcraft_brush::expand(symbols.document(), n))
|
||||
.flatten()
|
||||
.and_then(|art| art.visual_bounds_with(symbols));
|
||||
let b = vectorcraft_geom::union_opt(b, brushed)?;
|
||||
let pad = if matches!(n.kind, NodeKind::SymbolInstance { .. }) { crate::brush_fx::pad(symbols, n) } else { 0.0 };
|
||||
Some(b.inflate(pad, pad))
|
||||
}
|
||||
|
||||
/// [`art_bounds`] without brush art.
|
||||
fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
// The visible members of `g` (a Shaper group's first member is its source art, which paints
|
||||
// nothing).
|
||||
let union = |g: &Node| {
|
||||
let members = g.children()?.get(usize::from(g.extras().shaper.is_some())..).unwrap_or_default();
|
||||
members.iter().filter(|c| c.visible).fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c)))
|
||||
};
|
||||
match &n.kind {
|
||||
_ if effects::has_container_appearance(n) => {
|
||||
let art = effects::evaluate_container(Some(symbols.document()), n);
|
||||
let art = art.as_ref().unwrap_or(n);
|
||||
if art.clips() { art.visual_bounds_with(symbols) } else { union(art) }
|
||||
}
|
||||
NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. } => union(n),
|
||||
_ if has_fx(n) => {
|
||||
// A 3D effect's art.
|
||||
if let Some(art) = effects::revolve_art(n).or_else(|| effects::solid_art(n)) {
|
||||
return drawn_bounds(symbols, &art);
|
||||
}
|
||||
let g = effected_path(n, &node_bezpath(n)?);
|
||||
// The path as the fills and strokes without geometry effects of their own paint it
|
||||
// (as every fill and stroke would when none paints).
|
||||
let mut plain = n.appearance.clone();
|
||||
plain.items.retain(|i| !(paints(i) && effects::has_geometry(item_effects(i))));
|
||||
let on_path = !n.appearance.items.iter().any(paints) || plain.items.iter().any(paints);
|
||||
let mut r = (on_path && !g.elements().is_empty()).then(|| plain.stroked_bounds(g.bounding_box(), || g.clone()));
|
||||
let ctx = GeomContext::of(n);
|
||||
for item in n.appearance.items.iter().filter(|i| paints(i) && effects::has_geometry(item_effects(i))) {
|
||||
let ig = apply(item_effects(item), &g, &ctx.item(item));
|
||||
let o = if let AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 };
|
||||
r = vectorcraft_geom::union_opt(r, (!ig.elements().is_empty()).then(|| ig.bounding_box().inflate(o, o)));
|
||||
}
|
||||
r
|
||||
}
|
||||
// As [`cull_bounds_in`] measures them, without the object's own raster effects: the
|
||||
// reshaped art (type's outlines, and an image and the paths of its own fills and strokes,
|
||||
// measured one by one with the effects of those fills and strokes), grown by how far the
|
||||
// effects in a symbol's art and a live object's content paint past it.
|
||||
_ if has_object_fx(n) => {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_deref());
|
||||
let b = match &reshaped {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => {
|
||||
r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c)))
|
||||
}
|
||||
Some(r) => r.visual_bounds_with(symbols),
|
||||
None => None,
|
||||
}
|
||||
.or_else(|| n.visual_bounds_with(symbols))?;
|
||||
let a = match &reshaped {
|
||||
Some(r) => reshaped_art_effects_reach(symbols, n, r),
|
||||
None => art_effects_reach(symbols, n),
|
||||
};
|
||||
let a = a.max(live_content_reach(symbols, n));
|
||||
Some(b.inflate(a, a))
|
||||
}
|
||||
_ => {
|
||||
let b = n.visual_bounds_with(symbols)?;
|
||||
let a = art_effects_reach(symbols, n).max(live_content_reach(symbols, n));
|
||||
Some(b.inflate(a, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`drawn_bounds`] of `n` grown by how far its own raster effects paint past them. With Crop
|
||||
/// Marks, the [`drawn_bounds`] of its art: the marks and the object, which paints its own raster
|
||||
/// effects inside the art.
|
||||
pub(crate) fn painted_reach(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
if let Some(art) = effects::crop_marks_art(n) {
|
||||
return drawn_bounds(symbols, &art);
|
||||
}
|
||||
drawn_bounds(symbols, n).map(|b| spread(symbols, &n.appearance.effects, b))
|
||||
}
|
||||
|
||||
/// The outline the renderer clips `n`'s Feather, Inner Glow and Rasterize with Create Clipping
|
||||
/// Mask to: a path's or compound path's geometry with its object-level geometry effects applied,
|
||||
/// the outline of the art [`Renderer::fx_art`] makes for an object with object effects, and with
|
||||
/// Crop Marks the object's own. For other objects it is [`effects::clip_outline`].
|
||||
pub(crate) fn drawn_outline(doc: &Document, n: &Node) -> Option<(BezPath, FillRule)> {
|
||||
if let Some(art) = effects::crop_marks_art(n) {
|
||||
return drawn_outline(doc, art.children()?.first()?);
|
||||
}
|
||||
if has_object_fx(n) {
|
||||
let art = Renderer::new().fx_art(doc, &Arc::new(n.clone()), false);
|
||||
return effects::clip_outline(&art);
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. } if effects::has_geometry(&n.appearance.effects) => {
|
||||
Some((effected_path(n, &node_bezpath(n)?), *rule))
|
||||
}
|
||||
_ => effects::clip_outline(n),
|
||||
}
|
||||
}
|
||||
|
||||
/// How far raster effects `fx` paint beyond `b`, each spreading what the ones before it paint
|
||||
/// ([`spread`]).
|
||||
fn outset(symbols: &SymbolBoxes, fx: &[Effect], b: Rect) -> f64 {
|
||||
let s = spread(symbols, fx, b);
|
||||
[b.x0 - s.x0, b.y0 - s.y0, s.x1 - b.x1, s.y1 - b.y1].into_iter().fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Bounds used for culling: like `Node::visual_bounds_with`, but aware of live effects anywhere in
|
||||
/// the subtree (a shadow can reach the view while its object is outside it). Symbol instances are
|
||||
/// measured by their art from `symbols`.
|
||||
pub(crate) fn cull_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
cull_bounds_in(symbols, n, true)
|
||||
}
|
||||
|
||||
/// [`cull_bounds`] of `n`. With `after_reshape` true, the effects in the art of a symbol instance
|
||||
/// with geometry effects of its own are measured on the art as those effects reshape it
|
||||
/// ([`reshaped_art_effects_reach`]). With `after_reshape` false, they are measured on the art
|
||||
/// before those effects run ([`art_effects_reach`]), which `symbols` keeps for each symbol and
|
||||
/// linear part.
|
||||
fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Option<Rect> {
|
||||
let inner = |c: &Node| cull_bounds_in(symbols, c, after_reshape);
|
||||
match &n.kind {
|
||||
// The object and its crop marks.
|
||||
_ if effects::has_crop_marks(n) => effects::crop_marks_art(n).and_then(|art| inner(&art)),
|
||||
// The evaluated art (its fills, strokes and geometry effects) and the raster effects' reach.
|
||||
_ if effects::has_container_appearance(n) => {
|
||||
let art = effects::evaluate_container(Some(symbols.document()), n);
|
||||
let art = art.as_ref().unwrap_or(n);
|
||||
let b = if art.clips() {
|
||||
art.visual_bounds_with(symbols)
|
||||
} else {
|
||||
art.children()?.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c)))
|
||||
}?;
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
Some(b.inflate(o, o))
|
||||
}
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => {
|
||||
children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c)))
|
||||
}
|
||||
_ if has_fx(n) => visual_bounds(symbols, n),
|
||||
// The reshaped art (a symbol instance's reshaped art is made from its symbol's art; type's
|
||||
// outlines, and an image and the paths of its own fills and strokes, measured one by one
|
||||
// with the geometry effects of those fills and strokes), grown by how far the effects in a
|
||||
// symbol instance's art paint past that art.
|
||||
_ if has_object_fx(n) => {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_deref());
|
||||
let b = match &reshaped {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => {
|
||||
r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c)))
|
||||
}
|
||||
Some(r) => r.visual_bounds_with(symbols),
|
||||
None => None,
|
||||
}
|
||||
.or_else(|| n.visual_bounds_with(symbols))?;
|
||||
let a = match &reshaped {
|
||||
Some(r) if after_reshape => reshaped_art_effects_reach(symbols, n, r),
|
||||
_ => art_effects_reach(symbols, n),
|
||||
};
|
||||
let a = a.max(live_content_reach(symbols, n));
|
||||
let b = b.inflate(a, a);
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
Some(b.inflate(o, o))
|
||||
}
|
||||
NodeKind::SymbolInstance { .. } => {
|
||||
let b = n.visual_bounds_with(symbols)?;
|
||||
let a = art_effects_reach(symbols, n);
|
||||
Some(b.inflate(a, a))
|
||||
}
|
||||
_ => {
|
||||
let b = n.visual_bounds_with(symbols)?;
|
||||
let b = match &n.kind {
|
||||
NodeKind::Text(t) => vectorcraft_geom::union_opt(Some(b), glyphs_box(t, n.appearance.outset())).unwrap_or(b),
|
||||
_ => b,
|
||||
};
|
||||
let a = live_content_reach(symbols, n);
|
||||
Some(b.inflate(a, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The box of type `t`'s glyphs as laid out (its layout bounds, else laid out now), grown by its
|
||||
/// characters' strokes, miter spikes included, and by `outset`: on a path they stand above it and
|
||||
/// run past its ends, in an area they can hang past its frame, where its own box
|
||||
/// ([`Node::visual_bounds_with`]) doesn't.
|
||||
fn glyphs_box(t: &vectorcraft_doc::TextObject, outset: f64) -> Option<Rect> {
|
||||
let b = t.cached_bounds.unwrap_or_else(|| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).bounds);
|
||||
let o = outset.max(t.char_stroke_reach());
|
||||
(b.width() > 0.0 || b.height() > 0.0).then(|| t.xf.transform_rect_bbox(b).inflate(o, o))
|
||||
}
|
||||
|
||||
/// How far the content of live object `n` (a blend, envelope or repeat) can paint past its visual
|
||||
/// bounds: for each object in it, how far the effects in a symbol instance's art paint past that
|
||||
/// art ([`art_effects_reach`]) and how far brush art can paint ([`crate::brush_fx::pad`]), on the
|
||||
/// farthest side. 0 for any other kind of object.
|
||||
fn live_content_reach(symbols: &SymbolBoxes, n: &Node) -> f64 {
|
||||
if !vectorcraft_doc::live::is_live(n) {
|
||||
return 0.0;
|
||||
}
|
||||
let mut reach = 0.0f64;
|
||||
n.walk(&mut |c| reach = reach.max(art_effects_reach(symbols, c) + crate::brush_fx::pad(symbols, c)));
|
||||
reach
|
||||
}
|
||||
|
||||
/// How far the effects in the art of symbol instance `n` (and in the art of the symbols the
|
||||
/// instances in it show) paint past that art's visual bounds, on its farthest side, measured on the
|
||||
/// art as the instance places it before its effects run: 0 when the art has no visible effects, and
|
||||
/// for any other kind of object. The reach is the same wherever the art is moved to, so the art is
|
||||
/// placed by the linear part of the instance's transform alone, and `symbols` keeps the reach for
|
||||
/// each symbol and linear part, up to a limit ([`SymbolBoxes::effects_reach`]).
|
||||
fn art_effects_reach(symbols: &SymbolBoxes, n: &Node) -> f64 {
|
||||
let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return 0.0 };
|
||||
if !symbols.has_effects(symbol) {
|
||||
return 0.0;
|
||||
}
|
||||
let measure = |[m0, m1, m2, m3]: [f64; 4]| {
|
||||
let past = |art: &Arc<Node>| {
|
||||
let mut placed = (**art).clone();
|
||||
placed.transform(Affine::new([m0, m1, m2, m3, 0.0, 0.0]), false);
|
||||
match (cull_bounds(symbols, &placed), placed.visual_bounds_with(symbols)) {
|
||||
(Some(c), Some(v)) => [v.x0 - c.x0, v.y0 - c.y0, c.x1 - v.x1, c.y1 - v.y1].into_iter().fold(0.0, f64::max),
|
||||
_ => 0.0,
|
||||
}
|
||||
};
|
||||
symbols.measure_art(symbol, past).unwrap_or(0.0)
|
||||
};
|
||||
let [m0, m1, m2, m3, _, _] = xf.as_coeffs();
|
||||
symbols.effects_reach(symbol, [m0, m1, m2, m3], &measure)
|
||||
}
|
||||
|
||||
/// How far the effects in the art of symbol instance `n` paint past `r`, that art as `n`'s own
|
||||
/// geometry effects reshape it ([`effects::reshape`]), on its farthest side: 0 when the art has no
|
||||
/// visible effects, and for any other kind of object. The art in `r` is measured without the raster
|
||||
/// effects of `n` that `r` carries, by [`cull_bounds_in`] with `after_reshape` false: the effects in
|
||||
/// the art of an instance in it are measured before that instance's own geometry effects run.
|
||||
fn reshaped_art_effects_reach(symbols: &SymbolBoxes, n: &Node, r: &Node) -> f64 {
|
||||
let NodeKind::SymbolInstance { symbol, .. } = &n.kind else { return 0.0 };
|
||||
if !symbols.has_effects(symbol) {
|
||||
return 0.0;
|
||||
}
|
||||
let art = r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, cull_bounds_in(symbols, c, false)));
|
||||
match (art, r.visual_bounds_with(symbols)) {
|
||||
(Some(c), Some(v)) => [v.x0 - c.x0, v.y0 - c.y0, c.x1 - v.x1, c.y1 - v.y1].into_iter().fold(0.0, f64::max),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
use super::raster::ViewCrop;
|
||||
use super::*;
|
||||
|
||||
/// What [`Renderer::raster_fx`] paints effects around.
|
||||
pub(super) struct Content<'a> {
|
||||
/// The object the effects belong to (cached shadows are checked against it).
|
||||
pub(super) node: &'a Node,
|
||||
/// The `Arc` holding `node`, when known: cached rasters keep it and match it by identity.
|
||||
pub(super) owner: Option<&'a Arc<Node>>,
|
||||
/// First shadow-cache slot of these effects (`None`: don't cache).
|
||||
pub(super) cache: Option<usize>,
|
||||
/// Document-space bounds of the painted content.
|
||||
pub(super) reach: Rect,
|
||||
/// Its silhouette: Feather and Inner Glow clip to it (without one, Feather only blurs and
|
||||
/// Inner Glow is skipped).
|
||||
pub(super) outline: Option<(&'a BezPath, FillRule)>,
|
||||
/// What Rasterize makes its image over: the object's [`drawn_bounds`] as far as the effects
|
||||
/// before it spread them (`None`: `reach`).
|
||||
pub(super) bounds: Option<Rect>,
|
||||
/// Where the image of the Rasterize first in these effects is kept for the effects after a
|
||||
/// split ([`Renderer::raster_fx`]), which draw this content more than once.
|
||||
pub(super) kept: Option<&'a Kept>,
|
||||
}
|
||||
|
||||
/// The image of the last Rasterize before a split in an effect stack ([`Renderer::raster_fx`]). It
|
||||
/// is made the first time the effects after the split draw the content and drawn from here the
|
||||
/// other times, so the content before the split is drawn once.
|
||||
pub(super) struct Kept {
|
||||
/// Where the effects after the split take the content from: the visible area grown by how
|
||||
/// far they paint.
|
||||
pub(super) window: Rect,
|
||||
/// The view and pixel size of the split's frame, when the image is over
|
||||
/// [`Renderer::rasterize_image`]'s limits: the content is then drawn in that frame.
|
||||
pub(super) frame: Option<(Affine, f64)>,
|
||||
pub(super) image: std::cell::RefCell<Option<KeptImage>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum KeptImage {
|
||||
/// The image on its own grid, as [`Renderer::rasterize_image`] makes it, and how many images
|
||||
/// in the content failed to decode while it was made ([`crate::paint::decode_failures`]):
|
||||
/// each draw of the image from here counts them again ([`crate::paint::add_decode_failures`]).
|
||||
Grid(std::rc::Rc<(Vec<u8>, usize, usize)>, u64),
|
||||
/// An image over [`Renderer::rasterize_image`]'s limits: the content's raster with the
|
||||
/// Rasterize applied, as the first draw made it, and the transform from its pixels to
|
||||
/// document space.
|
||||
Raster(Arc<vello_cpu::Pixmap>, Affine),
|
||||
}
|
||||
|
||||
/// A cached shadow / glow raster: premultiplied, already tinted and faded, positioned relative to
|
||||
/// the view-space position of `anchor` (a document point).
|
||||
pub(crate) struct ShadowEntry {
|
||||
pub(super) node: Drawn,
|
||||
pub(super) linear: [u64; 4],
|
||||
pub(super) anchor: vectorcraft_geom::Point,
|
||||
/// Pixel offset of the raster's top-left from the anchor's view position.
|
||||
pub(super) rel: (f64, f64),
|
||||
pub(super) image: std::sync::Arc<vello_cpu::Pixmap>,
|
||||
/// View pixels per raster pixel: more than 1 for a glow made coarser to fit the budget.
|
||||
pub(super) scale: f64,
|
||||
/// `None` for a raster of the whole silhouette's reach: it draws after a pan too. For one
|
||||
/// cropped to the view, or made over such a raster, the view and the rect it was made for: it
|
||||
/// draws only there ([`PixelEntry::view`]).
|
||||
pub(super) view: Option<ViewCrop>,
|
||||
pub(super) made: Made,
|
||||
/// What the silhouette read when the raster was made.
|
||||
pub(super) inputs: Inputs,
|
||||
/// Made for the object before it moved ([`Renderer::move_shadow`]), or made over rasters
|
||||
/// made before their objects moved: only a frame rendered while interacting draws it.
|
||||
pub(super) moved: bool,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
|
||||
/// A shadow or glow raster ready to draw ([`Renderer::draw_shadow`]).
|
||||
pub(super) struct Shadow {
|
||||
pub(super) image: std::sync::Arc<vello_cpu::Pixmap>,
|
||||
/// Raster to view pixels.
|
||||
pub(super) at: Affine,
|
||||
pub(super) mode: vectorcraft_doc::color::BlendMode,
|
||||
/// Made before its object moved ([`ShadowEntry::moved`]).
|
||||
pub(super) moved: bool,
|
||||
/// For a raster cropped to the view, or made over a raster that is ([`ShadowEntry::view`]), the
|
||||
/// rect it was made for, in this frame's device pixels ([`ViewCrop::served`]).
|
||||
pub(super) cropped: Option<Rect>,
|
||||
}
|
||||
|
||||
impl ShadowEntry {
|
||||
/// The bytes it holds, for [`FX_RASTER_BUDGET`].
|
||||
pub(crate) fn bytes(&self) -> usize {
|
||||
raster_bytes(&self.image) + self.made.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
/// What a cached shadow, glow or filtered raster was made from. While
|
||||
/// [`crate::RenderOptions::interacting`], an object whose content only moved after the raster
|
||||
/// was made draws from that raster ([`Renderer::move_shadow`], [`Renderer::move_pixel_content`]).
|
||||
pub(crate) struct Made {
|
||||
/// The content's pixels as drawn into the raster before the effects ran: premultiplied RGBA,
|
||||
/// or their alpha alone under a shadow or glow that is one color ([`silhouette`]).
|
||||
pub(super) pixels: Vec<u8>,
|
||||
/// The effects run on them.
|
||||
pub(super) fx: Vec<RasterFx>,
|
||||
/// The size of the content's document-space bounds.
|
||||
pub(super) size: Size,
|
||||
/// The color channels the Photoshop-style filters were told of (none for shadows and glows).
|
||||
pub(super) channels: Option<Channels>,
|
||||
/// Feather's clip outline, moved to put the raster's anchor at the origin.
|
||||
pub(super) outline: Option<(BezPath, FillRule)>,
|
||||
}
|
||||
|
||||
/// How far (document units) the size of a moved object's content and the points of its Feather
|
||||
/// outline may lie from those of the content a raster was made from.
|
||||
const MOVED_SLACK: f64 = 1e-6;
|
||||
|
||||
impl Made {
|
||||
/// The bytes it holds beyond the raster.
|
||||
pub(super) fn bytes(&self) -> usize {
|
||||
self.pixels.len() + self.outline.as_ref().map_or(0, |(g, _)| crate::lru::path_bytes(g))
|
||||
}
|
||||
|
||||
/// Whether content of size `size` under the effects `fx`, told of `channels`, with Feather's
|
||||
/// `outline` (moved to put its anchor at the origin) may be this content moved: the same
|
||||
/// effects, channels and outline rule, and the size and outline within [`MOVED_SLACK`].
|
||||
pub(super) fn fits(&self, fx: &[&RasterFx], size: Size, channels: Option<Channels>, outline: Option<&(BezPath, FillRule)>) -> bool {
|
||||
let near = |a: f64, b: f64| (a - b).abs() <= MOVED_SLACK;
|
||||
self.fx.len() == fx.len()
|
||||
&& self.fx.iter().zip(fx).all(|(a, b)| a == *b)
|
||||
&& near(self.size.width, size.width)
|
||||
&& near(self.size.height, size.height)
|
||||
&& self.channels == channels
|
||||
&& match (&self.outline, outline) {
|
||||
(None, None) => true,
|
||||
(Some((a, ra)), Some((b, rb))) => ra == rb && same_path(a, b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a` and `b` have the same elements with points within [`MOVED_SLACK`] of each other.
|
||||
fn same_path(a: &BezPath, b: &BezPath) -> bool {
|
||||
let near = |p: &Point, q: &Point| (*p - *q).hypot() <= MOVED_SLACK;
|
||||
a.elements().len() == b.elements().len()
|
||||
&& a.elements().iter().zip(b.elements()).all(|pair| match pair {
|
||||
(PathEl::MoveTo(p), PathEl::MoveTo(q)) | (PathEl::LineTo(p), PathEl::LineTo(q)) => near(p, q),
|
||||
(PathEl::QuadTo(p0, p1), PathEl::QuadTo(q0, q1)) => near(p0, q0) && near(p1, q1),
|
||||
(PathEl::CurveTo(p0, p1, p2), PathEl::CurveTo(q0, q1, q2)) => near(p0, q0) && near(p1, q1) && near(p2, q2),
|
||||
(PathEl::ClosePath, PathEl::ClosePath) => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Feather's clip outline of `content` when `chain` feathers it, moved to put `anchor` at the
|
||||
/// origin ([`Made::outline`]).
|
||||
pub(super) fn feather_outline(chain: &[&RasterFx], content: &Content, anchor: Point) -> Option<(BezPath, FillRule)> {
|
||||
let (g, rule) = content.outline.filter(|_| chain.iter().any(|fx| matches!(fx, RasterFx::Feather { .. })))?;
|
||||
let mut g = g.clone();
|
||||
g.apply_affine(Affine::translate(-anchor.to_vec2()));
|
||||
Some((g, rule))
|
||||
}
|
||||
|
||||
/// The object a cached raster was made for: the drawn `Arc` itself when known (kept alive, so its
|
||||
/// address, the cache key, names it alone and a key match is exact), else a copy compared by value.
|
||||
pub(crate) enum Drawn {
|
||||
Shared(Arc<Node>),
|
||||
Copy(Box<Node>),
|
||||
}
|
||||
|
||||
impl Drawn {
|
||||
pub(super) fn of(n: &Node, owner: Option<&Arc<Node>>) -> Self {
|
||||
match owner {
|
||||
Some(a) => Self::Shared(a.clone()),
|
||||
None => Self::Copy(Box::new(n.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `n` the object drawn?
|
||||
pub(super) fn is(&self, n: &Node) -> bool {
|
||||
match self {
|
||||
Self::Shared(a) => std::ptr::eq(&**a, n),
|
||||
Self::Copy(c) => **c == *n,
|
||||
}
|
||||
}
|
||||
|
||||
/// The id of the object drawn.
|
||||
pub(super) fn id(&self) -> NodeId {
|
||||
match self {
|
||||
Self::Shared(a) => a.id,
|
||||
Self::Copy(c) => c.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What an object's content reads from outside its own nodes, in it or in the opacity masks of the
|
||||
/// objects in it ([`Reads::of`]): which parts of [`Inputs`] can change what its rasters show.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct Reads {
|
||||
/// Pattern paints: the pattern definitions. Their art can hold anything, so content painted
|
||||
/// with a pattern reads everything else too.
|
||||
pub(super) patterns: bool,
|
||||
/// Symbol instances and type with inline graphics: the symbols' art. Symbol art can hold
|
||||
/// anything, so content that shows a symbol reads everything else too.
|
||||
pub(super) symbols: bool,
|
||||
/// Images and placed documents: their pixels and files, and the options and the layer's Dim
|
||||
/// Images they are drawn with.
|
||||
pub(super) images: bool,
|
||||
/// Brushed strokes: the brush library.
|
||||
pub(super) brushes: bool,
|
||||
/// Type: the fonts, whether overprinting shows ([`crate::proof::overprints`]) and the
|
||||
/// substitution highlights.
|
||||
pub(super) text: bool,
|
||||
/// Plug-in effects on its objects: the plug-ins installed. Those in the symbol and pattern art
|
||||
/// it shows are found with the document's inputs ([`DocInputs`]).
|
||||
pub(super) plugins: bool,
|
||||
}
|
||||
|
||||
impl Reads {
|
||||
/// What the content of `n` reads.
|
||||
pub(crate) fn of(n: &Node) -> Self {
|
||||
Self::of_all(std::iter::once(n))
|
||||
}
|
||||
|
||||
/// What the content of `nodes` reads together. A pattern's art can hold anything, as a
|
||||
/// symbol's can, so content painted with a pattern reads everything else too (#1723).
|
||||
pub(crate) fn of_all<'a>(nodes: impl Iterator<Item = &'a Node>) -> Self {
|
||||
let mut reads = Self::default();
|
||||
nodes.for_each(|n| reads.add(n));
|
||||
if reads.symbols || reads.patterns {
|
||||
return Self { patterns: true, symbols: true, images: true, brushes: true, text: true, plugins: reads.plugins };
|
||||
}
|
||||
reads
|
||||
}
|
||||
|
||||
/// Does the content read anything outside its own nodes?
|
||||
pub(crate) fn any(self) -> bool {
|
||||
self != Self::default()
|
||||
}
|
||||
|
||||
/// Can installing or removing a plug-in change what the content draws? It can when an object in
|
||||
/// it has a plug-in effect, and when it shows symbol or pattern art.
|
||||
pub(crate) fn plugins_can_change(self) -> bool {
|
||||
self.plugins || self.symbols || self.patterns
|
||||
}
|
||||
|
||||
/// Does the content show symbol or pattern art?
|
||||
pub(crate) fn shows_definitions(self) -> bool {
|
||||
self.symbols || self.patterns
|
||||
}
|
||||
|
||||
fn add(&mut self, n: &Node) {
|
||||
let pattern = |p: &vectorcraft_color::Paint| matches!(p, vectorcraft_color::Paint::Pattern { .. });
|
||||
n.walk(&mut |c| {
|
||||
self.plugins |= effects::has_plugin_effect(c);
|
||||
for item in &c.appearance.items {
|
||||
self.patterns |= pattern(item.paint());
|
||||
self.brushes |= matches!(item, AppearanceItem::Stroke(s) if s.brush.is_some());
|
||||
}
|
||||
match &c.kind {
|
||||
NodeKind::SymbolInstance { .. } => self.symbols = true,
|
||||
NodeKind::Image(_) | NodeKind::PlacedDocument(_) => self.images = true,
|
||||
NodeKind::Text(t) => {
|
||||
self.text = true;
|
||||
self.symbols |= t.runs.iter().any(|r| r.inline.is_some());
|
||||
self.patterns |= t.runs.iter().any(|r| pattern(&r.style.fill) || pattern(&r.style.stroke));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(m) = c.extras().mask.as_deref() {
|
||||
self.add(&m.art);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// What a cached shadow, glow or filtered raster was drawn from besides its object, its effects
|
||||
/// and the view's zoom and rotation ([`Renderer::raster_inputs`]). A raster is drawn from only
|
||||
/// while its inputs equal the frame's.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Inputs {
|
||||
/// What the content reads.
|
||||
pub(super) reads: Reads,
|
||||
/// A hash of the inputs as found when the raster was made: those every raster reads, those
|
||||
/// `reads` names, and what the effects read themselves ([`Inputs::with`]).
|
||||
pub(super) key: u64,
|
||||
/// The document's resources and the color settings `key` was found with: holding them keeps
|
||||
/// the addresses they hashed for their values ([`DocInputs`]).
|
||||
pub(super) _doc: Arc<DocInputs>,
|
||||
}
|
||||
|
||||
impl PartialEq for Inputs {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self.reads, self.key) == (other.reads, other.key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Inputs {
|
||||
/// These inputs with `extra`, something the effects read themselves.
|
||||
pub(super) fn with(mut self, extra: impl Hash) -> Self {
|
||||
let mut h = DefaultHasher::new();
|
||||
(self.key, extra).hash(&mut h);
|
||||
self.key = h.finish();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// What raster contents read from the document by name or key, and the color settings, as a frame
|
||||
/// finds them ([`Renderer::doc_inputs`]): hashes of the patterns' tilings
|
||||
/// ([`crate::pattern::hash_tiling`]), of the symbols' names and art and of the images' keys and
|
||||
/// pixels, the address of the color settings, and whether the patterns' and symbols' art has a
|
||||
/// plug-in effect. Art, pixels and previews are hashed by the addresses of their shared values,
|
||||
/// and names, keys, tiles, tile types and overlaps are hashed by value. The live cache keeps these
|
||||
/// inputs with each evaluated live object, with each mesh's quads and with what each walk of a
|
||||
/// live object or of effect art found when it found no placed file (`crate::live`), and
|
||||
/// [`Renderer::fx_art`] keeps them with art that can be made from evaluated live objects.
|
||||
pub(crate) struct DocInputs {
|
||||
pub(super) patterns: u64,
|
||||
pub(super) symbols: u64,
|
||||
pub(super) images: u64,
|
||||
pub(crate) cms: usize,
|
||||
/// Whether an object in a pattern's or a symbol's art, or in an opacity mask there, has a
|
||||
/// visible plug-in effect.
|
||||
pub(super) plugins: bool,
|
||||
/// Weak references to the values whose addresses the hashes hold: a weak reference keeps the
|
||||
/// allocation, not the value, so no other value gets one of those addresses while these
|
||||
/// inputs are kept.
|
||||
pub(super) _held: Held,
|
||||
}
|
||||
|
||||
impl DocInputs {
|
||||
/// Whether `self` and `other` were made from the same patterns and symbols: their names,
|
||||
/// tilings and art.
|
||||
pub(crate) fn same_art(&self, other: &DocInputs) -> bool {
|
||||
(self.patterns, self.symbols) == (other.patterns, other.symbols)
|
||||
}
|
||||
}
|
||||
|
||||
/// The pattern and symbol art, the image pixels and the color settings, held weakly.
|
||||
pub(super) type Held = (Vec<Weak<Node>>, Vec<Weak<Vec<u8>>>, Weak<Cms>);
|
||||
|
||||
/// Most bytes the shadow and filtered-content rasters of one renderer hold together, with what each
|
||||
/// was made from ([`Made`]): the least recently drawn go beyond it.
|
||||
pub(crate) const FX_RASTER_BUDGET: usize = 512 << 20;
|
||||
|
||||
pub(super) fn raster_bytes(pm: &vello_cpu::Pixmap) -> usize {
|
||||
pm.width() as usize * pm.height() as usize * 4
|
||||
}
|
||||
|
||||
/// Live-effect geometry of a node, kept across frames: the object-level result and each
|
||||
/// appearance item's own result on it (filled in as items are drawn). The node is kept alive, so
|
||||
/// its address names it alone and a hit is exact.
|
||||
pub(crate) struct FxGeom {
|
||||
pub(crate) node: Arc<Node>,
|
||||
pub(super) g: Arc<BezPath>,
|
||||
pub(super) items: Vec<Option<Arc<BezPath>>>,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
use super::bounds::{apply, item_effects};
|
||||
use super::entries::{Content, DocInputs, Drawn, FX_RASTER_BUDGET, Inputs, KeptImage, Made, Reads, feather_outline};
|
||||
#[cfg(test)]
|
||||
use super::raster::RASTERIZED;
|
||||
use super::raster::{
|
||||
DRAG_PIXEL_BUDGET, PIXEL_BUDGET, PixelEntry, ViewCrop, content_changing, doc_raster_ppi, drag_scale, draw_placed, draw_scaled, filter_region,
|
||||
filter_region_within, hard_edged, image_size, linear_key, looks_around, pan_margin,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
impl Renderer {
|
||||
/// Paint `content` through the content-changing effects of `rfx` (Gaussian Blur, Feather, the
|
||||
/// Photoshop-style filters and Rasterize) in stack order on its pixels: rendered offscreen over
|
||||
/// all it can reach, filtered on the CPU and drawn back. A Rasterize first in the chain draws the
|
||||
/// content on its image's own grid instead ([`Self::rasterize_image`]) when that image is within
|
||||
/// the limits below; content with a [`Kept`] image draws that image after its first draw. The
|
||||
/// raster holds at most [`PIXEL_BUDGET`] pixels and [`crate::MAX_RASTER_SIDE`] a side: content
|
||||
/// over the budget is cropped to the view (content with a [`Kept`] image to its window) when
|
||||
/// every effect only looks nearby, and content still over either limit is rendered at a lower
|
||||
/// resolution. A whole raster at full resolution is cached while the object, the
|
||||
/// zoom and what the content reads ([`Inputs`]) stay the same (panning only moves it), and
|
||||
/// while interacting after the content only moved ([`Self::move_pixel_content`]). A cropped or
|
||||
/// lower-resolution raster is cached while the object, the zoom and what the content reads stay
|
||||
/// the same and a pan keeps the view inside the rect it was made for ([`PixelEntry::view`]): the
|
||||
/// view, and for a raster cropped to it that is made again at the same zoom and rotation, outside
|
||||
/// a drag and for content without a [`Kept`] image, the view with [`pan_margin`] around it. A
|
||||
/// lower-resolution one is kept resampled to the view's pixels ([`Self::resampled`]).
|
||||
/// While dragging, content without such a raster to draw renders within [`DRAG_PIXEL_BUDGET`]
|
||||
/// pixels and is not cached.
|
||||
pub(super) fn pixel_content(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
content: &Content,
|
||||
rfx: &[RasterFx],
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
let chain: Vec<&RasterFx> = rfx.iter().filter(content_changing).collect();
|
||||
// When the first draw of this content kept its raster, that raster is drawn.
|
||||
if let Some(KeptImage::Raster(image, place)) = content.kept.and_then(|k| k.image.borrow().clone()) {
|
||||
draw_placed(ctx, image, f.view * place);
|
||||
return;
|
||||
}
|
||||
// Content kept with an image over the limits is drawn in the split's frame, over where the
|
||||
// effects after the split take it from, then placed in this one.
|
||||
let call = f.view;
|
||||
let split_frame;
|
||||
let (f, screen) = match content.kept.and_then(|k| k.frame.map(|frame| (k.window, frame))) {
|
||||
Some((window, (view, px))) => {
|
||||
split_frame = Frame { view, px, ..*f };
|
||||
(&split_frame, view.transform_rect_bbox(window))
|
||||
}
|
||||
None => (f, Rect::new(0.0, 0.0, ctx.width() as f64, ctx.height() as f64)),
|
||||
};
|
||||
// Where the result can paint: each effect spreads what the ones before it painted.
|
||||
let reach = chain.iter().fold(content.reach, |r, fx| {
|
||||
let o = fx.outset(r);
|
||||
r.inflate(o, o)
|
||||
});
|
||||
let full = f.view.transform_rect_bbox(reach).inflate(2.0, 2.0);
|
||||
if !(full.area().is_finite() && full.x1 > screen.x0 && full.y1 > screen.y0 && full.x0 < screen.x1 && full.y0 < screen.y1) {
|
||||
return;
|
||||
}
|
||||
let fits = full.area() <= PIXEL_BUDGET;
|
||||
// Device pixels from a pixel to the content deciding it (`None`: anywhere).
|
||||
let margin = chain.iter().try_fold(2.0, |m, fx| looks_around(fx).map(|d| m + d / f.px));
|
||||
let linear = linear_key(f.view);
|
||||
let key = content.cache.map(|slot| (content.node as *const Node as usize, slot, f.ink));
|
||||
// Made again at this zoom and rotation (after a pan), a raster cropped to the view covers a
|
||||
// margin around the view too, so that later pans within the margin draw it.
|
||||
let again = key.and_then(|key| self.pixel_fx.get(&key)).is_some_and(|e| e.linear == linear) && content.kept.is_none() && !f.opts.dragging;
|
||||
let room = match margin {
|
||||
Some(m) if !fits && again => {
|
||||
let e = pan_margin((screen.width(), screen.height()), m, PIXEL_BUDGET, f64::from(crate::MAX_RASTER_SIDE));
|
||||
screen.inflate(e, e)
|
||||
}
|
||||
_ => screen,
|
||||
};
|
||||
let region = match margin {
|
||||
Some(m) if !fits => full.intersect(room.inflate(m, m)),
|
||||
_ => full,
|
||||
};
|
||||
let (x0, y0) = (region.x0.floor(), region.y0.floor());
|
||||
let (rw, rh) = ((region.x1 - x0).max(1.0), (region.y1 - y0).max(1.0));
|
||||
// (vello_cpu panics drawing near `u16::MAX` pixels a side.)
|
||||
let side = f64::from(crate::MAX_RASTER_SIDE);
|
||||
let k = (PIXEL_BUDGET / (rw * rh)).sqrt().min(side / rw).min(side / rh).min(1.0);
|
||||
let (w, h) = ((rw * k).ceil().clamp(1.0, side), (rh * k).ceil().clamp(1.0, side));
|
||||
// The cached raster: of the whole content at full size at this zoom, else of this view (and
|
||||
// the margin around it).
|
||||
let whole = fits && k >= 1.0;
|
||||
let in_view = (!whole).then(|| ViewCrop::of(f.view, room));
|
||||
let stamp = self.stamp;
|
||||
let channels = match f.ink {
|
||||
Ink::Cmy => effects::pixel::Channels::CmyPlane,
|
||||
Ink::K => effects::pixel::Channels::KPlane,
|
||||
Ink::Display | Ink::RichBlack if f.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk => effects::pixel::Channels::Cmyk,
|
||||
Ink::Display | Ink::RichBlack => effects::pixel::Channels::Rgb,
|
||||
};
|
||||
// De-Interlace's field lines: the rows of the document's raster grid (1 to 2400 ppi).
|
||||
let ppi = doc_raster_ppi(f.doc);
|
||||
// What the content reads (as the raster kept for it found it, else found now), with the
|
||||
// channels the effects run on and, under De-Interlace or Rasterize, the resolution.
|
||||
let inputs = key.map(|key| {
|
||||
let reads = self.pixel_fx.get(&key).filter(|e| e.node.is(content.node)).map_or_else(|| Reads::of(content.node), |e| e.inputs.reads);
|
||||
let interlaced = chain.iter().any(|fx| matches!(fx, RasterFx::Pixel(PixelFx::DeInterlace { .. }) | RasterFx::Rasterize(_)));
|
||||
self.raster_inputs(f, content.node, reads).with((std::mem::discriminant(&channels), interlaced.then_some(ppi.to_bits())))
|
||||
});
|
||||
let interacting = f.opts.interacting;
|
||||
let in_place = |e: &PixelEntry| match e.view {
|
||||
None => whole,
|
||||
Some(v) => !whole && v.serves(f.view, screen),
|
||||
};
|
||||
let exact = |e: &PixelEntry| {
|
||||
e.linear == linear && in_place(e) && e.node.is(content.node) && Some(&e.inputs) == inputs.as_ref() && (interacting || !e.moved)
|
||||
};
|
||||
if interacting
|
||||
&& whole
|
||||
&& let Some(key) = key
|
||||
&& let Some(inputs) = &inputs
|
||||
&& !self.pixel_fx.get(&key).is_some_and(exact)
|
||||
{
|
||||
self.move_pixel_content(f, content, key, &chain, channels, inputs, paint);
|
||||
}
|
||||
if let Some(e) = key.and_then(|key| self.pixel_fx.get_mut(&key)).filter(|e| exact(e)) {
|
||||
e.stamp = stamp;
|
||||
self.stats.moved += usize::from(e.moved);
|
||||
let p = f.view * e.anchor;
|
||||
let (image, at, k) = (e.image.clone(), ((p.x + e.rel.0).round(), (p.y + e.rel.1).round()), e.k);
|
||||
crate::paint::add_decode_failures(e.failed);
|
||||
draw_scaled(ctx, image, at, k);
|
||||
return;
|
||||
}
|
||||
// During a drag, content without a raster to draw renders within the drag's budget and keeps
|
||||
// no raster (#2558).
|
||||
let (k, w, h, key) = match drag_scale(f.opts.dragging, (rw, rh), k) {
|
||||
Some(d) => {
|
||||
self.stats.moved += 1;
|
||||
self.stats.previewed += 1;
|
||||
(d, (rw * d).ceil().clamp(1.0, side), (rh * d).ceil().clamp(1.0, side), None)
|
||||
}
|
||||
None => (k, w, h, key),
|
||||
};
|
||||
let (wu, hu) = (w as u16, h as u16);
|
||||
let view = Affine::scale(k) * Affine::translate((-x0, -y0)) * f.view;
|
||||
let to_doc = view.inverse();
|
||||
let px = f.px / k;
|
||||
let frame = Frame { mt: false, view, visible: to_doc.transform_rect_bbox(Rect::new(0.0, 0.0, w, h)), px, ..*f };
|
||||
let line = 72.0 / ppi;
|
||||
let (before, decoded) = (self.stats.moved, crate::paint::decode_failures());
|
||||
// A Rasterize first in the chain draws the content on its image's own grid instead, unless
|
||||
// that image is too large: the content is drawn once.
|
||||
let mut rasterized = match chain.first() {
|
||||
Some(RasterFx::Rasterize(r)) => {
|
||||
let rect = r.image(content.bounds.unwrap_or(content.reach), line);
|
||||
let kept = content.kept.and_then(|k| match &*k.image.borrow() {
|
||||
Some(KeptImage::Grid(image, failed)) => {
|
||||
crate::paint::add_decode_failures(*failed);
|
||||
Some(image.clone())
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let image = kept.or_else(|| {
|
||||
let failures = crate::paint::decode_failures();
|
||||
let image = self.rasterize_image(f, r, rect, paint).map(std::rc::Rc::new);
|
||||
if let (Some(k), Some(image)) = (content.kept, &image) {
|
||||
*k.image.borrow_mut() = Some(KeptImage::Grid(image.clone(), crate::paint::decode_failures() - failures));
|
||||
}
|
||||
image
|
||||
});
|
||||
image.map(|image| (image, rect))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let mut pm = vello_cpu::Pixmap::new(wu, hu);
|
||||
if rasterized.is_none() {
|
||||
#[cfg(test)]
|
||||
if matches!(chain.first(), Some(RasterFx::Rasterize(_))) {
|
||||
RASTERIZED.with(|n| n.set(n.get() + 1));
|
||||
}
|
||||
let mut off = f.offscreen_context(wu, hu);
|
||||
if hard_edged(&chain, px) {
|
||||
off.set_aliasing_threshold(crate::AntiAlias::None.threshold());
|
||||
}
|
||||
self.inside_layer(|r| paint(r, &mut off, &frame));
|
||||
off.flush();
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
}
|
||||
let failed = crate::paint::decode_failures() - decoded;
|
||||
// Content drawn over moved rasters makes a moved raster.
|
||||
let moved = self.stats.moved > before;
|
||||
// What the raster is made from, for its object once it moves (`move_pixel_content`): a
|
||||
// cropped or coarser raster never moves.
|
||||
let made = key.map(|_| Made {
|
||||
pixels: if whole { pm.data_as_u8_slice().to_vec() } else { Vec::new() },
|
||||
fx: chain.iter().map(|fx| (*fx).clone()).collect(),
|
||||
size: content.reach.size(),
|
||||
channels: Some(channels),
|
||||
outline: feather_outline(&chain, content, content.reach.origin()),
|
||||
});
|
||||
let (center, extent) = (content.reach.center(), 0.5 * content.reach.width().hypot(content.reach.height()));
|
||||
let space = effects::PixelSpace { to_doc, px, center, channels, line, extent };
|
||||
let (wz, hz) = (wu as usize, hu as usize);
|
||||
// What the content covers after the effects run so far: Rasterize makes its image over it.
|
||||
let mut bounds = content.bounds.unwrap_or(content.reach);
|
||||
// The silhouette Feather and Create Clipping Mask clip to: the content's, or after a Rasterize
|
||||
// on white without a clipping mask, its image.
|
||||
let mut image_outline: Option<BezPath> = None;
|
||||
for (i, fx) in chain.into_iter().enumerate() {
|
||||
let before = bounds;
|
||||
bounds = fx.spread(before, space.line);
|
||||
let outline = image_outline.as_ref().map(|g| (g, FillRule::NonZero)).or(content.outline);
|
||||
match fx {
|
||||
RasterFx::GaussianBlur { radius } => effects::pixel::gaussian_rgba(pm.data_as_u8_slice_mut(), wz, hz, radius / 2.0 / px),
|
||||
RasterFx::Feather { radius } => {
|
||||
effects::pixel::gaussian_rgba(pm.data_as_u8_slice_mut(), wz, hz, radius / 2.0 / px);
|
||||
if let Some((g, rule)) = outline {
|
||||
self.clip_pixels(&mut pm, &frame, g, rule);
|
||||
}
|
||||
}
|
||||
RasterFx::Pixel(p) => p.apply(pm.data_as_u8_slice_mut(), wz, hz, &space),
|
||||
RasterFx::Rasterize(r) => {
|
||||
match rasterized.take().filter(|_| i == 0) {
|
||||
Some((image, rect)) => {
|
||||
let (pixels, cols, rows) = &*image;
|
||||
r.compose(pm.data_as_u8_slice_mut(), wz, hz, &space, pixels, *cols, *rows, rect)
|
||||
}
|
||||
None => r.apply(pm.data_as_u8_slice_mut(), wz, hz, &space, before),
|
||||
}
|
||||
if let Some((g, rule)) = outline.filter(|_| r.clipping_mask) {
|
||||
self.clip_pixels(&mut pm, &frame, g, rule);
|
||||
}
|
||||
if r.white && !r.clipping_mask {
|
||||
image_outline = Some(bounds.to_path(0.1));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let image = std::sync::Arc::new(pm);
|
||||
// A raster kept for this view alone at a lower resolution than the view's is resampled to the
|
||||
// view's pixels once: each frame that draws it then copies it pixel for pixel, where sampling
|
||||
// it again across the whole view cost several times as much as a plain fill.
|
||||
let view_raster = key.is_some() && in_view.is_some() && k < 1.0 && content.kept.is_none();
|
||||
let (image, x0, y0, k) = match view_raster.then(|| self.resampled(f, image.clone(), (x0, y0), k, screen)).flatten() {
|
||||
Some((image, x, y)) => (image, x, y, 1.0),
|
||||
None => (image, x0, y0, k),
|
||||
};
|
||||
// A cancelled export drew part of the content: that raster isn't kept for later frames.
|
||||
if !vectorcraft_doc::cancel::is_cancelled()
|
||||
&& let Some(((key, made), inputs)) = key.zip(made).zip(inputs)
|
||||
{
|
||||
let anchor = content.reach.origin();
|
||||
let p = f.view * anchor;
|
||||
let rel = (x0 - p.x, y0 - p.y);
|
||||
let owner = content.owner.cloned().or_else(|| self.fx_owner.clone().filter(|o| std::ptr::eq(&**o, content.node)));
|
||||
let node = Drawn::of(content.node, owner.as_ref());
|
||||
let entry = PixelEntry { node, linear, anchor, rel, k, image: image.clone(), view: in_view, made, inputs, moved, failed, stamp };
|
||||
self.pixel_fx.insert(key, entry);
|
||||
self.trim_fx_rasters(FX_RASTER_BUDGET);
|
||||
}
|
||||
// An image over the limits was drawn at a frame's resolution: its raster is kept.
|
||||
match content.kept.filter(|kept| kept.image.borrow().is_none()) {
|
||||
Some(kept) => {
|
||||
let place = f.view.inverse() * Affine::translate((x0, y0)) * Affine::scale(1.0 / k);
|
||||
*kept.image.borrow_mut() = Some(KeptImage::Raster(image.clone(), place));
|
||||
if f.view == call { draw_scaled(ctx, image, (x0, y0), k) } else { draw_placed(ctx, image, call * place) }
|
||||
}
|
||||
None => draw_scaled(ctx, image, (x0, y0), k),
|
||||
}
|
||||
}
|
||||
|
||||
/// `image` as [`draw_scaled`] draws it with its top-left at (`x`, `y`) device pixels, `1 / k`
|
||||
/// device pixels a pixel, resampled to the device pixels it covers of `screen`, with their
|
||||
/// top-left. `None` when it covers none of them.
|
||||
fn resampled(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
image: std::sync::Arc<vello_cpu::Pixmap>,
|
||||
(x, y): (f64, f64),
|
||||
k: f64,
|
||||
screen: Rect,
|
||||
) -> Option<(std::sync::Arc<vello_cpu::Pixmap>, f64, f64)> {
|
||||
let covered = Rect::new(x, y, x + f64::from(image.width()) / k, y + f64::from(image.height()) / k).intersect(screen);
|
||||
let (x0, y0) = (covered.x0.floor(), covered.y0.floor());
|
||||
let (w, h) = (covered.x1.ceil() - x0, covered.y1.ceil() - y0);
|
||||
let side = f64::from(crate::MAX_RASTER_SIDE);
|
||||
if !((1.0..=side).contains(&w) && (1.0..=side).contains(&h)) {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = (w as u16, h as u16);
|
||||
let mut off = f.offscreen_context(w, h);
|
||||
draw_scaled(&mut off, image, (x - x0, y - y0), k);
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
Some((std::sync::Arc::new(pm), x0, y0))
|
||||
}
|
||||
|
||||
/// While interacting, give `content` (cache key `key`) the filtered raster an earlier frame
|
||||
/// made at full size for its object (the same id, slot and ink, at this zoom and rotation, with
|
||||
/// the same `inputs`) when only the content's place changed after that frame: made by the same
|
||||
/// effects `chain`, told of the same `channels`, from content whose bounds have the same size
|
||||
/// (and the same Feather outline against its anchor), and whose pixels are the pixels of
|
||||
/// `content` drawn into a raster placed as that one was against its anchor. The raster is kept
|
||||
/// as `content`'s, marked moved, and is drawn at the nearest whole pixel. Content under
|
||||
/// De-Interlace, or under a Rasterize first in the chain, takes none.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn move_pixel_content(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
content: &Content,
|
||||
key: (usize, usize, Ink),
|
||||
chain: &[&RasterFx],
|
||||
channels: Channels,
|
||||
inputs: &Inputs,
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
if chain.iter().any(|fx| matches!(fx, RasterFx::Pixel(PixelFx::DeInterlace { .. }))) || matches!(chain.first(), Some(RasterFx::Rasterize(_)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (linear, stamp, anchor) = (linear_key(f.view), self.stamp, content.reach.origin());
|
||||
let outline = feather_outline(chain, content, anchor);
|
||||
let fits = |k: &(usize, usize, Ink), e: &PixelEntry| {
|
||||
(k.1, k.2) == (key.1, key.2)
|
||||
&& e.stamp < stamp
|
||||
&& e.linear == linear
|
||||
&& e.k >= 1.0
|
||||
&& e.view.is_none()
|
||||
&& e.node.id() == content.node.id
|
||||
&& e.inputs == *inputs
|
||||
&& e.made.fits(chain, content.reach.size(), Some(channels), outline.as_ref())
|
||||
};
|
||||
let Some((old, rel, w, h)) = self.pixel_fx.iter().find(|(k, e)| fits(k, e)).map(|(k, e)| (*k, e.rel, e.image.width(), e.image.height()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The content drawn where the old content lies in its raster.
|
||||
let p = f.view * anchor;
|
||||
let view = Affine::translate((-(p.x + rel.0), -(p.y + rel.1))) * f.view;
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let frame = Frame { mt: false, view, visible, ..*f };
|
||||
let mut off = f.offscreen_context(w, h);
|
||||
if hard_edged(chain, f.px) {
|
||||
off.set_aliasing_threshold(crate::AntiAlias::None.threshold());
|
||||
}
|
||||
self.inside_layer(|r| paint(r, &mut off, &frame));
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
if self.pixel_fx.get(&old).is_none_or(|e| e.made.pixels.as_slice() != pm.data_as_u8_slice()) {
|
||||
return;
|
||||
}
|
||||
let owner = content.owner.cloned().or_else(|| self.fx_owner.clone().filter(|o| std::ptr::eq(&**o, content.node)));
|
||||
if let Some(mut e) = self.pixel_fx.remove(&old) {
|
||||
(e.node, e.anchor, e.moved) = (Drawn::of(content.node, owner.as_ref()), anchor, true);
|
||||
self.pixel_fx.insert(key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The inputs a raster of `n`'s content, which reads `reads`, is drawn with in frame `f`: the
|
||||
/// color settings, the edge smoothing, the document's raster effects resolution (which a
|
||||
/// Rasterize or De-Interlace anywhere in the content follows), the knockout `n` inherits and,
|
||||
/// while `n` holds the isolated group, isolation mode; the pattern definitions, symbols and
|
||||
/// brush library it reads; for images and placed documents their pixels and files, the
|
||||
/// layer's Dim Images and the options they are drawn with; for type the fonts, whether
|
||||
/// overprinting shows ([`crate::proof::overprints`]) and Document Setup's substitution
|
||||
/// highlights; and the plug-ins installed, when an object in it has a plug-in effect, and
|
||||
/// when it shows symbol or pattern art and an object in the document's symbol or pattern art
|
||||
/// has one.
|
||||
pub(crate) fn raster_inputs(&mut self, f: &Frame, n: &Node, reads: Reads) -> Inputs {
|
||||
let doc = self.doc_inputs(f.doc);
|
||||
let mut h = DefaultHasher::new();
|
||||
(doc.cms, f.opts.anti_alias.threshold(), self.knockout, f.doc.raster_effects_ppi.to_bits()).hash(&mut h);
|
||||
// Cached effects on containers can include paths made into guides.
|
||||
f.opts.hide_guides.hash(&mut h);
|
||||
if let Some((isolated, path)) = self.isolation_path(f)
|
||||
&& n.id != isolated
|
||||
&& path.contains(&n.id)
|
||||
{
|
||||
isolated.hash(&mut h);
|
||||
}
|
||||
if reads.patterns {
|
||||
doc.patterns.hash(&mut h);
|
||||
}
|
||||
if reads.symbols {
|
||||
doc.symbols.hash(&mut h);
|
||||
}
|
||||
if reads.images {
|
||||
let o = f.opts;
|
||||
(doc.images, self.dim_images.map(f32::to_bits), o.smooth_images, o.image_mips, o.trace_views, o.progressive_placed).hash(&mut h);
|
||||
o.trace_source.as_deref().hash(&mut h);
|
||||
o.mark_undecoded_images.hash(&mut h);
|
||||
}
|
||||
if reads.brushes {
|
||||
self.brushes.library_key(f.doc, self.stamp).hash(&mut h);
|
||||
}
|
||||
if reads.text {
|
||||
let o = f.opts;
|
||||
let highlights = o.highlight_substitutions.then_some((
|
||||
o.highlight_substituted_fonts,
|
||||
f.doc.setup.highlight_substituted_glyphs,
|
||||
o.highlight_alternate_glyphs,
|
||||
));
|
||||
(vectorcraft_text::FontDb::global().generation(), crate::proof::overprints(f.opts), highlights).hash(&mut h);
|
||||
}
|
||||
if reads.plugins || (doc.plugins && (reads.symbols || reads.patterns)) {
|
||||
effects::plugin_revision().hash(&mut h);
|
||||
}
|
||||
Inputs { reads, key: h.finish(), _doc: doc }
|
||||
}
|
||||
|
||||
/// [`DocInputs`] for document `doc`: found once a frame (and again for another document drawn
|
||||
/// in it, such as a placed document's own), and kept while they stay the same.
|
||||
pub(crate) fn doc_inputs(&mut self, doc: &Document) -> Arc<DocInputs> {
|
||||
let at = (self.stamp, std::ptr::from_ref(doc) as usize);
|
||||
if let Some((seen, d)) = &self.doc_inputs
|
||||
&& *seen == at
|
||||
{
|
||||
return d.clone();
|
||||
}
|
||||
let cms = vectorcraft_color::cms::active();
|
||||
let hash = |each: &mut dyn FnMut(&mut DefaultHasher)| {
|
||||
let mut h = DefaultHasher::new();
|
||||
each(&mut h);
|
||||
h.finish()
|
||||
};
|
||||
let address = |a: &Arc<Node>| Arc::as_ptr(a) as usize;
|
||||
let patterns = hash(&mut |h| doc.patterns.iter().for_each(|p| crate::pattern::hash_tiling(p, h)));
|
||||
let symbols = hash(&mut |h| doc.symbols.iter().for_each(|s| (&s.name, address(&s.art)).hash(h)));
|
||||
let images = hash(&mut |h| {
|
||||
for (key, blob) in &doc.images {
|
||||
(key, Arc::as_ptr(&blob.bytes) as usize, blob.proxy.as_ref().map(|p| Arc::as_ptr(p) as usize)).hash(h);
|
||||
}
|
||||
});
|
||||
let found = (patterns, symbols, images, Arc::as_ptr(&cms) as usize);
|
||||
let d = match &self.doc_inputs {
|
||||
Some((_, d)) if (d.patterns, d.symbols, d.images, d.cms) == found => d.clone(),
|
||||
_ => {
|
||||
let doc_art = || doc.patterns.iter().flat_map(|p| &p.art).chain(doc.symbols.iter().map(|s| &s.art));
|
||||
let art = doc_art().map(Arc::downgrade).collect();
|
||||
let pixels = doc.images.values().flat_map(|b| std::iter::once(&b.bytes).chain(&b.proxy)).map(Arc::downgrade).collect();
|
||||
let plugins = doc_art().any(|a| crate::holds_plugin_effect(a));
|
||||
Arc::new(DocInputs { patterns, symbols, images, cms: found.3, plugins, _held: (art, pixels, Arc::downgrade(&cms)) })
|
||||
}
|
||||
};
|
||||
self.doc_inputs = Some((at, d.clone()));
|
||||
d
|
||||
}
|
||||
|
||||
/// Drop the least recently drawn shadow and filtered-content rasters beyond `budget` bytes
|
||||
/// ([`FX_RASTER_BUDGET`]).
|
||||
pub(super) fn trim_fx_rasters(&mut self, budget: usize) {
|
||||
let shadows = self.shadows.iter().map(|(k, e)| (e.stamp, false, *k, e.bytes()));
|
||||
let pixels = self.pixel_fx.iter().map(|(k, e)| (e.stamp, true, *k, e.bytes()));
|
||||
let mut all: Vec<_> = shadows.chain(pixels).collect();
|
||||
let mut total: usize = all.iter().map(|e| e.3).sum();
|
||||
if total <= budget {
|
||||
return;
|
||||
}
|
||||
all.sort_unstable_by_key(|e| e.0);
|
||||
for (_, pixel, key, bytes) in all {
|
||||
if total <= budget {
|
||||
break;
|
||||
}
|
||||
if pixel {
|
||||
self.pixel_fx.remove(&key);
|
||||
} else {
|
||||
self.shadows.remove(&key);
|
||||
}
|
||||
total -= bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// The image Effect › Rasterize `r` makes of the content `paint` draws over `rect` (whole
|
||||
/// pixels of the image): drawn on the image's own grid as Object › Rasterize draws it (with
|
||||
/// hard edges when `r` doesn't anti-alias), then given `r`'s background and color model. `None`
|
||||
/// when it would hold more than [`PIXEL_BUDGET`] pixels or be wider or taller than
|
||||
/// [`crate::MAX_RASTER_SIDE`]. → (pixels, columns, rows)
|
||||
fn rasterize_image(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
r: &effects::Rasterize,
|
||||
rect: Rect,
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) -> Option<(Vec<u8>, usize, usize)> {
|
||||
let line = 72.0 / doc_raster_ppi(f.doc);
|
||||
let (w, h) = image_size(r, rect, line)?;
|
||||
#[cfg(test)]
|
||||
RASTERIZED.with(|n| n.set(n.get() + 1));
|
||||
let cell = r.cell(line);
|
||||
let view = Affine::scale(1.0 / cell) * Affine::translate(-rect.origin().to_vec2());
|
||||
// The effect's own anti-aliasing, whatever the view or the export draws with.
|
||||
let anti_alias = match (r.anti_alias, r.hinted) {
|
||||
(false, _) => crate::AntiAlias::None,
|
||||
(true, true) => crate::AntiAlias::Type,
|
||||
(true, false) => crate::AntiAlias::Art,
|
||||
};
|
||||
let opts = crate::RenderOptions { anti_alias, ..f.opts.clone() };
|
||||
let frame = Frame { mt: false, view, visible: rect, px: cell, opts: &opts, ..*f };
|
||||
let mut off = frame.offscreen_context(w, h);
|
||||
self.inside_layer(|s| paint(s, &mut off, &frame));
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
let mut pixels = pm.data_as_u8_slice().to_vec();
|
||||
r.finish(&mut pixels);
|
||||
Some((pixels, usize::from(w), usize::from(h)))
|
||||
}
|
||||
|
||||
/// Keep `pm`'s pixels only inside `g` filled by `rule` as `frame` draws it (Feather's clip).
|
||||
fn clip_pixels(&mut self, pm: &mut vello_cpu::Pixmap, frame: &Frame, g: &BezPath, rule: FillRule) {
|
||||
let mut m = frame.offscreen_context(pm.width(), pm.height());
|
||||
m.set_transform(frame.view);
|
||||
m.set_fill_rule(fill_rule(rule));
|
||||
m.set_paint(peniko::Color::BLACK);
|
||||
m.fill_path(g);
|
||||
m.flush();
|
||||
let mut mask = vello_cpu::Pixmap::new(pm.width(), pm.height());
|
||||
m.render(&mut mask, &mut self.resources);
|
||||
for (p, c) in pm.data_mut().iter_mut().zip(mask.data()) {
|
||||
let keep = |v: u8| ((v as u32 * c.a as u32 + 127) / 255) as u8;
|
||||
*p = vello_cpu::color::PremulRgba8 { r: keep(p.r), g: keep(p.g), b: keep(p.b), a: keep(p.a) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `draw`, which pushes filter layers. Single-threaded contexts draw directly and `draw`
|
||||
/// applies `composite` (blend mode, opacity) on its filter layer. On a multithreaded context
|
||||
/// the layer is drawn into an offscreen single-threaded context covering `reach` (document
|
||||
/// space) plus the blur's spread, then composited back with `composite`.
|
||||
/// While dragging, that context holds at most [`DRAG_PIXEL_BUDGET`] pixels.
|
||||
pub(super) fn with_filters(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
reach: Rect,
|
||||
blur: f64,
|
||||
composite: Option<(vectorcraft_doc::color::BlendMode, f32)>,
|
||||
draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame, Option<(vectorcraft_doc::color::BlendMode, f32)>),
|
||||
) {
|
||||
if !f.mt {
|
||||
return self.inside_layer(|r| draw(r, ctx, f, composite));
|
||||
}
|
||||
let Some((x0, y0, w, h, k)) = filter_region(f.view.transform_rect_bbox(reach), (ctx.width(), ctx.height()), blur / f.px) else { return };
|
||||
// During a drag, the region holds at most `DRAG_PIXEL_BUDGET` pixels (#2558).
|
||||
let drag = || filter_region_within(f.view.transform_rect_bbox(reach), (ctx.width(), ctx.height()), blur / f.px, DRAG_PIXEL_BUDGET);
|
||||
let (x0, y0, w, h, k) = match f.opts.dragging.then(drag).flatten().filter(|r| r.4 < k) {
|
||||
Some(r) => {
|
||||
self.stats.moved += 1;
|
||||
self.stats.previewed += 1;
|
||||
r
|
||||
}
|
||||
None => (x0, y0, w, h, k),
|
||||
};
|
||||
let mut off = f.offscreen_context(w, h);
|
||||
let shifted = Frame { mt: false, view: Affine::scale(k) * Affine::translate((-x0, -y0)) * f.view, px: f.px / k, ..*f };
|
||||
self.inside_layer(|r| draw(r, &mut off, &shifted, None));
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
let layered = composite.is_some_and(|(m, o)| m != vectorcraft_doc::color::BlendMode::Normal || o < 1.0);
|
||||
if let Some((m, o)) = composite.filter(|_| layered) {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(blend_mode(m)), Some(o), None, None);
|
||||
}
|
||||
if k < 1.0 {
|
||||
draw_scaled(ctx, std::sync::Arc::new(pm), (x0, y0), k);
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
} else {
|
||||
crate::group::draw_pixmap(ctx, std::sync::Arc::new(pm), (x0, y0));
|
||||
}
|
||||
if layered {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint the fills and strokes of `n` on geometry `g`, applying per-item geometry effects and
|
||||
/// painting per-item raster effects around that item alone.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn paint_items(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
n: &Node,
|
||||
g: &BezPath,
|
||||
rule: FillRule,
|
||||
gctx: &GeomContext,
|
||||
mut cache: Option<&mut Vec<Option<Arc<BezPath>>>>,
|
||||
) {
|
||||
let bounds = g.bounding_box();
|
||||
for (j, item) in n.appearance.items.iter().enumerate() {
|
||||
if !item.visible() || item.paint().is_none() {
|
||||
continue;
|
||||
}
|
||||
// The item's own geometry effects on `g`, from the cache when there is one.
|
||||
let slot = cache.as_deref_mut().and_then(|c| c.get_mut(j));
|
||||
let ig: Arc<BezPath> = match slot {
|
||||
Some(Some(ig)) => ig.clone(),
|
||||
Some(s) => s.insert(Arc::new(apply(item_effects(item), g, &gctx.item(item)))).clone(),
|
||||
None => Arc::new(apply(item_effects(item), g, &gctx.item(item))),
|
||||
};
|
||||
let ig = &*ig;
|
||||
let ib = if effects::has_geometry(item_effects(item)) && !ig.elements().is_empty() { ig.bounding_box() } else { bounds };
|
||||
let rfx = effects::raster_effects(item_effects(item));
|
||||
if rfx.is_empty() {
|
||||
self.paint_item(ctx, f, n, item, ig, rule, ib);
|
||||
continue;
|
||||
}
|
||||
// The item's silhouette: its fill area, or the outline of its stroke.
|
||||
let outline = match item {
|
||||
AppearanceItem::Fill(_) => ig.clone(),
|
||||
AppearanceItem::Stroke(st) => {
|
||||
let w = effects::stroke::aligned_width(st, effects::stroke::is_closed(ig));
|
||||
effects::stroke::line_outline(&effects::stroke::stroke_pieces(ig, st).line, st, w, f.px * 0.25)
|
||||
}
|
||||
};
|
||||
let outset = n.appearance.outset();
|
||||
let orule = if item.is_fill() { rule } else { FillRule::NonZero };
|
||||
// Each item's shadows get their own cache slots.
|
||||
// Rasterize makes its image over the item's silhouette.
|
||||
let bounds = rfx.iter().any(|fx| matches!(fx, RasterFx::Rasterize(_))).then(|| outline.bounding_box());
|
||||
let content = Content {
|
||||
node: n,
|
||||
owner: None,
|
||||
cache: Some((j + 1) << 8),
|
||||
reach: ib.inflate(outset, outset),
|
||||
outline: Some((&outline, orule)),
|
||||
bounds,
|
||||
kept: None,
|
||||
};
|
||||
self.raster_fx(ctx, f, &content, &rfx, &mut |r, c, fr| r.paint_item(c, fr, n, item, ig, rule, ib));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint one fill or stroke of `n` on its geometry `ig` (bounds `ib`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_item(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, item: &AppearanceItem, ig: &BezPath, rule: FillRule, ib: Rect) {
|
||||
match item {
|
||||
AppearanceItem::Fill(fl) => {
|
||||
let layered = fl.opacity < 1.0 || fl.blend != vectorcraft_doc::color::BlendMode::Normal;
|
||||
if layered {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(blend_mode(fl.blend)), Some(fl.opacity), None, None);
|
||||
}
|
||||
ctx.set_transform(f.view);
|
||||
if paint::set_paint(ctx, &fl.paint, ib, f) {
|
||||
ctx.set_fill_rule(fill_rule(rule));
|
||||
ctx.fill_path(ig);
|
||||
}
|
||||
if layered {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
AppearanceItem::Stroke(st) => {
|
||||
if st.width <= 0.0 {
|
||||
return;
|
||||
}
|
||||
// Brushed strokes keep their brush art under effects (e.g. a glowing scatter brush).
|
||||
if st.brush.is_some() && self.draw_brush(ctx, f, n, ig, st) {
|
||||
return;
|
||||
}
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, ig, rule, st, None, ib);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use super::bounds::drawn_bounds;
|
||||
use super::*;
|
||||
|
||||
fn visible(effects: &[Effect]) -> bool {
|
||||
effects.iter().any(|e| e.visible)
|
||||
}
|
||||
|
||||
/// Does this node need the effects path (a path or compound path with visible effects)?
|
||||
pub(crate) fn has_fx(n: &Node) -> bool {
|
||||
if !matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. }) {
|
||||
return false;
|
||||
}
|
||||
visible(&n.appearance.effects)
|
||||
|| n.appearance.items.iter().any(|i| match i {
|
||||
AppearanceItem::Fill(f) => visible(&f.effects),
|
||||
AppearanceItem::Stroke(s) => visible(&s.effects),
|
||||
})
|
||||
}
|
||||
|
||||
/// Does `n` carry visible object-level effects that only apply through its art (type, images,
|
||||
/// symbol instances, live objects; Crop Marks and colour adjustments on anything), is it type with
|
||||
/// a geometry effect on a fill or stroke of its own or an image with a fill or stroke of its own
|
||||
/// that paints a shape ([`effects::has_item_geometry`]), is it a path whose Transform effect copies
|
||||
/// it with a gradient or changes its fills and strokes ([`effects::has_transform_art`]), or is it a
|
||||
/// group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])?
|
||||
pub(crate) fn has_object_fx(n: &Node) -> bool {
|
||||
(effects::needs_outline(n) && (visible(&n.appearance.effects) || n.projection().is_some()))
|
||||
|| effects::has_item_geometry(n)
|
||||
|| effects::has_transform_art(n)
|
||||
|| effects::has_container_appearance(n)
|
||||
|| effects::has_crop_marks(n)
|
||||
|| effects::has_adjustment(n)
|
||||
|| effects::has_revolve(n)
|
||||
|| effects::has_solid(n)
|
||||
}
|
||||
|
||||
/// Is `n` a group or layer (whose evaluated art keeps its knockout setting)?
|
||||
pub(super) fn is_container(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. })
|
||||
}
|
||||
|
||||
/// Can the art [`Renderer::fx_art`] makes for `n` be made from live objects it evaluates? It can
|
||||
/// when `n` is a live object, and when `n` is a group or layer without a Pathfinder effect that
|
||||
/// holds live objects and whose geometry effects reshape them or whose own fills and strokes paint
|
||||
/// their evaluated outlines ([`effects::evaluate_container`]).
|
||||
pub(super) fn evaluates_live(n: &Node) -> bool {
|
||||
use vectorcraft_doc::live::{is_live, subtree_has_live};
|
||||
let to_members = effects::has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(effects::paints);
|
||||
is_live(n) || (is_container(n) && to_members && !effects::has_pathfinder(n) && subtree_has_live(n))
|
||||
}
|
||||
|
||||
/// Can the art [`Renderer::fx_art`] makes for `n` change while `n` stays the same, other than
|
||||
/// through live objects it evaluates? It can when `n` holds type (laid out in the fonts installed),
|
||||
/// a symbol instance (made from its symbol's art), or an object with a 3D effect or a color
|
||||
/// adjustment (shaded or recolored through the color settings), in it or in the opacity masks of
|
||||
/// the objects in it.
|
||||
pub(super) fn reads_outside(n: &Node) -> bool {
|
||||
let mut reads = false;
|
||||
n.walk(&mut |c| {
|
||||
reads |= matches!(c.kind, NodeKind::Text(_) | NodeKind::SymbolInstance { .. })
|
||||
|| effects::has_revolve(c)
|
||||
|| effects::has_solid(c)
|
||||
|| effects::has_adjustment(c)
|
||||
|| c.extras().mask.as_ref().is_some_and(|m| reads_outside(&m.art))
|
||||
});
|
||||
reads
|
||||
}
|
||||
|
||||
/// The [`Content::bounds`] of `n` under raster effects `rfx`: its [`drawn_bounds`] when `rfx`
|
||||
/// holds a Rasterize.
|
||||
pub(super) fn rasterize_bounds(symbols: &SymbolBoxes, n: &Node, rfx: &[RasterFx]) -> Option<Rect> {
|
||||
rfx.iter().any(|fx| matches!(fx, RasterFx::Rasterize(_))).then(|| drawn_bounds(symbols, n)).flatten()
|
||||
}
|
||||
|
||||
/// `f` with the content moved by (`dx`, `dy`) document units (a shadow's offset).
|
||||
pub(super) fn shifted<'a>(f: &Frame<'a>, dx: f64, dy: f64) -> Frame<'a> {
|
||||
Frame { view: f.view * Affine::translate((dx, dy)), visible: f.visible - Vec2::new(dx, dy), ..*f }
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use super::entries::{Drawn, Inputs, Made, raster_bytes};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn shadow_filter(dx: f64, dy: f64, blur: f64, color: peniko::Color) -> Filter {
|
||||
Filter::from_primitive(FilterPrimitive::DropShadowOnly {
|
||||
dx: dx as f32,
|
||||
dy: dy as f32,
|
||||
std_deviation: (blur / 2.0).max(0.0) as f32,
|
||||
color,
|
||||
edge_mode: EdgeMode::None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The layer filter of a drop shadow with Darkness: the object's own pixels blurred.
|
||||
pub(super) fn shade_filter(blur: f64) -> Filter {
|
||||
Filter::from_primitive(FilterPrimitive::GaussianBlur { std_deviation: (blur / 2.0).max(0.0) as f32, edge_mode: EdgeMode::None })
|
||||
}
|
||||
|
||||
/// How much darker (0..1) a drop shadow with Darkness `darkness` makes the values `ink` paints:
|
||||
/// black added leaves the cyan, magenta and yellow plane of a CMYK document as it is and scales
|
||||
/// the complemented black plane, as it scales screen colors, by 1 − `darkness`.
|
||||
pub(super) fn shade(ink: Ink, darkness: f32) -> f32 {
|
||||
match ink {
|
||||
Ink::Cmy => 0.0,
|
||||
Ink::K | Ink::Display | Ink::RichBlack => darkness,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a shadow or glow raster is made from ([`Made::pixels`]): the silhouette's alpha, or its
|
||||
/// premultiplied RGBA when the shadow takes the object's own colors (Darkness).
|
||||
pub(super) fn silhouette(pm: &vello_cpu::Pixmap, colored: bool) -> Vec<u8> {
|
||||
if colored { pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect() } else { pm.data().iter().map(|p| p.a).collect() }
|
||||
}
|
||||
|
||||
pub(super) fn pcolor(ink: Ink, c: &vectorcraft_doc::color::Color) -> peniko::Color {
|
||||
let [r, g, b] = ink.rgb(c);
|
||||
peniko::Color::new([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
/// Most pixels the raster of [`Renderer::pixel_content`] holds (a 4K screen's): larger content is
|
||||
/// cropped to the view when its effects only look nearby, else rendered at a lower resolution.
|
||||
pub(super) const PIXEL_BUDGET: f64 = 8.4e6;
|
||||
|
||||
/// Most Rasterize effects of one stack that [`Renderer::raster_fx`] draws (the last ones): each
|
||||
/// draws the content again inside the one after it.
|
||||
pub(super) const MAX_RASTERIZE: usize = 16;
|
||||
|
||||
/// The columns and rows of the image Rasterize `r` makes over `rect` (whole pixels of the image,
|
||||
/// `line` points wide at the document's raster effects resolution). `None` when the image would
|
||||
/// hold more than [`PIXEL_BUDGET`] pixels or be wider or taller than [`crate::MAX_RASTER_SIDE`].
|
||||
pub(super) fn image_size(r: &effects::Rasterize, rect: Rect, line: f64) -> Option<(u16, u16)> {
|
||||
let cell = r.cell(line);
|
||||
let (cols, rows) = ((rect.width() / cell).round(), (rect.height() / cell).round());
|
||||
let side = f64::from(crate::MAX_RASTER_SIDE);
|
||||
(cols >= 1.0 && rows >= 1.0 && cols <= side && rows <= side && cols * rows <= PIXEL_BUDGET).then_some((cols as u16, rows as u16))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// How many times a Rasterize has drawn the content before it on this thread.
|
||||
pub(crate) static RASTERIZED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Document Raster Effects Settings › Resolution of `doc`, in ppi (1 to 2400; 72 when it isn't a
|
||||
/// number).
|
||||
pub(super) fn doc_raster_ppi(doc: &Document) -> f64 {
|
||||
doc.raster_ppi()
|
||||
}
|
||||
|
||||
/// The effects [`Renderer::pixel_content`] runs on the content's pixels (those changing the
|
||||
/// content itself), in stack order.
|
||||
pub(super) fn content_changing(fx: &&RasterFx) -> bool {
|
||||
matches!(fx, RasterFx::Feather { .. } | RasterFx::GaussianBlur { .. } | RasterFx::Pixel(_) | RasterFx::Rasterize(_))
|
||||
}
|
||||
|
||||
/// Is the content drawn with hard edges for `chain` into a raster of pixels `px` wide (document
|
||||
/// units): does it go first through a Rasterize with anti-aliasing off whose pixels are no
|
||||
/// smaller? (Smaller ones show as the share of each raster pixel they cover, as smooth edges do.)
|
||||
pub(super) fn hard_edged(chain: &[&RasterFx], px: f64) -> bool {
|
||||
matches!(chain.first(), Some(RasterFx::Rasterize(r)) if !r.anti_alias && r.cell(72.0) >= px)
|
||||
}
|
||||
|
||||
/// How far (document units) from a pixel the content deciding it after `fx` lies; `None` when
|
||||
/// anywhere in the object.
|
||||
pub(super) fn looks_around(fx: &RasterFx) -> Option<f64> {
|
||||
match fx {
|
||||
RasterFx::Feather { radius } | RasterFx::GaussianBlur { radius } => Some(1.5 * radius),
|
||||
RasterFx::Pixel(p) => p.reach(),
|
||||
RasterFx::Rasterize(r) => Some(r.reach()),
|
||||
_ => Some(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of an object with Photoshop-style effects as filtered pixels, drawn at
|
||||
/// (`anchor`'s view position + `rel`) scaled by 1 / `k` (see [`Renderer::pixel_content`]).
|
||||
pub(crate) struct PixelEntry {
|
||||
pub(super) node: Drawn,
|
||||
pub(super) linear: [u64; 4],
|
||||
pub(super) anchor: vectorcraft_geom::Point,
|
||||
pub(super) rel: (f64, f64),
|
||||
pub(super) k: f64,
|
||||
pub(super) image: std::sync::Arc<vello_cpu::Pixmap>,
|
||||
/// For a raster of the content's whole reach at full size, `None`: it draws after a pan too.
|
||||
/// For one cropped to the view, or made at a lower resolution, the view and the rect it was
|
||||
/// made for: it draws only there, so dragging other art over the object keeps it (a wide blur,
|
||||
/// or any blur zoomed in, reaches past the pixel budget).
|
||||
pub(super) view: Option<ViewCrop>,
|
||||
pub(super) made: Made,
|
||||
/// What the content read when the raster was made, with the channels the effects ran on and,
|
||||
/// under De-Interlace, the raster resolution.
|
||||
pub(super) inputs: Inputs,
|
||||
/// Made for the object before it moved ([`Renderer::move_pixel_content`]), or made over
|
||||
/// rasters made before their objects moved: only a frame rendered while interacting draws
|
||||
/// it.
|
||||
pub(super) moved: bool,
|
||||
/// How many images in the content failed to decode while the raster was made
|
||||
/// ([`crate::paint::decode_failures`]): each frame that draws the raster counts them
|
||||
/// again ([`crate::paint::add_decode_failures`]).
|
||||
pub(super) failed: u64,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
|
||||
impl PixelEntry {
|
||||
/// The bytes it holds, for [`FX_RASTER_BUDGET`].
|
||||
pub(crate) fn bytes(&self) -> usize {
|
||||
raster_bytes(&self.image) + self.made.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
/// The four linear coefficients of `view` (zoom and rotation, not the pan), as cache keys.
|
||||
pub(super) fn linear_key(view: Affine) -> [u64; 4] {
|
||||
let c = view.as_coeffs();
|
||||
[c[0].to_bits(), c[1].to_bits(), c[2].to_bits(), c[3].to_bits()]
|
||||
}
|
||||
|
||||
/// The pan of the view a raster was cropped for, or made at a lower resolution than the view's, and
|
||||
/// the device-pixel rect it was made for in that view: the frame's, or around it the margin of
|
||||
/// [`pan_margin`] ([`PixelEntry::view`], [`ShadowEntry::view`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct ViewCrop {
|
||||
pan: Vec2,
|
||||
screen: Rect,
|
||||
}
|
||||
|
||||
impl ViewCrop {
|
||||
pub(super) fn of(view: Affine, screen: Rect) -> Self {
|
||||
Self { pan: view.translation(), screen }
|
||||
}
|
||||
|
||||
/// Does the raster draw in a frame through `view` (of the same zoom and rotation) onto
|
||||
/// `screen`? When the view is panned by whole pixels and the frame's rect, panned back, lies
|
||||
/// within the one the raster was made for: the same pixels in the same places, as when a frame
|
||||
/// is drawn again only in a part of it.
|
||||
pub(super) fn serves(&self, view: Affine, screen: Rect) -> bool {
|
||||
let d = view.translation() - self.pan;
|
||||
let whole = Vec2::new(d.x.round(), d.y.round());
|
||||
let s = screen - whole;
|
||||
(d - whole).hypot() < 1e-6 && s.x0 >= self.screen.x0 && s.y0 >= self.screen.y0 && s.x1 <= self.screen.x1 && s.y1 <= self.screen.y1
|
||||
}
|
||||
|
||||
/// The rect the raster was made for, in the device pixels of a frame through `view` that it
|
||||
/// serves ([`Self::serves`]).
|
||||
pub(super) fn served(&self, view: Affine) -> Rect {
|
||||
let d = view.translation() - self.pan;
|
||||
self.screen + Vec2::new(d.x.round(), d.y.round())
|
||||
}
|
||||
}
|
||||
|
||||
/// How far (device pixels, on each side) past a `w` × `h` view a raster cropped to it, and `m`
|
||||
/// pixels around that, is made when it is made again at the same zoom and rotation, so that later
|
||||
/// pans of up to that many pixels draw it: the most that keeps the raster within `budget` pixels and
|
||||
/// `side` pixels a side, with two pixels to spare for rounding, and at most half the view's longer
|
||||
/// side. 0 when the view and the `m` pixels around it alone leave no room.
|
||||
pub(super) fn pan_margin((w, h): (f64, f64), m: f64, budget: f64, side: f64) -> f64 {
|
||||
let (a, b) = (w + 2.0 * m, h + 2.0 * m);
|
||||
// The largest `e` with (a + 2e) × (b + 2e) within the budget, less two pixels for the rounding
|
||||
// of the raster's edges to whole pixels.
|
||||
let e = (((a - b).powi(2) + 4.0 * budget).sqrt() - (a + b)) / 4.0 - 2.0;
|
||||
// (Also for a view or spread that isn't a number.)
|
||||
if !e.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
e.min((side - a.max(b)) / 2.0 - 2.0).min(w.max(h) / 2.0).floor().max(0.0)
|
||||
}
|
||||
|
||||
/// Most pixels a raster of [`Renderer::pixel_content`] or [`Renderer::with_filters`] holds while
|
||||
/// [`crate::RenderOptions::dragging`] (a 1000 × 1000 raster's), when the frame does not draw a raster
|
||||
/// kept from an earlier frame instead (#2558).
|
||||
pub(super) const DRAG_PIXEL_BUDGET: f64 = 1.0e6;
|
||||
|
||||
/// The scale (raster pixels a device pixel) for content of `rw` × `rh` device pixels that would
|
||||
/// render at scale `k`, when a drag lowers it to stay within [`DRAG_PIXEL_BUDGET`]; `None` when it
|
||||
/// does not (`dragging` off, or content within the budget at `k`).
|
||||
pub(super) fn drag_scale(dragging: bool, (rw, rh): (f64, f64), k: f64) -> Option<f64> {
|
||||
let d = (DRAG_PIXEL_BUDGET / (rw * rh)).sqrt();
|
||||
(dragging && d < k).then_some(d)
|
||||
}
|
||||
|
||||
/// The raster a shadow or glow blurred by `blur` device pixels is filtered in, for content
|
||||
/// reaching `reach` (device pixels) in a view of `screen` pixels: (left, top, width, height,
|
||||
/// scale). It covers what of the reach can show in the view, the blur's spread around it
|
||||
/// included, at `scale` raster pixels a device pixel: 1, or less to stay within
|
||||
/// [`PIXEL_BUDGET`]. At a high zoom the spread of a wide blur is tens of thousands of pixels a
|
||||
/// side, whatever the size of the view (#1706). `None`: nothing to draw.
|
||||
pub(super) fn filter_region(reach: Rect, screen: (u16, u16), blur: f64) -> Option<(f64, f64, u16, u16, f64)> {
|
||||
filter_region_within(reach, screen, blur, PIXEL_BUDGET)
|
||||
}
|
||||
|
||||
/// The region [`filter_region`] returns, within `budget` pixels instead of [`PIXEL_BUDGET`].
|
||||
pub(super) fn filter_region_within(reach: Rect, screen: (u16, u16), blur: f64, budget: f64) -> Option<(f64, f64, u16, u16, f64)> {
|
||||
// 3σ of the Gaussian (σ = blur / 2) in pixels, plus a pixel of antialiasing.
|
||||
let spread = blur.max(0.0) * 1.5 + 2.0;
|
||||
let screen = Rect::new(0.0, 0.0, f64::from(screen.0), f64::from(screen.1)).inflate(spread, spread);
|
||||
let r = reach.inflate(spread, spread).intersect(screen);
|
||||
let (x0, y0) = (r.x0.floor(), r.y0.floor());
|
||||
let (rw, rh) = (r.x1.ceil() - x0, r.y1.ceil() - y0);
|
||||
// (Also false for a reach or a blur that isn't a number.)
|
||||
if !(rw >= 1.0 && rh >= 1.0 && rw.is_finite() && rh.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let side = f64::from(u16::MAX);
|
||||
let k = (budget / (rw * rh)).sqrt().min(side / rw).min(side / rh).min(1.0);
|
||||
let (w, h) = ((rw * k).floor().clamp(1.0, side), (rh * k).floor().clamp(1.0, side));
|
||||
Some((x0, y0, w as u16, h as u16, k))
|
||||
}
|
||||
|
||||
/// Draw `image` with its top-left at (`x`, `y`) device pixels, `1 / k` device pixels a pixel.
|
||||
pub(super) fn draw_scaled(ctx: &mut RenderContext, image: std::sync::Arc<vello_cpu::Pixmap>, (x, y): (f64, f64), k: f64) {
|
||||
let (w, h) = (image.width() as f64, image.height() as f64);
|
||||
ctx.set_transform(Affine::translate((x, y)) * Affine::scale(1.0 / k));
|
||||
// On whole pixels at full size the cheapest sampler is exact.
|
||||
let quality = if k < 1.0 { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low };
|
||||
let sampler = peniko::ImageSampler { quality, ..Default::default() };
|
||||
// The raster maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern,
|
||||
// radial or freeform gradient) would resample it (#1536).
|
||||
ctx.reset_paint_transform();
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
|
||||
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
|
||||
/// Draw `image` with its pixels placed by `xf` (pixels to device pixels). On whole pixels at full
|
||||
/// size it is drawn as [`draw_scaled`] draws it.
|
||||
pub(super) fn draw_placed(ctx: &mut RenderContext, image: std::sync::Arc<vello_cpu::Pixmap>, xf: Affine) {
|
||||
let [a, b, c, d, e, f] = xf.as_coeffs();
|
||||
let near = |v: f64, to: f64| (v - to).abs() < 1e-6;
|
||||
if near(a, 1.0) && near(b, 0.0) && near(c, 0.0) && near(d, 1.0) && near(e, e.round()) && near(f, f.round()) {
|
||||
return draw_scaled(ctx, image, (e.round(), f.round()), 1.0);
|
||||
}
|
||||
let (w, h) = (image.width() as f64, image.height() as f64);
|
||||
ctx.set_transform(xf);
|
||||
let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Medium, ..Default::default() };
|
||||
ctx.reset_paint_transform();
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
|
||||
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
use super::bounds::{cull_bounds, effected_path};
|
||||
use super::entries::{Content, Drawn, FX_RASTER_BUDGET, FxGeom, Inputs, Kept, Made, Reads, Shadow, ShadowEntry};
|
||||
use super::predicates::{evaluates_live, is_container, rasterize_bounds, reads_outside, shifted};
|
||||
use super::raster::{
|
||||
DRAG_PIXEL_BUDGET, MAX_RASTERIZE, PIXEL_BUDGET, ViewCrop, content_changing, doc_raster_ppi, image_size, linear_key, pan_margin, pcolor, shade,
|
||||
shade_filter, shadow_filter, silhouette,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
impl Renderer {
|
||||
/// Drop shadow / outer glow `fx` (shadow-cache slot `i` of `n`) as a cached raster: the
|
||||
/// silhouette `paint` draws is rendered once into a small crop, blurred and tinted on the CPU,
|
||||
/// and reused while the object, the zoom/rotation and what the silhouette reads ([`Inputs`]) are
|
||||
/// unchanged (pans only move it), and while interacting after the object only moved
|
||||
/// ([`Self::move_shadow`]). A raster of the whole reach too large for the budget is cropped to
|
||||
/// what can show in the `screen` (device pixels): the view, and around it the blur's spread and
|
||||
/// `around` (document units: how far the shadows and glows stacked over this one look), made
|
||||
/// coarser to fit the budget where the blur hides the coarser grid. Such a raster, and one
|
||||
/// made over it, is reused while a pan keeps the view inside the rect it was made for: the
|
||||
/// view, and when the raster kept for the object was made at the same zoom and rotation and no
|
||||
/// drag is on, the view with [`pan_margin`] around it, within `over` (the rect, in device
|
||||
/// pixels, that the cropped rasters of the shadows and glows it is made over were made for;
|
||||
/// `None` when it is made over none). `None` when the effect isn't a shadow/glow or the raster
|
||||
/// would still be too large (caller falls back to [`Self::draw_shadow_filter`]).
|
||||
/// While dragging on a multithreaded context, a raster that has to be made again counts as too
|
||||
/// large when it would hold more than [`DRAG_PIXEL_BUDGET`] pixels.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn shadow_raster(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
n: &Node,
|
||||
i: usize,
|
||||
fx: &RasterFx,
|
||||
reach: Rect,
|
||||
owner: Option<&Arc<Node>>,
|
||||
(screen, around, over): (Rect, f64, Option<Rect>),
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) -> Option<Shadow> {
|
||||
let (mode, opacity, dx, dy, blur, color, darkness) = match fx {
|
||||
RasterFx::DropShadow { mode, opacity, dx, dy, blur, color, darkness } => (*mode, *opacity, *dx, *dy, *blur, *color, *darkness),
|
||||
RasterFx::OuterGlow { mode, opacity, blur, color } => (*mode, *opacity, 0.0, 0.0, *blur, *color, None),
|
||||
_ => return None,
|
||||
};
|
||||
let linear = linear_key(f.view);
|
||||
let key = (n as *const Node as usize, i, f.ink);
|
||||
let stamp = self.stamp;
|
||||
let interacting = f.opts.interacting;
|
||||
// What the silhouette reads: as the raster kept for `n` found it, else found now.
|
||||
let reads = self.shadows.get(&key).filter(|e| e.node.is(n)).map_or_else(|| Reads::of(n), |e| e.inputs.reads);
|
||||
let inputs = self.raster_inputs(f, n, reads);
|
||||
let exact = |e: &ShadowEntry| {
|
||||
e.linear == linear && e.node.is(n) && e.inputs == inputs && (interacting || !e.moved) && e.view.is_none_or(|v| v.serves(f.view, screen))
|
||||
};
|
||||
if interacting && !self.shadows.get(&key).is_some_and(exact) {
|
||||
self.move_shadow(f, n, key, fx, reach + Vec2::new(dx, dy), owner, &inputs, paint);
|
||||
}
|
||||
let hit = self.shadows.get_mut(&key).filter(|e| exact(e));
|
||||
let (image, x, y, scale, moved, cropped) = match hit {
|
||||
Some(e) => {
|
||||
e.stamp = stamp;
|
||||
self.stats.moved += usize::from(e.moved);
|
||||
let p = f.view * e.anchor;
|
||||
(e.image.clone(), (p.x + e.rel.0).round(), (p.y + e.rel.1).round(), e.scale, e.moved, e.view.map(|v| v.served(f.view)))
|
||||
}
|
||||
None => {
|
||||
// An Outer Glow's silhouette grows by a quarter of a pixel of the document's raster grid first.
|
||||
let grow = if matches!(fx, RasterFx::OuterGlow { .. }) { 18.0 / doc_raster_ppi(f.doc) } else { 0.0 };
|
||||
let spread = (blur.max(0.0) * 1.5 + grow) / f.px + 2.0;
|
||||
let reach = reach + Vec2::new(dx, dy);
|
||||
let whole = f.view.transform_rect_bbox(reach).inflate(spread, spread);
|
||||
// The raster's region and scale within `budget` pixels: `None` when over the limits.
|
||||
let region = |r: Rect, coarser: bool, budget: f64| {
|
||||
let (x0, y0) = (r.x0.floor(), r.y0.floor());
|
||||
let (w, h) = (r.x1.ceil() - x0, r.y1.ceil() - y0);
|
||||
let scale = if coarser { (w * h / budget).sqrt().ceil().max(1.0) } else { 1.0 };
|
||||
if !scale.is_finite() || scale > 1.0 && blur / 2.0 / f.px / scale < 2.0 {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = ((w / scale).ceil(), (h / scale).ceil());
|
||||
((1.0..=8192.0).contains(&w) && (1.0..=8192.0).contains(&h) && w * h <= budget).then_some((x0, y0, w, h, scale))
|
||||
};
|
||||
// A glow too large for the budget is made coarser, where its blur hides the coarser grid: the filter
|
||||
// layer the caller falls back to can't make its silhouette solid.
|
||||
let full = region(whole, matches!(fx, RasterFx::OuterGlow { .. }), 4.0e6).filter(|_| over.is_none());
|
||||
// Else it is cropped to what can show in the view, within the budget of the filter
|
||||
// layer it would otherwise be drawn through, and made coarser in the same way. Made
|
||||
// again at this zoom and rotation (after a pan), it covers a margin around the view
|
||||
// too, so that later pans within the margin draw it.
|
||||
let ((x0, y0, w, h, scale), cropped) = match full {
|
||||
Some(r) => (r, None),
|
||||
None => {
|
||||
let m = spread + around / f.px;
|
||||
let again = self.shadows.get(&key).is_some_and(|e| e.linear == linear) && !f.opts.dragging;
|
||||
let e = if again { pan_margin((screen.width(), screen.height()), m, PIXEL_BUDGET, 8192.0) } else { 0.0 };
|
||||
let room = over.map_or(screen.inflate(e, e), |o| screen.inflate(e, e).intersect(o));
|
||||
let r = whole.intersect(room.inflate(m, m));
|
||||
if r.width() <= 0.0 || r.height() <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
(region(r, true, PIXEL_BUDGET)?, Some(room))
|
||||
}
|
||||
};
|
||||
// During a drag, a larger shadow on a multithreaded context is filtered within the
|
||||
// drag's budget (`with_filters`), and no raster is kept.
|
||||
if f.opts.dragging && f.mt && w * h > DRAG_PIXEL_BUDGET {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = (w as u16, h as u16);
|
||||
// Silhouette (the object's painted alpha, and its colors under Darkness), offset by
|
||||
// the shadow distance.
|
||||
let mut off = f.offscreen_context(w, h);
|
||||
let view = Affine::scale(1.0 / scale) * Affine::translate((-x0, -y0)) * f.view;
|
||||
// Art is culled against the raster's own area: members of a group outside the view cast their shadows in it.
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let crop = Frame { mt: false, view, visible, ..*f };
|
||||
let before = self.stats.moved;
|
||||
self.inside_layer(|r| paint(r, &mut off, &shifted(&crop, dx, dy)));
|
||||
// A silhouette drawn over moved rasters makes a moved shadow.
|
||||
let moved = self.stats.moved > before;
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
// What the raster is made from, for its object once it moves (`move_shadow`).
|
||||
let made =
|
||||
Made { pixels: silhouette(&pm, darkness.is_some()), fx: vec![fx.clone()], size: reach.size(), channels: None, outline: None };
|
||||
let (wu, hu) = (w as usize, h as usize);
|
||||
let sigma = blur / 2.0 / f.px / scale;
|
||||
if let Some(darkness) = darkness {
|
||||
// The object's own colors, blurred, with the black Darkness adds (premultiplied).
|
||||
let keep = 1.0 - shade(f.ink, darkness);
|
||||
let mut planes: [Vec<f32>; 4] = [
|
||||
pm.data().iter().map(|p| p.r as f32).collect(),
|
||||
pm.data().iter().map(|p| p.g as f32).collect(),
|
||||
pm.data().iter().map(|p| p.b as f32).collect(),
|
||||
pm.data().iter().map(|p| p.a as f32).collect(),
|
||||
];
|
||||
for plane in &mut planes {
|
||||
effects::pixel::blur_plane(plane, wu, hu, sigma);
|
||||
}
|
||||
let [r, g, b, a] = &planes;
|
||||
for (k, px) in pm.data_mut().iter_mut().enumerate() {
|
||||
let al = (a[k] * opacity).clamp(0.0, 255.0).round();
|
||||
let value = |plane: &Vec<f32>| (plane[k] * opacity * keep).round().clamp(0.0, al) as u8;
|
||||
*px = vello_cpu::color::PremulRgba8 { r: value(r), g: value(g), b: value(b), a: al as u8 };
|
||||
}
|
||||
} else {
|
||||
// An Outer Glow blurs a solid silhouette: wherever the art has any alpha, as AI files draw a glow round a glow.
|
||||
let solid = matches!(fx, RasterFx::OuterGlow { .. });
|
||||
let mut a: Vec<f32> = pm.data().iter().map(|p| if solid && p.a > 0 { 255.0 } else { p.a as f32 }).collect();
|
||||
effects::pixel::dilate_plane(&mut a, wu, hu, grow / f.px / scale);
|
||||
effects::pixel::blur_plane(&mut a, wu, hu, sigma);
|
||||
let [cr, cg, cb] = f.ink.rgb(&color);
|
||||
let tint = |v: f32, ch: f32| (v * ch).round().clamp(0.0, 255.0) as u8;
|
||||
for (px, av) in pm.data_mut().iter_mut().zip(&a) {
|
||||
let al = (av * opacity).clamp(0.0, 255.0);
|
||||
*px = vello_cpu::color::PremulRgba8 { r: tint(al, cr), g: tint(al, cg), b: tint(al, cb), a: al.round() as u8 };
|
||||
}
|
||||
}
|
||||
let image = std::sync::Arc::new(pm);
|
||||
let anchor = reach.origin();
|
||||
let p = f.view * anchor;
|
||||
let node = Drawn::of(n, owner);
|
||||
let rel = (x0 - p.x, y0 - p.y);
|
||||
let view = cropped.map(|room| ViewCrop::of(f.view, room));
|
||||
self.shadows.insert(key, ShadowEntry { node, linear, anchor, rel, image: image.clone(), scale, view, made, inputs, moved, stamp });
|
||||
self.trim_fx_rasters(FX_RASTER_BUDGET);
|
||||
(image, x0, y0, scale, moved, cropped)
|
||||
}
|
||||
};
|
||||
Some(Shadow { image, at: Affine::translate((x, y)) * Affine::scale(scale), mode, moved, cropped })
|
||||
}
|
||||
|
||||
/// Draw shadow raster `s` through `to`, from the view the raster was made for to the one `ctx`
|
||||
/// draws in. The blend mode applies per draw: a blend layer would be a full-viewport compositing
|
||||
/// pass per shadow. A full-size raster drawn as made sits on whole pixels, so the cheapest
|
||||
/// sampler is exact.
|
||||
fn draw_shadow(&mut self, ctx: &mut RenderContext, s: &Shadow, to: Affine) {
|
||||
let (w, h) = (s.image.width() as f64, s.image.height() as f64);
|
||||
let at = to * s.at;
|
||||
ctx.set_transform(at);
|
||||
ctx.set_blend_mode(blend_mode(s.mode));
|
||||
let [a, b, c, d, _, _] = at.as_coeffs();
|
||||
let exact = (a, b, c, d) == (1.0, 0.0, 0.0, 1.0) && at.translation().x.fract() == 0.0 && at.translation().y.fract() == 0.0;
|
||||
let quality = if exact { peniko::ImageQuality::Low } else { peniko::ImageQuality::Medium };
|
||||
let sampler = peniko::ImageSampler { quality, ..Default::default() };
|
||||
ctx.reset_paint_transform();
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(s.image.clone()), sampler });
|
||||
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
|
||||
ctx.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
|
||||
/// Drop shadow / outer glow `fx` of what `paint` draws (bounded by `reach`) through a vello
|
||||
/// filter layer, when there is no raster for it ([`Self::shadow_raster`]). vello's shadow filter
|
||||
/// blurs the soft alpha, so an Outer Glow drawn here lacks the raster's solid silhouette.
|
||||
fn draw_shadow_filter(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
fx: &RasterFx,
|
||||
reach: Rect,
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
let (mode, opacity, dx, dy, blur, color, darkness) = match fx {
|
||||
RasterFx::DropShadow { mode, opacity, dx, dy, blur, color, darkness } => (*mode, *opacity, *dx, *dy, *blur, *color, *darkness),
|
||||
RasterFx::OuterGlow { mode, opacity, blur, color } => (*mode, *opacity, 0.0, 0.0, *blur, *color, None),
|
||||
_ => return,
|
||||
};
|
||||
let filter = match darkness {
|
||||
Some(_) => shade_filter(blur),
|
||||
None => shadow_filter(0.0, 0.0, blur, pcolor(f.ink, &color)),
|
||||
};
|
||||
// The offset moves the content rather than the filter: vello_cpu drops layer content
|
||||
// that lies entirely outside the viewport before filtering.
|
||||
let reach = reach + Vec2::new(dx, dy);
|
||||
self.with_filters(ctx, f, reach, blur, Some((mode, opacity)), |r, c, fr, comp| {
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, comp.map(|m| blend_mode(m.0)), comp.map(|m| m.1), None, Some(filter));
|
||||
paint(r, c, &shifted(fr, dx, dy));
|
||||
// Darkness: black over the object's own colors, keeping their alpha.
|
||||
if let Some(black) = darkness.map(|d| shade(f.ink, d)).filter(|d| *d > 0.0) {
|
||||
c.set_transform(fr.view);
|
||||
c.set_blend_mode(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcAtop));
|
||||
c.set_paint(peniko::Color::new([0.0, 0.0, 0.0, black]));
|
||||
c.fill_rect(&reach);
|
||||
c.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal));
|
||||
}
|
||||
c.pop_layer();
|
||||
});
|
||||
}
|
||||
|
||||
/// While interacting, give `n` (shadow-cache key `key`) the shadow or glow raster an earlier
|
||||
/// frame made for its object (the same id, effect slot and ink, at this zoom and rotation, with
|
||||
/// the same `inputs`) when only the silhouette's place changed after that frame: made by the
|
||||
/// same effect `fx` from a silhouette whose bounds have the size of `reach` (the content's
|
||||
/// bounds moved by the shadow's offset), and whose alpha (and colors, under Darkness) are those
|
||||
/// of `n`'s silhouette drawn into a raster placed as that one was against its anchor
|
||||
/// ([`silhouette`]). The raster is kept as `n`'s, marked
|
||||
/// moved, and is drawn at the nearest whole pixel.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn move_shadow(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
n: &Node,
|
||||
key: (usize, usize, Ink),
|
||||
fx: &RasterFx,
|
||||
reach: Rect,
|
||||
owner: Option<&Arc<Node>>,
|
||||
inputs: &Inputs,
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
let (dx, dy, colored) = match fx {
|
||||
RasterFx::DropShadow { dx, dy, darkness, .. } => (*dx, *dy, darkness.is_some()),
|
||||
_ => (0.0, 0.0, false),
|
||||
};
|
||||
let (linear, stamp, size) = (linear_key(f.view), self.stamp, reach.size());
|
||||
let fits = |k: &(usize, usize, Ink), e: &ShadowEntry| {
|
||||
(k.1, k.2) == (key.1, key.2)
|
||||
&& e.stamp < stamp
|
||||
&& e.linear == linear
|
||||
&& e.view.is_none()
|
||||
&& e.node.id() == n.id
|
||||
&& e.inputs == *inputs
|
||||
&& e.made.fits(&[fx], size, None, None)
|
||||
};
|
||||
let Some((old, rel, w, h, scale)) =
|
||||
self.shadows.iter().find(|(k, e)| fits(k, e)).map(|(k, e)| (*k, e.rel, e.image.width(), e.image.height(), e.scale))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The silhouette drawn where the old one lies in its raster.
|
||||
let anchor = reach.origin();
|
||||
let p = f.view * anchor;
|
||||
let mut off = f.offscreen_context(w, h);
|
||||
let view = Affine::scale(1.0 / scale) * Affine::translate((-(p.x + rel.0), -(p.y + rel.1))) * f.view;
|
||||
// Culled against the raster's area, as `shadow_raster` draws the silhouette.
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let crop = Frame { mt: false, view, visible, ..*f };
|
||||
self.inside_layer(|r| paint(r, &mut off, &shifted(&crop, dx, dy)));
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
if self.shadows.get(&old).is_none_or(|e| silhouette(&pm, colored) != e.made.pixels) {
|
||||
return;
|
||||
}
|
||||
if let Some(mut e) = self.shadows.remove(&old) {
|
||||
(e.node, e.anchor, e.moved) = (Drawn::of(n, owner), anchor, true);
|
||||
self.shadows.insert(key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// `draw_shape` for nodes with live effects.
|
||||
pub(crate) fn draw_shape_fx(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, bp: &BezPath, rule: FillRule) {
|
||||
// The node `draw_arc` is drawing gets its geometry from the cache: `bp` is its own path,
|
||||
// made from the node alone, so the effects give the same result every frame while the
|
||||
// plug-ins installed stay the same (`check_plugins`).
|
||||
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned();
|
||||
let mut cached = owner.map(|o| {
|
||||
let key = Arc::as_ptr(&o) as usize;
|
||||
let stamp = self.stamp;
|
||||
let e = match self.fx_geom.remove(&key) {
|
||||
Some(e) if Arc::ptr_eq(&e.node, &o) => e,
|
||||
_ => FxGeom { g: Arc::new(effected_path(n, bp)), items: vec![None; n.appearance.items.len()], node: o, stamp },
|
||||
};
|
||||
(key, FxGeom { stamp, ..e })
|
||||
});
|
||||
let g = match &cached {
|
||||
Some((_, e)) => e.g.clone(),
|
||||
None => Arc::new(effected_path(n, bp)),
|
||||
};
|
||||
self.draw_shape_fx_geom(ctx, f, n, &g, rule, cached.as_mut().map(|(_, e)| &mut e.items));
|
||||
if let Some((key, e)) = cached {
|
||||
self.fx_geom.insert(key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::draw_shape_fx`] on the effected geometry `g`, with the items' cached geometry.
|
||||
fn draw_shape_fx_geom(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
n: &Node,
|
||||
g: &BezPath,
|
||||
rule: FillRule,
|
||||
items: Option<&mut Vec<Option<Arc<BezPath>>>>,
|
||||
) {
|
||||
if g.elements().is_empty() {
|
||||
return;
|
||||
}
|
||||
if f.opts.outline {
|
||||
self.hairline(ctx, f, g, [0, 0, 0, 255]);
|
||||
return;
|
||||
}
|
||||
let gctx = GeomContext::of(n);
|
||||
let rfx = effects::raster_effects(&n.appearance.effects);
|
||||
// Document-space reach of the painted geometry (strokes, arrowheads…).
|
||||
let outset = n.appearance.outset();
|
||||
let reach = g.bounding_box().inflate(outset, outset);
|
||||
let bounds = rasterize_bounds(f.symbols, n, &rfx);
|
||||
// Under Rasterize the content reaches as far as its fills' and strokes' raster effects paint.
|
||||
let reach = bounds.map_or(reach, |b| reach.union(b));
|
||||
let content = Content { node: n, owner: None, cache: Some(0), reach, outline: Some((g, rule)), bounds, kept: None };
|
||||
let mut items = items;
|
||||
self.raster_fx(ctx, f, &content, &rfx, &mut |r, c, fr| r.paint_items(c, fr, n, g, rule, &gctx, items.as_deref_mut()));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
|
||||
/// Draw type, an image, a symbol instance or a live object with object-level effects
|
||||
/// ([`has_object_fx`]) inside its transparency group: its art (reshaped by the geometry
|
||||
/// effects, see [`effects::reshape`]) with the raster effects painted around it as for paths,
|
||||
/// the art standing for the object's silhouette.
|
||||
pub(crate) fn draw_object_fx(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, cache: bool) {
|
||||
let opacity = self.opacity_of(a);
|
||||
if !f.opts.outline && (opacity < 1.0 || a.blend != vectorcraft_doc::color::BlendMode::Normal || a.isolate) {
|
||||
let blends = self.blends_through(a);
|
||||
let bounds = if blends { self.bounds_of(f.symbols, a) } else { None };
|
||||
let comp = crate::group::Composite { blend: a.blend, opacity, isolated: a.isolate, blends, bounds, ..Default::default() };
|
||||
self.group(ctx, f, comp, &mut |r, c, fr| r.draw_object_fx_art(c, fr, a, cache));
|
||||
} else {
|
||||
self.draw_object_fx_art(ctx, f, a, cache);
|
||||
}
|
||||
self.stats.drawn += 1;
|
||||
}
|
||||
|
||||
/// What [`Self::draw_object_fx`] draws inside the object's transparency group.
|
||||
fn draw_object_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, cache: bool) {
|
||||
let art = self.fx_art(f.doc, a, cache);
|
||||
// Art the cache did not keep is made again in the next frame.
|
||||
let kept = cache && self.fx_arts.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &art));
|
||||
let outer = self.made_again;
|
||||
self.made_again = outer || !kept;
|
||||
// With Crop Marks, the object inside the art paints its own raster effects.
|
||||
let rfx = if effects::has_crop_marks(a) { vec![] } else { effects::raster_effects(&a.appearance.effects) };
|
||||
match cull_bounds(f.symbols, &art) {
|
||||
Some(reach) if !f.opts.outline && !rfx.is_empty() => {
|
||||
// Feather, Inner Glow and Rasterize with Create Clipping Mask clip to the art's outline.
|
||||
let clipped = rfx.iter().any(|x| {
|
||||
matches!(x, RasterFx::Feather { .. } | RasterFx::InnerGlow { .. }) || matches!(x, RasterFx::Rasterize(r) if r.clipping_mask)
|
||||
});
|
||||
let region = if clipped { self.clip_of(&art) } else { None };
|
||||
// A symbol's art can change while the instance doesn't: no cached shadows.
|
||||
let cache = (cache && !matches!(a.kind, NodeKind::SymbolInstance { .. })).then_some(0);
|
||||
let bounds = rasterize_bounds(f.symbols, a, &rfx);
|
||||
let reach = bounds.map_or(reach, |b| reach.union(b));
|
||||
let content = Content { node: a, owner: Some(a), cache, reach, outline: region.as_deref().map(|r| (&r.0, r.1)), bounds, kept: None };
|
||||
self.raster_fx(ctx, f, &content, &rfx, &mut |r, c, fr| r.draw_fx_art(c, fr, a, &art));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
_ => self.draw_fx_art(ctx, f, a, &art),
|
||||
}
|
||||
self.made_again = outer;
|
||||
}
|
||||
|
||||
/// The art [`Self::draw_object_fx`] draws for `a` (cached by `Arc` identity when `cache`):
|
||||
/// reshaped by its geometry effects, else type, images and paths as themselves and the others
|
||||
/// as their evaluated art (groups and layers: [`effects::evaluate_container`], keeping their
|
||||
/// knockout setting), recoloured by the colour adjustments in it ([`Self::adjusted_art`]);
|
||||
/// without the object's transparency and effects (a path keeps its geometry effects). Art that
|
||||
/// is made from live objects it evaluated ([`evaluates_live`]), or from type, symbol art or
|
||||
/// colors shaded through the color settings ([`reads_outside`]), is kept only while what it
|
||||
/// reads from outside `a` stays the same ([`Self::live_found`]).
|
||||
pub(super) fn fx_art(&mut self, doc: &vectorcraft_doc::Document, a: &Arc<Node>, cache: bool) -> Arc<Node> {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
let mut kept = None;
|
||||
if cache
|
||||
&& let Some((node, art, found)) = self.fx_arts.get(&key, self.stamp)
|
||||
&& Arc::ptr_eq(node, a)
|
||||
{
|
||||
match found {
|
||||
None => return art.clone(),
|
||||
Some(was) => kept = Some((art.clone(), was.0)),
|
||||
}
|
||||
}
|
||||
let found = (kept.is_some() || (cache && (evaluates_live(a) || reads_outside(a)))).then(|| self.live_found(doc, a));
|
||||
if let Some((art, was)) = kept
|
||||
&& found.as_ref().is_some_and(|f| f.0 == was)
|
||||
{
|
||||
return art;
|
||||
}
|
||||
let symbol = match &a.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => {
|
||||
doc.symbols.iter().find(|s| s.name == *symbol).map(|s| crate::brush_fx::instance_art(&s.art, a))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let container = is_container(a);
|
||||
let art = match effects::crop_marks_art(a)
|
||||
.or_else(|| effects::revolve_art(a))
|
||||
.or_else(|| effects::solid_art(a))
|
||||
.or_else(|| effects::reshape(Some(doc), a, symbol.as_ref()))
|
||||
{
|
||||
Some(r) => r,
|
||||
None if container => effects::evaluate_container(Some(doc), a).unwrap_or_else(|| (**a).clone()),
|
||||
None if matches!(a.kind, NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::Path { .. } | NodeKind::Compound { .. }) => (**a).clone(),
|
||||
None => effects::outline_art(Some(doc), a, symbol.as_ref()).unwrap_or_else(|| Node::group(a.id, vec![])),
|
||||
};
|
||||
let art = if effects::has_revolve(a) || effects::has_solid(a) { effects::evaluate_container(Some(doc), &art).unwrap_or(art) } else { art };
|
||||
let mut art = self.adjusted_art(art, effects::color_map(&a.appearance.effects).as_ref());
|
||||
// A path's geometry effects still apply when it is drawn.
|
||||
let path = matches!(art.kind, NodeKind::Path { .. } | NodeKind::Compound { .. });
|
||||
art.appearance.effects.retain(|e| path && effects::is_geometry(&e.id));
|
||||
let mut art = art.with_mask(None);
|
||||
art.opacity = 1.0;
|
||||
art.blend = Default::default();
|
||||
art.isolate = false;
|
||||
// One element: its pieces never knock each other out (a group's members still do as its
|
||||
// knockout setting says).
|
||||
art.knockout = if container { a.knockout } else { vectorcraft_doc::Knockout::Off };
|
||||
let art = Arc::new(art);
|
||||
if cache {
|
||||
let bytes = crate::lru::node_bytes(&art);
|
||||
self.fx_arts.insert(key, (a.clone(), art.clone(), found), bytes, self.stamp);
|
||||
}
|
||||
art
|
||||
}
|
||||
|
||||
/// Draw `art`, the art [`Self::fx_art`] made for `a`. Type art that `fx_arts` keeps for `a` is
|
||||
/// drawn as `fx_owner`, so [`Self::draw_text`] takes its glyph geometry from `texts`.
|
||||
fn draw_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, art: &Arc<Node>) {
|
||||
let kept =
|
||||
matches!(art.kind, NodeKind::Text(_)) && self.fx_arts.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, art));
|
||||
if !kept {
|
||||
return self.draw_node(ctx, f, art, true);
|
||||
}
|
||||
let outer = self.fx_owner.replace(art.clone());
|
||||
self.draw_node(ctx, f, art, true);
|
||||
self.fx_owner = outer;
|
||||
}
|
||||
|
||||
/// `art` with the colour adjustments inside it applied, then `outer` (its object's own) on
|
||||
/// everything ([`effects::adjust`]). Embedded images become recoloured copies, cached with the
|
||||
/// decoded images under a key naming the adjustment.
|
||||
fn adjusted_art(&mut self, mut art: Node, outer: Option<&effects::ColorMap>) -> Node {
|
||||
art.appearance.effects.retain(|e| !effects::is_adjustment(&e.id));
|
||||
let mut wanted: Vec<(String, String, u64, effects::ColorMap)> = vec![];
|
||||
let adjusted = effects::adjust(&art, outer, &mut |key, map| {
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
std::hash::Hash::hash(&format!("{map:?}"), &mut h);
|
||||
let hash = std::hash::Hasher::finish(&h);
|
||||
let derived = format!("{key}\u{0}adjust\u{0}{hash:016x}");
|
||||
wanted.push((key.to_string(), derived.clone(), hash, map.clone()));
|
||||
Some(derived)
|
||||
});
|
||||
for (key, derived, hash, map) in wanted {
|
||||
self.adjusted_image(&key, derived, hash, &map);
|
||||
}
|
||||
adjusted.unwrap_or(art)
|
||||
}
|
||||
|
||||
/// Paint `content` with raster effects `rfx` (see the module docs): shadows and outer glows
|
||||
/// below it, the content blurred or feathered, inner glows above it (clipped to its outline).
|
||||
/// `paint` draws the content in the frame it is given.
|
||||
pub(super) fn raster_fx(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
content: &Content,
|
||||
rfx: &[RasterFx],
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
// Only the last MAX_RASTERIZE Rasterize effects are drawn.
|
||||
let count = rfx.iter().filter(|fx| matches!(fx, RasterFx::Rasterize(_))).count();
|
||||
let capped: Vec<RasterFx>;
|
||||
let rfx = if count > MAX_RASTERIZE {
|
||||
let mut left_out = count - MAX_RASTERIZE;
|
||||
capped = rfx
|
||||
.iter()
|
||||
.filter(|fx| {
|
||||
let out = left_out > 0 && matches!(fx, RasterFx::Rasterize(_));
|
||||
left_out -= usize::from(out);
|
||||
!out
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
&capped[..]
|
||||
} else {
|
||||
rfx
|
||||
};
|
||||
// Rasterize at the document's raster effects resolution takes it here.
|
||||
let resolved: Vec<RasterFx>;
|
||||
let rfx = if rfx.iter().any(|fx| matches!(fx, RasterFx::Rasterize(r) if r.ppi.is_none())) {
|
||||
let ppi = doc_raster_ppi(f.doc);
|
||||
resolved = rfx.iter().map(|fx| fx.at_document_ppi(ppi)).collect();
|
||||
&resolved[..]
|
||||
} else {
|
||||
rfx
|
||||
};
|
||||
// Rasterize makes an image of everything the effects before it paint, shadows and glows
|
||||
// included, and the shadows and glows after it paint around that image. When some come
|
||||
// after the last Rasterize, the stack splits after it, else when any effect comes before
|
||||
// it, the stack splits before it; the effects after the split paint around the content
|
||||
// drawn with those before it. Rasterize then comes first in its part.
|
||||
if let Some(at) = rfx.iter().rposition(|fx| matches!(fx, RasterFx::Rasterize(_))) {
|
||||
let paints_around = |fx: &[RasterFx]| fx.iter().any(|fx| !content_changing(&fx));
|
||||
let (_, rest) = rfx.split_at(at + 1);
|
||||
let split = if paints_around(rest) { Some(at + 1) } else { (at > 0).then_some(at) };
|
||||
if let Some(split) = split {
|
||||
let (before, after) = rfx.split_at(split);
|
||||
let line = 72.0 / doc_raster_ppi(f.doc);
|
||||
let (mut reach, mut spread) = (content.reach, content.bounds);
|
||||
// After a Rasterize on white without a clipping mask, the silhouette is its image,
|
||||
// and it stays the silhouette through the effects after it (as in `pixel_content`).
|
||||
let mut image = None;
|
||||
// Whether the image of a Rasterize before the split is over `rasterize_image`'s
|
||||
// limits. Such an image is kept only as far as the view needs it, so the effects
|
||||
// after the split are not cached.
|
||||
let (mut over, mut last_over) = (false, false);
|
||||
for fx in before {
|
||||
if let RasterFx::Rasterize(r) = fx {
|
||||
last_over = image_size(r, r.image(spread.unwrap_or(reach), line), line).is_none();
|
||||
over |= last_over;
|
||||
}
|
||||
let o = fx.outset(reach);
|
||||
reach = reach.inflate(o, o);
|
||||
spread = spread.map(|b| fx.spread(b, line));
|
||||
if let RasterFx::Rasterize(r) = fx
|
||||
&& r.white
|
||||
&& !r.clipping_mask
|
||||
{
|
||||
image = Some(spread.unwrap_or(reach).to_path(0.1));
|
||||
}
|
||||
}
|
||||
// When the split comes after the Rasterize, the shadows and glows after it and the
|
||||
// content each draw the content before the split. The first draw makes the image,
|
||||
// and the others draw the one it keeps.
|
||||
let window = after.iter().fold(f.visible, |w, fx| {
|
||||
let o = fx.outset(w);
|
||||
w.inflate(o, o)
|
||||
});
|
||||
let frame = last_over.then_some((f.view, f.px));
|
||||
let kept = (split > at).then(|| Kept { window, frame, image: std::cell::RefCell::new(None) });
|
||||
let inner = Content {
|
||||
node: content.node,
|
||||
owner: content.owner,
|
||||
cache: None,
|
||||
reach: content.reach,
|
||||
outline: content.outline,
|
||||
bounds: content.bounds,
|
||||
kept: kept.as_ref(),
|
||||
};
|
||||
let outline = image.as_ref().map(|g| (g, FillRule::NonZero)).or(content.outline);
|
||||
// When the split comes before the Rasterize, the part from it on draws the content
|
||||
// once, and its image is the one kept for a split above.
|
||||
let outer = Content {
|
||||
node: content.node,
|
||||
owner: content.owner,
|
||||
cache: content.cache.filter(|_| !over),
|
||||
reach,
|
||||
outline,
|
||||
bounds: spread,
|
||||
kept: content.kept.filter(|_| split == at),
|
||||
};
|
||||
self.raster_fx(ctx, f, &outer, after, &mut |r, c, fr| r.raster_fx(c, fr, &inner, before, paint));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let reach = content.reach;
|
||||
// The `Arc` drawn: the caller's, or the node `draw_arc` is drawing.
|
||||
let owner = content.owner.cloned().or_else(|| self.fx_owner.clone().filter(|o| std::ptr::eq(&**o, content.node)));
|
||||
// Below the content: shadows and outer glows, each of the content with the ones before it in the stack, as AI
|
||||
// files draw a glow round a glow and the SVG export nests them. Their rasters are made first to last, each once
|
||||
// and drawn into the later ones' silhouettes as an image (so n of them cost n rasters, not 2^n draws), then
|
||||
// drawn with the last lowest. One without a raster (an object drawn uncached, a glow past the budget) is drawn
|
||||
// through a filter layer from the content alone, and goes into the later silhouettes that way.
|
||||
let mut made: Vec<(&RasterFx, Option<Shadow>)> = vec![];
|
||||
let mut stacked = reach;
|
||||
let screen = Rect::new(0.0, 0.0, f64::from(ctx.width()), f64::from(ctx.height()));
|
||||
// How far (document units) a shadow or glow looks from a pixel to the silhouette under it.
|
||||
let looks = |fx: &RasterFx| match fx {
|
||||
RasterFx::DropShadow { dx, dy, blur, .. } => blur.max(0.0) * 1.5 + dx.abs() + dy.abs(),
|
||||
RasterFx::OuterGlow { blur, .. } => blur.max(0.0) * 1.5,
|
||||
_ => 0.0,
|
||||
};
|
||||
for (i, fx) in rfx.iter().enumerate().filter(|(_, x)| x.is_below()) {
|
||||
// A raster cropped to the view covers what the shadows and glows stacked over it read.
|
||||
let around: f64 = rfx.iter().skip(i + 1).filter(|x| x.is_below()).map(looks).sum();
|
||||
// The rect that the cropped rasters of the earlier ones (drawn into this one's silhouette) were all made for.
|
||||
let over = made.iter().filter_map(|(_, s)| s.as_ref()?.cropped).reduce(|a, b| a.intersect(b));
|
||||
let shadow = {
|
||||
let earlier = &made;
|
||||
let view = f.view;
|
||||
let mut art = |r: &mut Self, c: &mut RenderContext, fr: &Frame| {
|
||||
let to = fr.view * view.inverse();
|
||||
for (fx, s) in earlier.iter().rev() {
|
||||
match s {
|
||||
Some(s) => {
|
||||
r.stats.moved += usize::from(s.moved);
|
||||
r.draw_shadow(c, s, to);
|
||||
}
|
||||
None => r.draw_shadow_filter(c, fr, fx, reach, paint),
|
||||
}
|
||||
}
|
||||
paint(r, c, fr);
|
||||
};
|
||||
let view = (screen, around, over);
|
||||
content.cache.and_then(|slot| self.shadow_raster(f, content.node, slot + i, fx, stacked, owner.as_ref(), view, &mut art))
|
||||
};
|
||||
stacked = match fx {
|
||||
RasterFx::DropShadow { dx, dy, blur, .. } => stacked.union((stacked + Vec2::new(*dx, *dy)).inflate(blur * 1.5, blur * 1.5)),
|
||||
_ => stacked.inflate(fx.outset(stacked), fx.outset(stacked)),
|
||||
};
|
||||
made.push((fx, shadow));
|
||||
}
|
||||
for (fx, s) in made.iter().rev() {
|
||||
match s {
|
||||
Some(s) => self.draw_shadow(ctx, s, Affine::IDENTITY),
|
||||
None => self.draw_shadow_filter(ctx, f, fx, reach, paint),
|
||||
}
|
||||
}
|
||||
// The content itself (blurred / feathered / filtered), on its pixels: a filter layer's blur
|
||||
// works on a coarser grid tied to the canvas, so the blurred art shifted by up to a pixel
|
||||
// and looked blocky as the view moved by parts of a pixel (#755).
|
||||
let blurs = rfx.iter().any(|fx| matches!(fx, RasterFx::Feather { radius } | RasterFx::GaussianBlur { radius } if *radius > 0.0));
|
||||
if blurs || rfx.iter().any(|fx| matches!(fx, RasterFx::Pixel(_) | RasterFx::Rasterize(_))) {
|
||||
self.pixel_content(ctx, f, content, rfx, paint);
|
||||
} else {
|
||||
paint(self, ctx, f);
|
||||
}
|
||||
// Above the content: inner glows, clipped to its outline.
|
||||
let Some((g, rule)) = content.outline else { return };
|
||||
for fx in rfx {
|
||||
let RasterFx::InnerGlow { mode, opacity, blur, color, center } = fx else { continue };
|
||||
let filter = shadow_filter(0.0, 0.0, *blur, pcolor(f.ink, color));
|
||||
self.with_filters(ctx, f, g.bounding_box(), *blur, Some((*mode, *opacity)), |_, c, fr, comp| {
|
||||
// The blend layer goes outside the clip: a clip layer is isolated, so a blend
|
||||
// inside it would mix with nothing instead of the content below.
|
||||
if let Some((m, o)) = comp {
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(blend_mode(m)), Some(o), None, None);
|
||||
}
|
||||
c.set_transform(fr.view);
|
||||
c.set_fill_rule(fill_rule(rule));
|
||||
c.push_clip_layer(g);
|
||||
c.push_layer(None, None, None, None, Some(filter));
|
||||
c.set_paint(peniko::Color::BLACK);
|
||||
if *center {
|
||||
c.set_fill_rule(fill_rule(rule));
|
||||
c.fill_path(g);
|
||||
} else {
|
||||
// Everything outside the shape, so the glow bleeds in from the edges.
|
||||
let pad = blur * 2.0 + 4.0 * fr.px;
|
||||
let mut inv = g.bounding_box().inflate(pad, pad).to_path(0.1);
|
||||
inv.extend(g.iter());
|
||||
c.set_fill_rule(peniko::Fill::EvenOdd);
|
||||
c.fill_path(&inv);
|
||||
}
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
if comp.is_some() {
|
||||
c.pop_layer();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user