mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #2409 from dotCooCoo/feat/live-rasterize-effect
Effects: Effect › Rasterize draws an object as pixels while its paths stay editable
This commit is contained in:
+532
-43
@@ -17,6 +17,12 @@
|
||||
//! filters, Gaussian Blur and Feather, see [`effects::pixel`]), then drawn back
|
||||
//! ([`Renderer::pixel_content`]). The result is cached per object while it and the zoom don't
|
||||
//! change.
|
||||
//! - Rasterize: the content drawn again on the image's own grid, as Object › Rasterize draws it
|
||||
//! ([`Renderer::rasterize_image`]), then into the content's raster
|
||||
//! ([`effects::Rasterize::compose`]; an image too large for that is made from the content's
|
||||
//! raster, [`effects::Rasterize::apply`]), clipped to its outline with Create Clipping Mask. The
|
||||
//! effects before it in the stack, and the shadows and glows after it, are drawn around it by
|
||||
//! splitting the stack there ([`Renderer::raster_fx`]).
|
||||
//!
|
||||
//! While [`crate::RenderOptions::interacting`], an object whose content only moved draws its
|
||||
//! cached shadows, glows and filtered content at its new place instead of running the effects
|
||||
@@ -110,6 +116,41 @@ struct Content<'a> {
|
||||
/// Its silhouette: Feather and Inner Glow clip to it (without one, Feather only blurs and
|
||||
/// Inner Glow is skipped).
|
||||
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`).
|
||||
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.
|
||||
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.
|
||||
struct Kept {
|
||||
/// Where the effects after the split take the content from: the visible area grown by how
|
||||
/// far they paint.
|
||||
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.
|
||||
frame: Option<(Affine, f64)>,
|
||||
image: std::cell::RefCell<Option<KeptImage>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum KeptImage {
|
||||
/// The image on its own grid, as [`Renderer::rasterize_image`] makes it.
|
||||
Grid(std::rc::Rc<(Vec<u8>, usize, usize)>),
|
||||
/// 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),
|
||||
}
|
||||
|
||||
/// The [`Content::bounds`] of `n` under raster effects `rfx`: its [`drawn_bounds`] when `rfx`
|
||||
/// holds a Rasterize.
|
||||
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).
|
||||
@@ -157,7 +198,7 @@ fn item_effects(item: &AppearanceItem) -> &[Effect] {
|
||||
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 = effects::outset(&n.appearance.effects, b);
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
b.inflate(o, o)
|
||||
});
|
||||
}
|
||||
@@ -178,11 +219,173 @@ pub(crate) fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
let s = n.appearance.outset();
|
||||
r.map(|r| {
|
||||
let r = r.inflate(s, s);
|
||||
let o = effects::outset(&n.appearance.effects, r);
|
||||
// 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(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 measured one by one with the effects of its own 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(n, symbol.as_deref());
|
||||
let b = match &reshaped {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) => {
|
||||
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`.
|
||||
@@ -209,7 +412,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio
|
||||
} else {
|
||||
art.children()?.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c)))
|
||||
}?;
|
||||
let o = effects::outset(&n.appearance.effects, b);
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
Some(b.inflate(o, o))
|
||||
}
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => {
|
||||
@@ -239,7 +442,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio
|
||||
};
|
||||
let a = a.max(live_content_reach(symbols, n));
|
||||
let b = b.inflate(a, a);
|
||||
let o = effects::outset(&n.appearance.effects, b);
|
||||
let o = outset(symbols, &n.appearance.effects, b);
|
||||
Some(b.inflate(o, o))
|
||||
}
|
||||
NodeKind::SymbolInstance { .. } => {
|
||||
@@ -351,10 +554,43 @@ fn pcolor(ink: Ink, c: &vectorcraft_doc::color::Color) -> peniko::Color {
|
||||
/// cropped to the view when its effects only look nearby, else rendered at a lower resolution.
|
||||
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.
|
||||
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`].
|
||||
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).
|
||||
fn doc_raster_ppi(doc: &Document) -> f64 {
|
||||
Some(doc.raster_effects_ppi).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0)
|
||||
}
|
||||
|
||||
/// The effects [`Renderer::pixel_content`] runs on the content's pixels (those changing the
|
||||
/// content itself), in stack order.
|
||||
fn content_changing(fx: &&RasterFx) -> bool {
|
||||
matches!(fx, RasterFx::Feather { .. } | RasterFx::GaussianBlur { .. } | RasterFx::Pixel(_))
|
||||
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.)
|
||||
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
|
||||
@@ -363,6 +599,7 @@ 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),
|
||||
}
|
||||
}
|
||||
@@ -442,6 +679,23 @@ fn draw_scaled(ctx: &mut RenderContext, image: std::sync::Arc<vello_cpu::Pixmap>
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -939,7 +1193,10 @@ impl Renderer {
|
||||
// Document-space reach of the painted geometry (strokes, arrowheads…).
|
||||
let outset = n.appearance.outset();
|
||||
let reach = g.bounding_box().inflate(outset, outset);
|
||||
let content = Content { node: n, owner: None, cache: Some(0), reach, outline: Some((g, rule)) };
|
||||
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);
|
||||
@@ -973,12 +1230,16 @@ impl Renderer {
|
||||
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 and Inner Glow clip to the art's outline.
|
||||
let clipped = rfx.iter().any(|x| matches!(x, RasterFx::Feather { .. } | RasterFx::InnerGlow { .. }));
|
||||
// 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 content = Content { node: a, owner: Some(a), cache, reach, outline: region.as_deref().map(|r| (&r.0, r.1)) };
|
||||
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);
|
||||
}
|
||||
@@ -1093,6 +1354,102 @@ impl Renderer {
|
||||
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)));
|
||||
@@ -1134,7 +1491,7 @@ impl Renderer {
|
||||
// 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(_))) {
|
||||
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);
|
||||
@@ -1176,11 +1533,15 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint `content` through the content-changing effects of `rfx` (Gaussian Blur, Feather and the
|
||||
/// Photoshop-style filters) in stack order on its pixels: rendered offscreen over all it can
|
||||
/// reach, filtered on the CPU and drawn back. The raster holds at most [`PIXEL_BUDGET`] pixels:
|
||||
/// larger content is cropped to the view when every effect only looks nearby, else rendered
|
||||
/// at a lower resolution. A whole raster at full resolution is cached while the object, the
|
||||
/// 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`]).
|
||||
fn pixel_content(
|
||||
@@ -1192,26 +1553,42 @@ impl Renderer {
|
||||
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);
|
||||
let (sw, sh) = (ctx.width() as f64, ctx.height() as f64);
|
||||
if !(full.area().is_finite() && full.x1 > 0.0 && full.y1 > 0.0 && full.x0 < sw && full.y0 < sh) {
|
||||
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 region = match margin {
|
||||
Some(m) if !fits => full.intersect(Rect::new(0.0, 0.0, sw, sh).inflate(m, m)),
|
||||
Some(m) if !fits => full.intersect(screen.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));
|
||||
let side = u16::MAX as f64;
|
||||
// (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, when the whole content was rendered at full size at this zoom.
|
||||
@@ -1225,12 +1602,12 @@ impl Renderer {
|
||||
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 = Some(f.doc.raster_effects_ppi).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0);
|
||||
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, the resolution.
|
||||
// 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 { .. })));
|
||||
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;
|
||||
@@ -1256,15 +1633,45 @@ impl Renderer {
|
||||
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 mut off = f.offscreen_context(wu, hu);
|
||||
let line = 72.0 / ppi;
|
||||
let (before, decoded) = (self.stats.moved, crate::paint::decode_failures());
|
||||
self.inside_layer(|r| paint(r, &mut off, &frame));
|
||||
// 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)) => Some(image.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let image = kept.or_else(|| {
|
||||
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()));
|
||||
}
|
||||
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;
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(wu, hu);
|
||||
off.render(&mut pm, &mut self.resources);
|
||||
// What the raster is made from, for its object once it moves (`move_pixel_content`).
|
||||
let made = key.map(|_| Made {
|
||||
pixels: pm.data_as_u8_slice().to_vec(),
|
||||
@@ -1274,18 +1681,41 @@ impl Renderer {
|
||||
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: 72.0 / ppi, extent };
|
||||
let space = effects::PixelSpace { to_doc, px, center, channels, line, extent };
|
||||
let (wz, hz) = (wu as usize, hu as usize);
|
||||
for fx in chain {
|
||||
// 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)) = content.outline {
|
||||
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));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1302,7 +1732,15 @@ impl Renderer {
|
||||
self.pixel_fx.insert(key, PixelEntry { node, linear, anchor, rel, k, image: image.clone(), made, inputs, moved, failed, stamp });
|
||||
self.trim_fx_rasters(FX_RASTER_BUDGET);
|
||||
}
|
||||
draw_scaled(ctx, image, (x0, y0), k);
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// While interacting, give `content` (cache key `key`) the filtered raster an earlier frame
|
||||
@@ -1312,7 +1750,7 @@ impl Renderer {
|
||||
/// (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 takes none.
|
||||
/// De-Interlace, or under a Rasterize first in the chain, takes none.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn move_pixel_content(
|
||||
&mut self,
|
||||
@@ -1324,7 +1762,8 @@ impl Renderer {
|
||||
inputs: &Inputs,
|
||||
paint: &mut dyn FnMut(&mut Self, &mut RenderContext, &Frame),
|
||||
) {
|
||||
if chain.iter().any(|fx| matches!(fx, RasterFx::Pixel(PixelFx::DeInterlace { .. }))) {
|
||||
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());
|
||||
@@ -1348,6 +1787,9 @@ impl Renderer {
|
||||
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);
|
||||
@@ -1363,17 +1805,19 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// The inputs a raster of `n`'s content, which reads `reads`, is drawn with in frame `f`: the
|
||||
/// color settings, the edge smoothing, 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.
|
||||
/// 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).hash(&mut h);
|
||||
(doc.cms, f.opts.anti_alias.threshold(), self.knockout, f.doc.raster_effects_ppi.to_bits()).hash(&mut h);
|
||||
if let Some((isolated, path)) = self.isolation_path(f)
|
||||
&& n.id != isolated
|
||||
&& path.contains(&n.id)
|
||||
@@ -1466,6 +1910,42 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
@@ -1564,8 +2044,17 @@ impl Renderer {
|
||||
let outset = n.appearance.outset();
|
||||
let orule = if item.is_fill() { rule } else { FillRule::NonZero };
|
||||
// Each item's shadows get their own cache slots.
|
||||
let content =
|
||||
Content { node: n, owner: None, cache: Some((j + 1) << 8), reach: ib.inflate(outset, outset), outline: Some((&outline, orule)) };
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,19 @@ pub fn effect_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounds of what `n` draws with its own and its members' geometry effects applied and with the
|
||||
/// reach of its own and its members' raster effects, measured as [`Node::visual_bounds_with`]
|
||||
/// measures art without effects: what Object › Rasterize makes its image over.
|
||||
pub fn drawn_bounds(doc: &Document, n: &Node) -> Option<Rect> {
|
||||
fx::painted_reach(&doc.symbol_boxes(), n)
|
||||
}
|
||||
|
||||
/// The outline, with `n`'s geometry effects applied, that the canvas clips `n`'s Rasterize with
|
||||
/// Create Clipping Mask to. Object › Rasterize's clipping mask follows it.
|
||||
pub fn drawn_outline(doc: &Document, n: &Node) -> Option<(BezPath, FillRule)> {
|
||||
fx::drawn_outline(doc, n)
|
||||
}
|
||||
|
||||
/// Largest raster an export may ask [`Renderer::render_region`] for, per side. The CPU rasteriser
|
||||
/// addresses at most `u16::MAX` pixels per side (and panics at that edge).
|
||||
pub const MAX_RASTER_SIDE: u32 = 32_768;
|
||||
@@ -2428,6 +2441,8 @@ mod tests_objectfx;
|
||||
#[cfg(test)]
|
||||
mod tests_placed_document;
|
||||
#[cfg(test)]
|
||||
mod tests_rasterize;
|
||||
#[cfg(test)]
|
||||
mod tests_setup;
|
||||
#[cfg(test)]
|
||||
mod tests_strokegradient;
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
//! Effect › Rasterize on the canvas: the art drawn as square pixels of the effect's resolution
|
||||
//! over its bounds and the room around them, on white or on nothing, its paths left as they are.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, Effect, Node};
|
||||
use vectorcraft_geom::{Anchor, PathData, Point, SubPath, shapes};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A red shape (`path`, over 10..30 pt) with the effects `fx` (id, params), in a document whose
|
||||
/// raster effects resolution is `ppi`.
|
||||
fn doc(path: PathData, fx: &[(&str, Value)], ppi: f64) -> Document {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.raster_effects_ppi = ppi;
|
||||
let l = d.layers[0].id;
|
||||
let mut n = Node::path(d.alloc_id(), path, Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0));
|
||||
n.appearance.effects = fx.iter().map(|(id, params)| Effect { id: (*id).into(), params: params.clone(), visible: true }).collect();
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A right triangle over 10..30 pt whose long side runs diagonally.
|
||||
fn triangle() -> PathData {
|
||||
let corner = |x, y| Anchor::corner(Point::new(x, y));
|
||||
PathData::single(SubPath::new(vec![corner(10.0, 10.0), corner(30.0, 10.0), corner(10.0, 30.0)], true))
|
||||
}
|
||||
|
||||
/// `d` drawn at `zoom` device pixels a point on a transparent canvas `size` pixels square.
|
||||
fn draw(d: &Document, zoom: f64, size: u32) -> Rendered {
|
||||
Renderer::new().render(d, size, size, Affine::scale(zoom), &RenderOptions { background: None, artboards: false, ..Default::default() })
|
||||
}
|
||||
|
||||
fn pixel(r: &Rendered, x: usize, y: usize) -> [u8; 4] {
|
||||
let i = (y * r.width as usize + x) * 4;
|
||||
[r.pixels[i], r.pixels[i + 1], r.pixels[i + 2], r.pixels[i + 3]]
|
||||
}
|
||||
|
||||
/// Whether each of the `n` × `n` squares `side` device pixels wide from (`at`, `at`) is one color.
|
||||
fn squares_are_flat(r: &Rendered, at: usize, side: usize, n: usize) -> bool {
|
||||
(0..n * n).all(|k| {
|
||||
let (x0, y0) = (at + (k % n) * side, at + (k / n) * side);
|
||||
let first = pixel(r, x0, y0);
|
||||
(0..side * side).all(|i| pixel(r, x0 + i % side, y0 + i / side) == first)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_draws_the_art_in_pixels_of_its_resolution() {
|
||||
// (params, the document's resolution, device pixels a point, device pixels an image pixel)
|
||||
let cases = [
|
||||
(json!({"useDocumentResolution": false, "resolution": 72}), 300.0, 10.0, 10),
|
||||
(json!({"useDocumentResolution": false, "resolution": 72}), 300.0, 2.0, 2),
|
||||
(json!({"useDocumentResolution": false, "resolution": 300}), 72.0, 25.0, 6),
|
||||
(json!({}), 150.0, 25.0, 12),
|
||||
];
|
||||
for (params, ppi, zoom, side) in cases {
|
||||
let at = (10.0 * zoom) as usize;
|
||||
// The whole image pixels inside the 20 pt triangle's bounds.
|
||||
let n = (20.0 * zoom) as usize / side;
|
||||
let canvas = (40.0 * zoom) as u32;
|
||||
let rasterized = draw(&doc(triangle(), &[("rasterize", params.clone())], ppi), zoom, canvas);
|
||||
assert!(squares_are_flat(&rasterized, at, side, n), "{params}");
|
||||
// Along the diagonal an image pixel takes the mean of the art it covers.
|
||||
let partly = (0..n).map(|k| pixel(&rasterized, at + k * side, at + (n - 1 - k) * side)[3]).filter(|a| *a > 0 && *a < 255).count();
|
||||
assert!(partly > n / 2, "{params}: {partly} of {n}");
|
||||
// Without the effect the diagonal edge runs through those squares.
|
||||
assert!(!squares_are_flat(&draw(&doc(triangle(), &[], ppi), zoom, canvas), at, side, n));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_is_drawn_where_only_its_image_reaches_the_view() {
|
||||
// At 1 ppi a pixel of the image is 72 pt wide: the 20 pt square's image runs to 82 pt.
|
||||
let d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[("rasterize", json!({"background": "white"}))], 1.0);
|
||||
// A view of 40..80 pt across, where only the image's white reaches.
|
||||
let opts = RenderOptions { background: None, artboards: false, ..Default::default() };
|
||||
let r = Renderer::new().render(&d, 40, 40, Affine::translate((-40.0, 0.0)), &opts);
|
||||
assert_eq!(pixel(&r, 20, 20)[3], 255);
|
||||
// A blur of 4 pt (6 pt of spread) and 10 pt of white around it: the white reaches 46 pt.
|
||||
let fx = [("blur.gaussian", json!({"radius": 4})), ("rasterize", json!({"background": "white", "addAround": 10}))];
|
||||
let d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &fx, 72.0);
|
||||
let r = Renderer::new().render(&d, 40, 40, Affine::translate((-45.0, 0.0)), &opts);
|
||||
assert_eq!(pixel(&r, 0, 20), [255; 4]);
|
||||
// A fill's own Rasterize with 100 pt of white around: the white reaches 130 pt.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let n = Arc::make_mut(&mut d.children_mut(Some(l)).unwrap()[0]);
|
||||
n.appearance.items[0].effects_mut().push(Effect {
|
||||
id: "rasterize".into(),
|
||||
params: json!({"background": "white", "addAround": 100}),
|
||||
visible: true,
|
||||
});
|
||||
let r = Renderer::new().render(&d, 40, 40, Affine::translate((-115.0, 0.0)), &opts);
|
||||
assert_eq!(pixel(&r, 5, 20), [255; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_that_paints_nothing_takes_no_room_in_the_image() {
|
||||
// A second fill, hidden, under a Gaussian Blur that would spread 15 pt.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[("rasterize", json!({"background": "white"}))], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let n = Arc::make_mut(&mut d.children_mut(Some(l)).unwrap()[0]);
|
||||
let mut hidden = n.appearance.items[0].clone();
|
||||
if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut hidden {
|
||||
f.visible = false;
|
||||
f.effects.push(Effect { id: "blur.gaussian".into(), params: json!({"radius": 10}), visible: true });
|
||||
}
|
||||
n.appearance.items.push(hidden.clone());
|
||||
// A third, hidden too, offset 5 pt outward.
|
||||
if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut hidden {
|
||||
f.effects = vec![Effect { id: "path.offsetPath".into(), params: json!({"offset": 5}), visible: true }];
|
||||
}
|
||||
n.appearance.items.push(hidden);
|
||||
let r = draw(&d, 10.0, 400);
|
||||
assert_eq!((pixel(&r, 105, 105), pixel(&r, 95, 200)[3]), ([255, 0, 0, 255], 0));
|
||||
// Nor does a hidden member of a group.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[], 72.0);
|
||||
let mut away = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(34.0, 10.0, 38.0, 30.0)), Appearance::default());
|
||||
away.visible = false;
|
||||
let l = d.layers[0].id;
|
||||
let mut members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
members.push(Arc::new(away));
|
||||
let mut group = Node::group(d.alloc_id(), members);
|
||||
group.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({"background": "white"}), visible: true });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
assert_eq!(pixel(&draw(&d, 10.0, 400), 350, 200)[3], 0);
|
||||
// Nor does a Shaper group's source art, its first member, which paints nothing.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[], 72.0);
|
||||
let none = Appearance::basic(Paint::None, Paint::None, 0.0);
|
||||
let source = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(34.0, 10.0, 38.0, 30.0)), none);
|
||||
let l = d.layers[0].id;
|
||||
let mut members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
members.insert(0, Arc::new(source));
|
||||
let mut group = Node::group(d.alloc_id(), members);
|
||||
group.extras_mut().shaper = Some(Box::new(vectorcraft_doc::shaper::ShaperSpec { faces: vec![], edges: vec![] }));
|
||||
group.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({"background": "white"}), visible: true });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
assert_eq!(pixel(&draw(&d, 10.0, 400), 350, 200)[3], 0);
|
||||
// Nor does the path where its only fill's own Transform effect moves the fill away from it.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &[("rasterize", json!({"background": "white"}))], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let n = Arc::make_mut(&mut d.children_mut(Some(l)).unwrap()[0]);
|
||||
if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut n.appearance.items[0] {
|
||||
f.effects.push(Effect { id: "distort.transform".into(), params: json!({"moveH": 40}), visible: true });
|
||||
}
|
||||
let r = draw(&d, 10.0, 800);
|
||||
assert_eq!((pixel(&r, 600, 200), pixel(&r, 200, 200)[3]), ([255, 0, 0, 255], 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_covers_the_share_of_a_screen_pixel_its_image_covers() {
|
||||
// A 1 pt square from x = 10.25 at 72 ppi and 100%: its image pixel covers a quarter of
|
||||
// screen column 11, on white and on nothing.
|
||||
let square = || shapes::rectangle(Rect::new(10.25, 10.0, 11.25, 11.0));
|
||||
for (params, want) in [(json!({}), [64, 0, 0, 64]), (json!({"background": "white"}), [64, 0, 0, 64])] {
|
||||
let r = draw(&doc(square(), &[("rasterize", params.clone())], 72.0), 1.0, 20);
|
||||
assert_eq!(pixel(&r, 11, 10), want, "{params}");
|
||||
assert_eq!(pixel(&r, 12, 10)[3], 0, "{params}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_wider_than_a_raster_can_be_are_drawn_with_their_pixel_effects() {
|
||||
// 65,530 pt across at 100%: wider than the rasters the effects are made in can safely be.
|
||||
let bar = || shapes::rectangle(Rect::new(0.0, 10.0, 65_530.0, 11.0));
|
||||
let opts = RenderOptions { background: None, artboards: false, ..Default::default() };
|
||||
for fx in [("rasterize", json!({"background": "white"})), ("blur.gaussian", json!({"radius": 1}))] {
|
||||
let r = Renderer::new().render(&doc(bar(), std::slice::from_ref(&fx), 72.0), 40, 20, Affine::translate((-65_500.0, 0.0)), &opts);
|
||||
assert!(pixel(&r, 10, 10)[3] > 0, "{}", fx.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stack_of_ten_thousand_rasterize_effects_is_drawn() {
|
||||
let fx = vec![("rasterize", json!({"background": "white"})); 10_000];
|
||||
let r = draw(&doc(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), &fx, 72.0), 1.0, 40);
|
||||
assert_eq!(pixel(&r, 20, 20), [255, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stack_of_rasterize_effects_with_shadows_draws_the_content_of_each_once() {
|
||||
let shadow = ("stylize.dropShadow", json!({"mode": "normal", "opacity": 100, "x": 2, "y": 2, "blur": 1, "color": "#0000ff"}));
|
||||
let glow = ("stylize.outerGlow", json!({"mode": "normal", "opacity": 100, "blur": 2, "color": "#00ff00"}));
|
||||
// Five Rasterize effects, each followed by a drop shadow and an outer glow: on a 20 pt square
|
||||
// at 72 ppi, and on a 300 pt square at 2,400 ppi, whose images are too large for the canvas to
|
||||
// draw on their own grid.
|
||||
for (ppi, side, zoom) in [(72.0, 20.0, 1.0), (2400.0, 300.0, 0.1)] {
|
||||
let rasterize = ("rasterize", json!({"useDocumentResolution": false, "resolution": ppi}));
|
||||
let fx: Vec<_> = (0..5).flat_map(|_| [rasterize.clone(), shadow.clone(), glow.clone()]).collect();
|
||||
let d = doc(shapes::rectangle(Rect::new(10.0, 10.0, 10.0 + side, 10.0 + side)), &fx, 72.0);
|
||||
let drawn = || crate::fx::RASTERIZED.with(std::cell::Cell::get);
|
||||
let before = drawn();
|
||||
let r = draw(&d, zoom, 40);
|
||||
assert_eq!(drawn() - before, 5, "{ppi} ppi");
|
||||
assert_eq!(pixel(&r, 15, 15), [255, 0, 0, 255], "{ppi} ppi");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_document_resolution_redraws_a_rasterized_member_of_a_blurred_group() {
|
||||
let mut d = doc(triangle(), &[("rasterize", json!({}))], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
let mut group = Node::group(d.alloc_id(), members);
|
||||
group.appearance.effects.push(Effect { id: "blur.gaussian".into(), params: json!({"radius": 0.5}), visible: true });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
let opts = RenderOptions { background: None, artboards: false, ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
r.render(&d, 400, 400, Affine::scale(10.0), &opts);
|
||||
d.raster_effects_ppi = 18.0;
|
||||
let again = r.render(&d, 400, 400, Affine::scale(10.0), &opts);
|
||||
assert_eq!(again.pixels, Renderer::new().render(&d, 400, 400, Affine::scale(10.0), &opts).pixels);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_export_draws_the_image_with_the_effects_own_anti_aliasing() {
|
||||
// A supersampled Art Optimized export draws its art with hard edges and then averages them;
|
||||
// the effect's image keeps its smooth diagonal.
|
||||
let d = doc(triangle(), &[("rasterize", json!({"useDocumentResolution": false, "resolution": 72}))], 72.0);
|
||||
let opts = RenderOptions { background: None, artboards: false, anti_alias: AntiAlias::Art, supersample: true, ..Default::default() };
|
||||
let r = Renderer::new().render_region_with(&d, Rect::new(0.0, 0.0, 40.0, 40.0), 10.0, &opts);
|
||||
let partly = (0..20).map(|k| pixel(&r, 105 + k * 10, 105 + (19 - k) * 10)[3]).filter(|a| *a > 0 && *a < 255).count();
|
||||
assert!(partly > 10, "{partly} of 20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_without_anti_aliasing_has_hard_edges_at_any_zoom() {
|
||||
let fx = [("rasterize", json!({"useDocumentResolution": false, "resolution": 72, "antiAlias": "none"}))];
|
||||
for zoom in [10.0, 1.0] {
|
||||
let r = draw(&doc(triangle(), &fx, 72.0), zoom, (40.0 * zoom) as u32);
|
||||
let alphas: Vec<u8> = r.pixels.chunks(4).map(|p| p[3]).collect();
|
||||
assert!(alphas.iter().all(|a| *a == 0 || *a == 255), "zoom {zoom}");
|
||||
assert!(alphas.contains(&255));
|
||||
}
|
||||
// A bar 0.6 pt wide covers the center of the image pixel it starts, which it then fills, and
|
||||
// which at 50% covers half a screen pixel: it shows there, partly covering it.
|
||||
let bar = || shapes::rectangle(Rect::new(10.3, 10.0, 10.9, 30.0));
|
||||
assert_eq!(pixel(&draw(&doc(bar(), &fx, 72.0), 10.0, 400), 105, 200)[3], 255);
|
||||
let half = pixel(&draw(&doc(bar(), &fx, 72.0), 0.5, 20), 5, 10)[3];
|
||||
assert!(half > 0 && half < 255, "{half}");
|
||||
// At 100% that image pixel, 10.3..11.3 pt, covers 70% of screen column 10 and 30% of column 11.
|
||||
let full = draw(&doc(bar(), &fx, 72.0), 1.0, 40);
|
||||
assert_eq!((pixel(&full, 10, 20)[3], pixel(&full, 11, 20)[3]), (178, 77));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_keeps_what_an_earlier_effect_spreads_beyond_the_bounds() {
|
||||
let fx = [("blur.gaussian", json!({"radius": 4})), ("rasterize", json!({"useDocumentResolution": false, "resolution": 72}))];
|
||||
let r = draw(&doc(triangle(), &fx, 72.0), 10.0, 400);
|
||||
// 1.5 pt left of the triangle's bounds, the blur still shows.
|
||||
assert!(pixel(&r, 85, 150)[3] > 0);
|
||||
// So does a blur of the fill's own.
|
||||
let mut d = doc(triangle(), &fx[1..], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let n = Arc::make_mut(&mut d.children_mut(Some(l)).unwrap()[0]);
|
||||
n.appearance.items[0].effects_mut().push(Effect { id: "blur.gaussian".into(), params: json!({"radius": 4}), visible: true });
|
||||
assert!(pixel(&draw(&d, 10.0, 400), 85, 150)[3] > 0);
|
||||
// A fill's own Add Around that stays inside a 40 pt stroke takes no room past the stroke.
|
||||
let mut d = doc(shapes::rectangle(Rect::new(40.0, 40.0, 60.0, 60.0)), &[("rasterize", json!({"background": "white"}))], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let n = Arc::make_mut(&mut d.children_mut(Some(l)).unwrap()[0]);
|
||||
n.appearance.items[0].effects_mut().push(Effect { id: "rasterize".into(), params: json!({"addAround": 10}), visible: true });
|
||||
n.appearance.items[1] = vectorcraft_doc::AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 40.0));
|
||||
let r = draw(&d, 2.0, 200);
|
||||
assert_eq!((pixel(&r, 156, 100)[3], pixel(&r, 170, 100)[3]), (255, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_and_shadows_paint_in_stack_order() {
|
||||
let square = || shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0));
|
||||
let shadow = ("stylize.dropShadow", json!({"mode": "normal", "opacity": 100, "x": 5, "y": 5, "blur": 0, "color": "#0000ff"}));
|
||||
let gray = ("rasterize", json!({"colorModel": "grayscale"}));
|
||||
// Below the square's corner, only the shadow paints.
|
||||
let first = draw(&doc(square(), &[shadow.clone(), gray.clone()], 72.0), 10.0, 400);
|
||||
let [r, g, b, a] = pixel(&first, 330, 330);
|
||||
assert!(a == 255 && r == g && g == b, "the shadow is rasterized in grayscale: {:?}", [r, g, b, a]);
|
||||
let after = draw(&doc(square(), &[gray.clone(), shadow.clone()], 72.0), 10.0, 400);
|
||||
assert_eq!(pixel(&after, 330, 330), [0, 0, 255, 255], "a shadow after Rasterize keeps its color");
|
||||
// A shadow after Rasterize is the image's: a circle's square image on white casts a square shadow.
|
||||
let circle = shapes::ellipse(Rect::new(10.0, 10.0, 30.0, 30.0));
|
||||
let cast = draw(&doc(circle, &[("rasterize", json!({"background": "white"})), shadow.clone()], 72.0), 10.0, 400);
|
||||
assert_eq!(pixel(&cast, 320, 160), [0, 0, 255, 255]);
|
||||
// Between two Rasterize effects, the second one takes it in.
|
||||
let between = draw(&doc(square(), &[("rasterize", json!({})), shadow.clone(), gray], 72.0), 10.0, 400);
|
||||
let [r, g, b, _] = pixel(&between, 330, 330);
|
||||
assert!(r == g && g == b && b > 0, "{:?}", [r, g, b]);
|
||||
// A member's shadow is part of a group's art.
|
||||
let mut d = doc(square(), std::slice::from_ref(&shadow), 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
let mut group = Node::group(d.alloc_id(), members);
|
||||
group.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({}), visible: true });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
assert_eq!(pixel(&draw(&d, 10.0, 400), 330, 330), [0, 0, 255, 255]);
|
||||
// So are a member's Crop Marks: the top right mark runs from x 39 to 57 at y 10.
|
||||
let mut d = doc(square(), &[("cropMarks", json!({}))], 72.0);
|
||||
let l = d.layers[0].id;
|
||||
let members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
let mut group = Node::group(d.alloc_id(), members);
|
||||
group.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({}), visible: true });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
let r = draw(&d, 10.0, 400);
|
||||
assert!((95..105).any(|y| pixel(&r, 395, y)[3] > 0));
|
||||
// So is the shadow in a symbol's art, for its instance.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut art = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
|
||||
Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0),
|
||||
);
|
||||
art.appearance.effects.push(Effect { id: shadow.0.into(), params: shadow.1.clone(), visible: true });
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Shadowed".into(), art: Arc::new(art) });
|
||||
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shadowed".into(), xf: Affine::translate((20.0, 20.0)) });
|
||||
instance.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({}), visible: true });
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, instance).unwrap();
|
||||
assert_eq!(pixel(&draw(&d, 10.0, 400), 330, 330), [0, 0, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_takes_in_the_art_of_a_3d_effect() {
|
||||
let square = || shapes::rectangle(Rect::new(30.0, 30.0, 50.0, 50.0));
|
||||
let extrude = ("threeD.extrude", json!({}));
|
||||
let solid = draw(&doc(square(), std::slice::from_ref(&extrude), 72.0), 10.0, 800);
|
||||
// Pixels the extruded art covers outside the square.
|
||||
let outside: Vec<(usize, usize)> = (0..800 * 800)
|
||||
.map(|i| (i % 800, i / 800))
|
||||
.filter(|(x, y)| !((300..500).contains(x) && (300..500).contains(y)) && pixel(&solid, *x, *y)[3] == 255)
|
||||
.collect();
|
||||
assert!(!outside.is_empty());
|
||||
let rasterized = draw(&doc(square(), &[extrude, ("rasterize", json!({}))], 72.0), 10.0, 800);
|
||||
assert!(outside.iter().all(|(x, y)| pixel(&rasterized, *x, *y)[3] > 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_takes_in_brush_art_wider_than_the_stroke() {
|
||||
// A 1 pt line under the 10 pt. Oval brush, which paints about 2 pt from its center line.
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
|
||||
ap.stroke_mut().unwrap().brush = Some("10 pt. Oval".into());
|
||||
let mut n = Node::path(d.alloc_id(), shapes::line((20.0, 50.0).into(), (180.0, 50.0).into()), ap);
|
||||
n.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({}), visible: true });
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
let r = Renderer::new().render(&d, 200, 100, Affine::IDENTITY, &RenderOptions { background: None, artboards: false, ..Default::default() });
|
||||
assert!(pixel(&r, 100, 52)[3] > 128, "{:?}", pixel(&r, 100, 52));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_takes_in_type_that_its_own_fills_effects_move() {
|
||||
// "IIII" is set at 40 pt from x 5 pt with a red fill of its own under the characters, and a
|
||||
// Transform effect on that fill moves it 100 pt to the right.
|
||||
let typed = |fx: &[Effect]| {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let style = vectorcraft_doc::text::CharStyle { size: 40.0, fill: Paint::solid(Color::BLACK), ..Default::default() };
|
||||
let text = vectorcraft_doc::text::TextObject::point(Point::new(5.0, 60.0), "IIII", style);
|
||||
let mut n = Node::new(d.alloc_id(), vectorcraft_doc::NodeKind::Text(Box::new(text)));
|
||||
let mut fill = vectorcraft_doc::FillLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
fill.effects.push(Effect { id: "distort.transform".into(), params: json!({"moveH": 100}), visible: true });
|
||||
n.appearance.items.push(vectorcraft_doc::AppearanceItem::Fill(fill));
|
||||
n.appearance.set_contents_at(1);
|
||||
n.appearance.effects = fx.to_vec();
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
Renderer::new().render(&d, 200, 100, Affine::IDENTITY, &RenderOptions { background: None, artboards: false, ..Default::default() })
|
||||
};
|
||||
// The pixels right of x 100 that the moved fill paints.
|
||||
let moved = |r: &Rendered| (0..200 * 100).filter(|i| i % 200 >= 100 && pixel(r, i % 200, i / 200)[0] > 128).count();
|
||||
assert!(moved(&typed(&[])) > 50);
|
||||
let rasterize = Effect { id: "rasterize".into(), params: json!({"useDocumentResolution": false, "resolution": 72}), visible: true };
|
||||
assert!(moved(&typed(&[rasterize])) > 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_fills_its_area_on_white_and_add_around_grows_it() {
|
||||
let circle = || shapes::ellipse(Rect::new(10.0, 10.0, 30.0, 30.0));
|
||||
let at = |params: Value| draw(&doc(circle(), &[("rasterize", params)], 72.0), 10.0, 400);
|
||||
let white = at(json!({"background": "white"}));
|
||||
// Inside the bounds, outside the circle: white; outside the bounds: nothing.
|
||||
assert_eq!((pixel(&white, 105, 105), pixel(&white, 200, 200)), ([255; 4], [255, 0, 0, 255]));
|
||||
assert_eq!(pixel(&white, 95, 200)[3], 0);
|
||||
let clear = at(json!({"background": "transparent"}));
|
||||
assert_eq!((pixel(&clear, 105, 105)[3], pixel(&clear, 200, 200)), (0, [255, 0, 0, 255]));
|
||||
// Add Around Object: 2 pt more on every side.
|
||||
let around = at(json!({"background": "white", "addAround": 2}));
|
||||
assert_eq!((pixel(&around, 85, 200), pixel(&around, 75, 200)[3]), ([255; 4], 0));
|
||||
// A second Rasterize makes its image over the first one's whole pixels, no larger.
|
||||
let twice = draw(&doc(circle(), &[("rasterize", json!({})), ("rasterize", json!({"background": "white"}))], 72.0), 10.0, 400);
|
||||
assert_eq!((pixel(&twice, 105, 105), pixel(&twice, 95, 200)[3], pixel(&twice, 305, 200)[3]), ([255; 4], 0, 0));
|
||||
// A Feather or an Inner Glow after a Rasterize on white works on the image's square edge.
|
||||
let white = ("rasterize", json!({"background": "white"}));
|
||||
let feathered = draw(&doc(circle(), &[white.clone(), ("stylize.feather", json!({"radius": 1}))], 72.0), 10.0, 400);
|
||||
assert!(pixel(&feathered, 106, 106)[3] > 0);
|
||||
let glow = ("stylize.innerGlow", json!({"mode": "normal", "opacity": 100, "blur": 2, "color": "#ff0000", "source": "edge"}));
|
||||
let glowing = draw(&doc(circle(), &[white.clone(), glow.clone()], 72.0), 10.0, 400);
|
||||
assert!(pixel(&glowing, 102, 102)[1] < 200, "{:?}", pixel(&glowing, 102, 102));
|
||||
// With a Rasterize on nothing between them, the glow follows the same edge.
|
||||
let glowing = draw(&doc(circle(), &[white, ("rasterize", json!({})), glow], 72.0), 10.0, 400);
|
||||
assert!(pixel(&glowing, 102, 102)[1] < 200, "{:?}", pixel(&glowing, 102, 102));
|
||||
// Create Clipping Mask: the white stays inside the circle.
|
||||
let clipped = at(json!({"background": "white", "clippingMask": true}));
|
||||
assert_eq!((pixel(&clipped, 105, 105)[3], pixel(&clipped, 200, 200)), (0, [255, 0, 0, 255]));
|
||||
// Inside the outline the white shows where the art paints nothing: a circle stroked only.
|
||||
let mut ring = doc(circle(), &[], 72.0);
|
||||
let l = ring.layers[0].id;
|
||||
let n = Arc::make_mut(&mut ring.children_mut(Some(l)).unwrap()[0]);
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 1.0).into();
|
||||
n.appearance.effects.push(Effect { id: "rasterize".into(), params: json!({"background": "white", "clippingMask": true}), visible: true });
|
||||
let ring = draw(&ring, 10.0, 400);
|
||||
assert_eq!((pixel(&ring, 200, 200), pixel(&ring, 105, 105)[3]), ([255; 4], 0));
|
||||
// A stroke 1 pt wide widens the image by half a point a side, as it widens the visual bounds.
|
||||
let mut stroked = doc(circle(), &[], 72.0);
|
||||
let n = Arc::make_mut(&mut stroked.children_mut(Some(l)).unwrap()[0]);
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 1.0).into();
|
||||
n.appearance.effects.push(Effect {
|
||||
id: "rasterize".into(),
|
||||
params: json!({"background": "white", "resolution": 720, "useDocumentResolution": false}),
|
||||
visible: true,
|
||||
});
|
||||
let stroked = draw(&stroked, 10.0, 400);
|
||||
assert_eq!((pixel(&stroked, 96, 105), pixel(&stroked, 93, 105)[3]), ([255; 4], 0));
|
||||
}
|
||||
Reference in New Issue
Block a user