mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
Render: a moved path reuses its gradient stroke slices
A path whose stroke has a gradient along or across it kept its slices under the node's address, so every nudge made the slices again for the whole path, although its stroke outline was already reused. The renderer now keeps the key of a stroke's latest slices under the object's id, the stroke's place and the tolerance level, and when the object has only moved (same stroke and gradient, same fill rule, anchors moved by one offset) it draws those slices moved by the offset, translating only the slices that reach the piece being clipped. Scale, rotate, an anchor edit or a change to the stroke or its gradient makes the slices again. The pixels are the same.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use super::options::{AntiAlias, RenderOptions};
|
||||
use super::threads::single_threaded_context;
|
||||
use super::{Affine, Arc, BezPath, Document, FillRule, HashMap, Ink, Node, Rect, RenderContext, StrokeLayer, SymbolBoxes, TextObject, live};
|
||||
use super::{Affine, Arc, BezPath, Document, FillRule, HashMap, Ink, Node, Rect, RenderContext, StrokeLayer, SymbolBoxes, TextObject, kurbo, live};
|
||||
|
||||
/// Hasher for pointer-keyed caches: a multiply-xorshift (SipHash showed up in frame profiles).
|
||||
#[derive(Default, Clone, Copy)]
|
||||
@@ -51,10 +51,25 @@ pub(crate) struct StrokeEntry {
|
||||
/// The slices of a stroke whose gradient runs along or across it, each with its paint and bounds.
|
||||
pub(crate) type Slices = Arc<Vec<(BezPath, vectorcraft_color::Paint, Rect)>>;
|
||||
|
||||
/// [`Slices`] cached for a stroke: (owning node, slices, last frame used).
|
||||
/// The key of cached [`Slices`]: ([`Renderer::stroke_owner`] of the stroke's object, stroke layer
|
||||
/// address, tolerance level).
|
||||
pub(crate) type SliceKey = (usize, usize, i32);
|
||||
|
||||
/// What the [`SliceKey`] of the latest slices of an object's stroke is found under once the object
|
||||
/// has changed: (the object's id, the stroke's place in its appearance items, tolerance level).
|
||||
pub(crate) type MovedSliceKey = (u64, usize, i32);
|
||||
|
||||
/// [`Slices`] cached for a stroke: (owning node, the node whose path and stroke the slices were
|
||||
/// made from, either the owning node or an earlier copy of it that the owning node is moved from,
|
||||
/// the slices, the offset they are drawn moved by, the fill rule they were made with, whether they
|
||||
/// were made along the path [`Node::stroke_path`] makes from that node, last frame used).
|
||||
pub(crate) struct SliceEntry {
|
||||
pub(crate) node: Arc<Node>,
|
||||
pub(crate) made: Arc<Node>,
|
||||
pub(crate) slices: Slices,
|
||||
pub(crate) by: kurbo::Vec2,
|
||||
pub(crate) rule: FillRule,
|
||||
pub(crate) own: bool,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,11 @@ pub struct Renderer {
|
||||
clip_paints: FrameCache<usize, ClipPaintEntry>,
|
||||
/// Gradients along or across strokes (see [`Self::fill_path_gradient`]), keyed by
|
||||
/// ([`Self::stroke_owner`], stroke layer address, tolerance level).
|
||||
stroke_slices: PtrMap<(usize, usize, i32), SliceEntry>,
|
||||
stroke_slices: PtrMap<frame::SliceKey, SliceEntry>,
|
||||
/// The key in `stroke_slices` of the latest slices of each stroke of an object, under its
|
||||
/// [`frame::MovedSliceKey`]: a moved copy of the object draws those slices moved
|
||||
/// ([`Self::moved_slices`]).
|
||||
moved_slices: PtrMap<frame::MovedSliceKey, frame::SliceKey>,
|
||||
/// What the image keys colour adjustments give their recoloured images name: the image
|
||||
/// recoloured and the adjustment, with its hash (see [`Self::adjusted_image`]).
|
||||
adjusts: HashMap<String, (String, u64, vectorcraft_effects::ColorMap)>,
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::{
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use paint::{STROKES_OUTLINED, anchors_moved, offset_between};
|
||||
pub(crate) use paint::{SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between};
|
||||
|
||||
mod images;
|
||||
mod nodes;
|
||||
@@ -76,6 +76,7 @@ impl Renderer {
|
||||
twice_drawn: 0,
|
||||
clip_paints: FrameCache::new(lru::CLIP_BUDGET, 256),
|
||||
stroke_slices: PtrMap::default(),
|
||||
moved_slices: PtrMap::default(),
|
||||
adjusts: HashMap::new(),
|
||||
dim_images: None,
|
||||
inline_depth: 0,
|
||||
@@ -195,6 +196,8 @@ impl Renderer {
|
||||
}
|
||||
if self.stroke_slices.len() > 256 {
|
||||
self.stroke_slices.retain(|_, e| g - e.stamp <= 3);
|
||||
let slices = &self.stroke_slices;
|
||||
self.moved_slices.retain(|_, k| slices.contains_key(k));
|
||||
}
|
||||
if self.shadows.len() > 256 {
|
||||
self.shadows.retain(|_, e| g - e.stamp <= 3);
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
Slices, StrokeAlign, StrokeEntry, StrokeLayer, TextGeom, blend_mode, drawable, effects, fill_rule, fx, kurbo, lru, owns_stroke, peniko,
|
||||
rects_overlap,
|
||||
};
|
||||
use crate::frame::{MovedKey, StrokeKey};
|
||||
use crate::frame::{MovedKey, MovedSliceKey, StrokeKey};
|
||||
use crate::paint;
|
||||
|
||||
impl Renderer {
|
||||
@@ -164,7 +164,9 @@ impl Renderer {
|
||||
/// Paint the line and arrowheads (`pieces`) of stroke `st` of `bp` (filled with `rule`) with
|
||||
/// its gradient `g` laid along or across the path ([`effects::stroke::gradient_slices`]). Each
|
||||
/// piece clips the slices, grown a pixel into each other and each replacing what it covers, so
|
||||
/// neighbours meet without seams and translucent colours paint once.
|
||||
/// neighbours meet without seams and translucent colours paint once. The slices of a stroke
|
||||
/// of `self.cur` are kept in `stroke_slices`, and a moved copy of the object draws the slices
|
||||
/// made before it moved, moved by the same offset ([`Self::moved_slices`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fill_path_gradient(
|
||||
&mut self,
|
||||
@@ -180,6 +182,8 @@ impl Renderer {
|
||||
) {
|
||||
let level = (f.px * 0.25).log2().floor() as i32;
|
||||
let make = || -> Slices {
|
||||
#[cfg(test)]
|
||||
SLICES_MADE.with(|n| n.set(n.get() + 1));
|
||||
let tol = 2f64.powi(level);
|
||||
// Reach a pixel past the stroke, so its antialiased edge has colour under it.
|
||||
let slices = effects::stroke::gradient_slices(bp, rule, st, tol, f.px + tol, f.px);
|
||||
@@ -193,22 +197,37 @@ impl Renderer {
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
let slices = match self.cur.clone().filter(|n| owns_stroke(n, st)) {
|
||||
let (slices, by) = match self.cur.clone().filter(|n| owns_stroke(n, st)) {
|
||||
Some(node) => {
|
||||
let key = (self.stroke_owner(Arc::as_ptr(&node) as usize), st as *const StrokeLayer as usize, level);
|
||||
match self.stroke_slices.get_mut(&key) {
|
||||
Some(e) if Arc::ptr_eq(&e.node, &node) => {
|
||||
e.stamp = self.stamp;
|
||||
e.slices.clone()
|
||||
(e.slices.clone(), e.by)
|
||||
}
|
||||
_ => {
|
||||
let slices = make();
|
||||
self.stroke_slices.insert(key, SliceEntry { node, slices: slices.clone(), stamp: self.stamp });
|
||||
slices
|
||||
// Under its object's id, the stroke's place and the tolerance level, the key
|
||||
// of the stroke's latest slices is kept, except for art made again in every
|
||||
// frame.
|
||||
let place = node.appearance.items.iter().position(|i| matches!(i, AppearanceItem::Stroke(s) if std::ptr::eq(s, st)));
|
||||
let moved = place.filter(|_| !self.made_again).map(|place| (node.id.0, place, level));
|
||||
let kept = moved.and_then(|mk| self.moved_slices(mk, &node, st, rule, bp));
|
||||
let (made, slices, by, own) = match kept {
|
||||
Some((made, slices, by)) => (made, slices, by, true),
|
||||
None => {
|
||||
let own = moved.is_some() && self.own_path(&node, bp);
|
||||
(node.clone(), make(), kurbo::Vec2::ZERO, own)
|
||||
}
|
||||
};
|
||||
self.stroke_slices.insert(key, SliceEntry { node, made, slices: slices.clone(), by, rule, own, stamp: self.stamp });
|
||||
if let Some(mk) = moved {
|
||||
self.moved_slices.insert(mk, key);
|
||||
}
|
||||
(slices, by)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => make(),
|
||||
None => (make(), kurbo::Vec2::ZERO),
|
||||
};
|
||||
let line = match self.cached_stroke(f, chars, bp, &pieces.line, st, width) {
|
||||
Some(o) => o,
|
||||
@@ -223,8 +242,18 @@ impl Renderer {
|
||||
ctx.push_clip_layer(clip);
|
||||
ctx.set_blend_mode(BlendMode::new(Mix::Normal, Compose::Copy));
|
||||
for (shape, paint, b) in slices.iter() {
|
||||
if rects_overlap(*b, cb) && paint::set_paint(ctx, paint, *b, f) {
|
||||
ctx.fill_path(shape);
|
||||
let b = *b + by;
|
||||
if !rects_overlap(b, cb) {
|
||||
continue;
|
||||
}
|
||||
if by == kurbo::Vec2::ZERO {
|
||||
if paint::set_paint(ctx, paint, b, f) {
|
||||
ctx.fill_path(shape);
|
||||
}
|
||||
} else if paint::set_paint(ctx, &moved_paint(paint, by), b, f) {
|
||||
let mut shape = shape.clone();
|
||||
shape.apply_affine(Affine::translate(by));
|
||||
ctx.fill_path(&shape);
|
||||
}
|
||||
}
|
||||
ctx.set_blend_mode(BlendMode::default());
|
||||
@@ -364,6 +393,38 @@ impl Renderer {
|
||||
Some((Arc::new(outline), crop))
|
||||
}
|
||||
|
||||
/// The slices of stroke `st` of `node` along `bp` (filled with `rule`), made from the latest
|
||||
/// slices kept under `mk` ([`MovedSliceKey`]) when those were made along their object's own
|
||||
/// path, their object is `node` changed, its stroke there is `st` and its fill rule `rule`, and
|
||||
/// `bp` is its path moved (to within rounding): the node they were made from, those slices,
|
||||
/// and the offset from that node to `bp`, which the slices are drawn moved by. The anchors of
|
||||
/// the two objects are compared first ([`anchors_moved`]), and the earlier path is made again
|
||||
/// from its object only when they match. `None` when there are no such slices.
|
||||
fn moved_slices(
|
||||
&self,
|
||||
mk: MovedSliceKey,
|
||||
node: &Node,
|
||||
st: &StrokeLayer,
|
||||
rule: FillRule,
|
||||
bp: &BezPath,
|
||||
) -> Option<(Arc<Node>, Slices, kurbo::Vec2)> {
|
||||
let e = self.stroke_slices.get(self.moved_slices.get(&mk)?)?;
|
||||
if !e.own || e.rule != rule || e.made.id != node.id {
|
||||
return None;
|
||||
}
|
||||
let Some(AppearanceItem::Stroke(old)) = e.made.appearance.items.get(mk.1) else {
|
||||
return None;
|
||||
};
|
||||
if !std::ptr::eq(old, st) && old != st {
|
||||
return None;
|
||||
}
|
||||
if !anchors_moved(&e.made, node) {
|
||||
return None;
|
||||
}
|
||||
let by = offset_between(&e.made.stroke_path()?, bp)?;
|
||||
Some((e.made.clone(), e.slices.clone(), by))
|
||||
}
|
||||
|
||||
/// What the `strokes`, `char_strokes` and `stroke_slices` keys hold for a stroke owner at
|
||||
/// `address` (`self.cur` or kept glyph geometry): the address, or 0 while art made again in
|
||||
/// every frame is drawn ([`Self::made_again`]).
|
||||
@@ -382,6 +443,24 @@ thread_local! {
|
||||
pub(crate) static CROP_STROKES: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
|
||||
/// How many stroke outlines of nodes [`Renderer::cached_stroke`] has made on this thread.
|
||||
pub(crate) static STROKES_OUTLINED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// How many sets of gradient stroke slices [`Renderer::fill_path_gradient`] has made on this
|
||||
/// thread.
|
||||
pub(crate) static SLICES_MADE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Paint `p` of a slice ([`Slices`]) moved by `by`: a gradient's start, end and focal point moved.
|
||||
fn moved_paint(p: &vectorcraft_color::Paint, by: kurbo::Vec2) -> vectorcraft_color::Paint {
|
||||
let mut p = p.clone();
|
||||
if let vectorcraft_color::Paint::Gradient(g) = &mut p
|
||||
&& let Some(geom) = &mut g.geom
|
||||
{
|
||||
geom.start += by;
|
||||
geom.end += by;
|
||||
if let Some(focal) = &mut geom.focal {
|
||||
*focal += by;
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// The offset that moves path `was` onto path `now`, when `now` is `was` moved (each point within
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! A moved object draws its stroke from the outline made for it before it moved, moved by the same
|
||||
//! A moved object draws its stroke from the outline made for it before it moved, and a gradient
|
||||
//! along or across its stroke from the slices made for it before it moved, moved by the same
|
||||
//! offset, with the pixels a new renderer draws. A change other than a move outlines the stroke
|
||||
//! again.
|
||||
//! and makes the slices again.
|
||||
|
||||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
||||
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, ArrowAlign, Arrowhead, Dash, FillLayer, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeLayer, WidthProfile,
|
||||
Appearance, AppearanceItem, ArrowAlign, Arrowhead, Dash, FillLayer, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeGradientMode, StrokeLayer,
|
||||
WidthProfile,
|
||||
};
|
||||
use vectorcraft_geom::{PathData, Point, Vec2};
|
||||
|
||||
use crate::renderer::{STROKES_OUTLINED, anchors_moved, offset_between};
|
||||
use crate::renderer::{SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between};
|
||||
use crate::*;
|
||||
|
||||
const SIZE: u32 = 200;
|
||||
@@ -326,6 +328,119 @@ fn points_each_near_the_one_before_are_checked_against_one_offset() {
|
||||
assert!(!anchors_moved(&node(&was), &node(&edited)));
|
||||
}
|
||||
|
||||
/// How many sets of gradient stroke slices the renderers on this thread have made.
|
||||
fn sliced() -> usize {
|
||||
SLICES_MADE.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// A stroke of `width` whose black to blue gradient runs `mode` (along or across the path).
|
||||
fn gradient_stroke(width: f64, mode: StrokeGradientMode) -> StrokeLayer {
|
||||
let g = Gradient::new(GradientKind::Linear, vec![GradientStop::new(0.0, Color::BLACK), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))]);
|
||||
let mut st = stroke(width, LineJoin::Round, LineCap::Butt);
|
||||
st.paint = Paint::Gradient(Box::new(GradientPaint::new(g)));
|
||||
st.gradient_mode = mode;
|
||||
st
|
||||
}
|
||||
|
||||
/// The documents of the gradient cases: open and closed paths, short and long (their line outlined
|
||||
/// near the view), each with a gradient along and across the stroke, with the point the view is
|
||||
/// centered on.
|
||||
fn gradient_cases() -> Vec<(String, (Document, NodeId), Point)> {
|
||||
let mut out = vec![];
|
||||
for mode in [StrokeGradientMode::Along, StrokeGradientMode::Across] {
|
||||
let only = || vec![AppearanceItem::Stroke(gradient_stroke(12.0, mode))];
|
||||
out.push((format!("a short open path, {mode:?}"), doc(&zigzag(20), only()), Point::new(70.0, 15.0)));
|
||||
out.push((format!("a short closed path, {mode:?}"), doc(&ring(40, 80.0), only()), Point::new(80.0, 0.0)));
|
||||
out.push((format!("a long open path, {mode:?}"), doc(&zigzag(3000), only()), Point::new(12_000.0, 15.0)));
|
||||
out.push((format!("a long closed path, {mode:?}"), doc(&ring(4000, 5000.0), only()), Point::new(0.0, 5000.0)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Draws `d` after `change` with `r` and with a new renderer, checks that the two draw the same
|
||||
/// pixels, and returns how many sets of slices `r` made.
|
||||
fn after_sliced(r: &mut Renderer, d: &mut Document, id: NodeId, at: Point, change: impl FnOnce(&mut Node)) -> usize {
|
||||
change(d.node_mut(id).unwrap());
|
||||
let before = sliced();
|
||||
let img = render(r, d, at);
|
||||
let made = sliced() - before;
|
||||
assert!(img.pixels == render(&mut Renderer::new(), d, at).pixels, "the same pixels as a new renderer");
|
||||
assert!(img.pixels.chunks(4).any(|p| p[0] < 128), "the stroke shows");
|
||||
made
|
||||
}
|
||||
|
||||
/// Sets the color of the last stop of each stroke's gradient in `n`.
|
||||
fn recolor(n: &mut Node, color: Color) {
|
||||
restroke(n, |s| {
|
||||
if let Paint::Gradient(g) = &mut s.paint
|
||||
&& let Some(stop) = g.gradient.stops.last_mut()
|
||||
{
|
||||
stop.color = color;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_moved_path_draws_its_gradient_stroke_from_the_slices_made_before() {
|
||||
for (what, (mut d, id), at) in gradient_cases() {
|
||||
let mut r = Renderer::new();
|
||||
render(&mut r, &d, at);
|
||||
for by in [Vec2::new(1.0, 0.0), Vec2::new(0.25, -0.5), Vec2::new(-3.0, 2.0), Vec2::new(0.1, 0.0)] {
|
||||
let made = after_sliced(&mut r, &mut d, id, at, |n| n.transform(Affine::translate(by), true));
|
||||
assert_eq!(made, 0, "{what}, moved by {by:?}: no new slices");
|
||||
}
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, |_| {}), 0, "{what}: drawn again after the moves");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_changed_otherwise_makes_its_gradient_stroke_slices_again() {
|
||||
let edit_anchor = |n: &mut Node| {
|
||||
let NodeKind::Path { path, .. } = &mut n.kind else {
|
||||
panic!("not a path");
|
||||
};
|
||||
let mut els = path.to_bezpath().elements().to_vec();
|
||||
if let Some(kurbo::PathEl::LineTo(p)) = els.get_mut(2) {
|
||||
*p += Vec2::new(0.0, 3.0);
|
||||
}
|
||||
*path = PathData::from_bezpath(&BezPath::from_vec(els));
|
||||
};
|
||||
for (what, (mut d, id), at) in gradient_cases() {
|
||||
let mut r = Renderer::new();
|
||||
render(&mut r, &d, at);
|
||||
let nudge = |n: &mut Node| n.transform(Affine::translate((0.5, 0.0)), true);
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, nudge), 0, "{what}: moved");
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, edit_anchor), 1, "{what}: an anchor moved");
|
||||
// A move after the edit uses the slices made for the edited path.
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, nudge), 0, "{what}: moved after the edit");
|
||||
// Scaled and rotated about the view's center, stroke weights kept.
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, |n| n.transform(Affine::scale_about(1.001, at), false)), 1, "{what}: scaled");
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, |n| n.transform(Affine::rotate_about(0.001, at), false)), 1, "{what}: rotated");
|
||||
assert_eq!(after_sliced(&mut r, &mut d, id, at, |n| recolor(n, Color::rgb(0.0, 0.5, 0.0))), 1, "{what}: another gradient");
|
||||
// Moved with the gradient changed back: the slices of that gradient made before are not used.
|
||||
assert_eq!(
|
||||
after_sliced(&mut r, &mut d, id, at, |n| {
|
||||
recolor(n, Color::rgb(0.0, 0.0, 1.0));
|
||||
nudge(n);
|
||||
}),
|
||||
1,
|
||||
"{what}: the gradient changed back and moved"
|
||||
);
|
||||
// Moved with the gradient laid the other way.
|
||||
let turn = |s: &mut StrokeLayer| {
|
||||
s.gradient_mode = if s.gradient_mode == StrokeGradientMode::Along { StrokeGradientMode::Across } else { StrokeGradientMode::Along }
|
||||
};
|
||||
assert_eq!(
|
||||
after_sliced(&mut r, &mut d, id, at, |n| {
|
||||
restroke(n, turn);
|
||||
nudge(n);
|
||||
}),
|
||||
1,
|
||||
"{what}: the gradient laid the other way and moved"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_moved_out_of_the_region_its_outline_was_cropped_to_is_outlined_again() {
|
||||
let (mut d, id) = doc(&zigzag(3000), vec![AppearanceItem::Stroke(stroke(6.0, LineJoin::Miter, LineCap::Square))]);
|
||||
|
||||
Reference in New Issue
Block a user