//! 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, /// 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, } 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| { 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 { 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>, 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(&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]) -> bool { Node::children_blend(children, &mut |c| self.blends_of(c)) } fn blends_of(&mut self, a: &Arc) -> 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, 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, backdrop: &Arc) { 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, 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 = 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) -> Option { 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 }