Files
vectorcraft/crates/render/src/group.rs
T

338 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Transparency groups: groups with opacity or a blend mode, clip groups, opacity masks, the page
//! group. Most are vello layers, which are isolated (their content starts from transparency). A
//! non-isolated group whose content blends is drawn offscreen over a copy of its backdrop, so its
//! children blend with the art below it, then mixed back in (backdrop-copy compositing).
use std::sync::Arc;
use vectorcraft_color::BlendMode as Blend;
use vectorcraft_doc::Node;
use vectorcraft_geom::{Affine, BezPath, FillRule, Rect};
use vello_cpu::peniko::{self, BlendMode, Compose, Mix};
use vello_cpu::{Pixmap, RenderContext};
use crate::{Frame, Renderer, blend_mode, fill_rule};
/// How a group composites onto what is below it.
pub(crate) struct Composite<'a> {
/// Clipping path (document space).
pub clip: Option<(&'a BezPath, FillRule)>,
pub blend: Blend,
pub opacity: f32,
/// Opacity mask (the context's size).
pub mask: Option<vello_cpu::Mask>,
/// The content starts from transparency instead of from the art below.
pub isolated: bool,
/// Blending inside reaches the group's backdrop ([`Node::blends_through`]).
pub blends: bool,
/// Area the content covers (document space); `None` = anywhere.
pub bounds: Option<Rect>,
}
impl Default for Composite<'_> {
fn default() -> Self {
Self { clip: None, blend: Blend::Normal, opacity: 1.0, mask: None, isolated: false, blends: false, bounds: None }
}
}
/// Group content drawn into a context (called once, or twice for a non-isolated group with a blend
/// mode: over the backdrop copy, then alone for its shape).
pub(crate) type Content<'d> = dyn FnMut(&mut Renderer, &mut RenderContext, &Frame) + 'd;
/// A pixel region of a context: origin and size.
#[derive(Clone, Copy)]
struct Region {
x: u16,
y: u16,
w: u16,
h: u16,
}
impl Renderer {
/// Draw a group's content with `draw` and composite it as `c` says. Blending content is drawn
/// offscreen: over a copy of the backdrop when the group isn't isolated (possible only with
/// no layer open around it, else it is drawn isolated), from transparency when it is (so
/// non-isolated groups inside can copy their backdrop).
pub(crate) fn group(&mut self, ctx: &mut RenderContext, f: &Frame, c: Composite, draw: &mut Content) {
if c.blends
&& !c.isolated
&& c.opacity >= 1.0
&& c.blend == Blend::Normal
&& c.mask.is_none()
&& let Some((path, rule)) = c.clip
{
// A plain clip clips each drawing instead of compositing a layer: blending inside
// reaches the art below directly.
ctx.set_transform(f.view);
ctx.set_fill_rule(fill_rule(rule));
ctx.push_clip_path(path);
self.clip_paths.push((path.clone(), rule, f.view));
draw(self, ctx, f);
self.clip_paths.pop();
ctx.pop_clip_path();
return;
}
if c.blends && (c.isolated || self.nested == 0) {
return self.group_offscreen(ctx, f, c, draw);
}
ctx.set_transform(f.view);
if let Some((_, rule)) = c.clip {
ctx.set_fill_rule(fill_rule(rule));
}
ctx.push_layer(c.clip.map(|(p, _)| p), Some(blend_mode(c.blend)), Some(c.opacity), c.mask, None);
self.nested += 1;
// The layer starts from transparency: no backdrop for the content.
let backdrop = self.backdrop.take();
draw(self, ctx, f);
self.backdrop = backdrop;
self.nested -= 1;
ctx.pop_layer();
}
fn group_offscreen(&mut self, ctx: &mut RenderContext, f: &Frame, c: Composite, draw: &mut Content) {
let Composite { clip, blend, opacity, mask, isolated, bounds, .. } = c;
let Some(r) = region(ctx, f, bounds) else { return };
let view = Affine::translate((-(r.x as f64), -(r.y as f64))) * f.view;
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, r.w as f64, r.h as f64));
let sub = Frame { mt: false, view, visible, ..*f };
let backdrop = (!isolated).then(|| self.flatten(ctx, r));
let content = self.offscreen(r, &sub, backdrop.clone(), draw);
let at = (r.x as f64, r.y as f64);
let push = |ctx: &mut RenderContext, mode: BlendMode, mask: Option<vello_cpu::Mask>| {
ctx.set_transform(f.view);
if let Some((_, rule)) = clip {
ctx.set_fill_rule(fill_rule(rule));
}
ctx.push_layer(clip.map(|(p, _)| p), Some(mode), Some(opacity), mask, None);
};
match backdrop {
// The backdrop under the content, mixed with it by opacity, mask and clip: take that
// share of the backdrop out, then add the same share of the content.
Some(_) if blend == Blend::Normal => {
push(ctx, BlendMode::new(Mix::Normal, Compose::DestOut), mask.clone());
ctx.set_transform(Affine::IDENTITY);
ctx.set_paint(peniko::Color::BLACK);
ctx.fill_rect(&Rect::new(at.0, at.1, at.0 + r.w as f64, at.1 + r.h as f64));
ctx.pop_layer();
push(ctx, BlendMode::new(Mix::Normal, Compose::Plus), mask);
draw_pixmap(ctx, Arc::new(content), at);
}
// A blend mode applies to the group as an object of its own: take the backdrop's share
// out of the content (it needs the content's own coverage), then blend the rest.
Some(b) => {
let alone = self.offscreen(r, &sub, None, draw);
push(ctx, blend_mode(blend), mask);
draw_pixmap(ctx, Arc::new(remove_backdrop(content, &alone, &b)), at);
}
None => {
push(ctx, blend_mode(blend), mask);
draw_pixmap(ctx, Arc::new(content), at);
}
}
ctx.pop_layer();
}
/// Render what `ctx` holds so far, and start it again from that picture (so later drawing sees
/// it unchanged). Returns the pixels of `r`.
fn flatten(&mut self, ctx: &mut RenderContext, r: Region) -> Arc<Pixmap> {
let all = self.finish(ctx, self.raster);
ctx.reset();
let crop = if (r.x, r.y, r.w, r.h) == (0, 0, all.width(), all.height()) { None } else { Some(Arc::new(cropped(&all, r))) };
let all = Arc::new(all);
// With 8-bit compositing, rendering over the picture gives the pixels that drawing it back
// as an image gives: the context starts again empty, and `finish` renders what it draws
// next over the picture. With floating-point compositing the picture is drawn back.
if self.raster.render_mode == vello_cpu::RenderMode::OptimizeSpeed {
self.picture = Some(all.clone());
} else {
draw_pixmap(ctx, all.clone(), (0.0, 0.0));
}
for (path, rule, xf) in &self.clip_paths {
ctx.set_transform(*xf);
ctx.set_fill_rule(fill_rule(*rule));
ctx.push_clip_path(path);
}
crop.unwrap_or(all)
}
/// Render what `ctx` holds into a pixmap of its size: over the picture [`Self::flatten`]
/// started it again from, if any, else over transparency.
pub(crate) fn finish(&mut self, ctx: &mut RenderContext, settings: vello_cpu::RasterizerSettings) -> Pixmap {
ctx.flush();
let Some(below) = self.picture.take() else {
let mut pm = Pixmap::new(ctx.width(), ctx.height());
ctx.render_with(&mut pm, &mut self.resources, settings);
return pm;
};
let mut pm = Arc::unwrap_or_clone(below);
let over = vello_cpu::RasterizerSettings { composite_mode: vello_cpu::CompositeMode::SrcOver, ..settings };
ctx.render_with(&mut pm, &mut self.resources, over);
pm
}
/// `draw` rendered into a new context of region `r` (frame `sub`), over `backdrop` if any.
fn offscreen(&mut self, r: Region, sub: &Frame, backdrop: Option<Arc<Pixmap>>, draw: &mut Content) -> Pixmap {
let mut ctx = sub.offscreen_context(r.w, r.h);
if let Some(b) = &backdrop {
draw_pixmap(&mut ctx, b.clone(), (0.0, 0.0));
}
let outer = (
std::mem::replace(&mut self.nested, 0),
std::mem::replace(&mut self.backdrop, backdrop),
std::mem::take(&mut self.clip_paths),
self.picture.take(),
);
draw(self, &mut ctx, sub);
let pm = self.finish(&mut ctx, self.raster);
(self.nested, self.backdrop, self.clip_paths, self.picture) = outer;
pm
}
/// Run `draw` inside layers (or into another context) it opens itself: groups drawn there can't
/// copy their backdrop, and see none.
pub(crate) fn inside_layer<T>(&mut self, draw: impl FnOnce(&mut Self) -> T) -> T {
self.nested += 1;
let backdrop = self.backdrop.take();
let out = draw(self);
self.backdrop = backdrop;
self.nested -= 1;
out
}
/// Whether blending inside `n` reaches its backdrop ([`Node::blends_through`], cached per
/// container).
pub(crate) fn blends_through(&mut self, n: &Node) -> bool {
n.blends_through_with(&mut |c| self.blends_of(c))
}
/// [`Self::blends_through`] of the group `children` make up.
pub(crate) fn children_blend(&mut self, children: &[Arc<Node>]) -> bool {
Node::children_blend(children, &mut |c| self.blends_of(c))
}
fn blends_of(&mut self, a: &Arc<Node>) -> bool {
if !a.is_container() {
return a.blends_through();
}
let key = Arc::as_ptr(a) as usize;
if let Some(e) = self.blends.get_mut(&key)
&& Arc::ptr_eq(&e.0, a)
{
e.2 = self.stamp;
return e.1;
}
let v = a.blends_through_with(&mut |c| self.blends_of(c));
if self.blends.len() > 1024 {
let g = self.stamp;
self.blends.retain(|_, e| g - e.2 <= 3);
}
self.blends.insert(key, (a.clone(), v, self.stamp));
v
}
/// Draw knockout element `c` in a layer composited with `compose`: as its knockout shape
/// (`DestOut` erases what the elements below drew there, `SrcOver` draws the shape), or as
/// itself (`Plus` adds it).
pub(crate) fn knockout_pass(&mut self, ctx: &mut RenderContext, f: &Frame, c: &Arc<Node>, compose: Compose) {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, compose)), None, None, None);
self.nested += 1;
let shape = compose != Compose::Plus && !c.knockout_shape;
self.shape_of = if shape { Arc::as_ptr(c) as usize } else { 0 };
self.draw_arc(ctx, f, c);
self.shape_of = 0;
self.nested -= 1;
ctx.pop_layer();
}
/// Add knockout element `c` of a non-isolated knockout group drawn over a copy of its backdrop
/// (`backdrop`, in `ctx`'s pixels), once its `DestOut` pass has erased what the elements below
/// it drew: `c` composited with the backdrop at a shape of 1, times its knockout shape
/// (ISO 32000-1 §11.4.6). That is `c` drawn over the backdrop, less (1 − shape) × backdrop.
/// `c` over the backdrop and its shape are drawn offscreen over `c`'s bounds, and `c` is drawn
/// as in the other knockout passes (groups in it can't copy their backdrop).
pub(crate) fn knockout_over(&mut self, ctx: &mut RenderContext, f: &Frame, c: &Arc<Node>, backdrop: &Arc<Pixmap>) {
let bounds = self.bounds_of(f.symbols, c);
let Some(r) = region(ctx, f, bounds) else { return };
let view = Affine::translate((-(r.x as f64), -(r.y as f64))) * f.view;
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, r.w as f64, r.h as f64));
let sub = Frame { mt: false, view, visible, ..*f };
let whole = (r.x, r.y, r.w, r.h) == (0, 0, backdrop.width(), backdrop.height());
let below = if whole { backdrop.clone() } else { Arc::new(cropped(backdrop, r)) };
let over = self.offscreen(r, &sub, Some(below.clone()), &mut |s, cx, fr| s.inside_layer(|s| s.draw_arc(cx, fr, c)));
let shape = self.offscreen(r, &sub, None, &mut |s, cx, fr| s.knockout_pass(cx, fr, c, Compose::SrcOver));
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::Plus)), None, None, None);
draw_pixmap(ctx, Arc::new(inside_shape(over, &shape, &below)), (r.x as f64, r.y as f64));
ctx.pop_layer();
}
}
/// Draw `pm` with its top-left corner at pixel `at` (whole pixels: copied exactly).
pub(crate) fn draw_pixmap(ctx: &mut RenderContext, pm: Arc<Pixmap>, at: (f64, f64)) {
let (w, h) = (pm.width() as f64, pm.height() as f64);
#[cfg(test)]
PIXELS_DRAWN.with(|n| n.set(n.get() + u64::from(pm.width()) * u64::from(pm.height())));
ctx.set_transform(Affine::translate(at));
// The pixmap maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern,
// radial or freeform gradient) would resample it (#2426).
ctx.reset_paint_transform();
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() });
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
ctx.set_transform(Affine::IDENTITY);
}
#[cfg(test)]
thread_local! {
/// The pixels [`draw_pixmap`] has drawn on this thread.
pub(crate) static PIXELS_DRAWN: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// The pixel region of `ctx` that `bounds` (document space) covers, with a margin for
/// antialiasing; `None` when it is off the context.
fn region(ctx: &RenderContext, f: &Frame, bounds: Option<Rect>) -> Option<Region> {
let full = Rect::new(0.0, 0.0, ctx.width() as f64, ctx.height() as f64);
let r = match bounds {
Some(b) => f.view.transform_rect_bbox(b).inflate(2.0, 2.0).intersect(full),
None => full,
};
let (x0, y0, x1, y1) = (r.x0.floor().max(0.0), r.y0.floor().max(0.0), r.x1.ceil().min(full.x1), r.y1.ceil().min(full.y1));
(x1 > x0 && y1 > y0).then_some(Region { x: x0 as u16, y: y0 as u16, w: (x1 - x0) as u16, h: (y1 - y0) as u16 })
}
/// The pixels of region `r` of `pm`.
fn cropped(pm: &Pixmap, r: Region) -> Pixmap {
let mut out = Pixmap::new(r.w, r.h);
let (src_w, w) = (pm.width() as usize, r.w as usize);
for row in 0..r.h as usize {
let from = (r.y as usize + row) * src_w + r.x as usize;
out.data_mut()[row * w..(row + 1) * w].copy_from_slice(&pm.data()[from..from + w]);
}
out
}
/// The group's own colour from `content` (the group drawn over `backdrop`) and `alone` (drawn
/// over transparency, for its coverage): content = group + (1 − group alpha) × backdrop, all
/// premultiplied.
fn remove_backdrop(mut content: Pixmap, alone: &Pixmap, backdrop: &Pixmap) -> Pixmap {
for ((c, a), b) in content.data_mut().iter_mut().zip(alone.data()).zip(backdrop.data()) {
let keep = 255 - a.a as u32;
let sub = |v: u8, bv: u8| (v as u32).saturating_sub((keep * bv as u32 + 127) / 255).min(a.a as u32) as u8;
(c.r, c.g, c.b, c.a) = (sub(c.r, b.r), sub(c.g, b.g), sub(c.b, b.b), a.a);
}
content
}
/// What a knockout element adds once its knockout shape has erased what was below it: `over` (the
/// element drawn over `backdrop`) less (1 − shape alpha) × `backdrop`, with `shape` the knockout
/// shape, all premultiplied. Each color is kept within the alpha left.
fn inside_shape(mut over: Pixmap, shape: &Pixmap, backdrop: &Pixmap) -> Pixmap {
for ((o, s), b) in over.data_mut().iter_mut().zip(shape.data()).zip(backdrop.data()) {
let outside = 255 - s.a as u32;
let sub = |v: u8, bv: u8| (v as u32).saturating_sub((outside * bv as u32 + 127) / 255);
let a = sub(o.a, b.a);
(o.r, o.g, o.b, o.a) = (sub(o.r, b.r).min(a) as u8, sub(o.g, b.g).min(a) as u8, sub(o.b, b.b).min(a) as u8, a as u8);
}
over
}