mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
M8.16: Envelopes bend images, symbols, gradients, patterns and appearance; warps keep their frame
- Images distort as a raster mesh warp: cut by Fidelity into triangles,
each a clipped image under the affine map of its corners (pieces overlap
slightly, no seams; the renderer paints them without clip layers; SVG
writes the pixels once and <use>s them).
- Symbol instances distort as their symbol's art (the renderer's cache
follows edits to the symbol).
- Distort Appearance bakes strokes (filled outlines) and geometry effects
before bending; Distort Linear Gradients bends linear gradients; Distort
Pattern Fills bends the tiles. Opacity masks of the content bend too.
- Bug fix: a rotated, sheared or reflected warp envelope re-warped along
the page axes. Envelopes now keep their own frame (append-only model
field), so the warp turns with the art; move and scale keep it square.
- Live evaluation shares one set of hooks (vectorcraft_doc::live::Hooks,
vectorcraft_effects::{expand_live, expanded_live_group,
expand_live_deep}) between the canvas, Expand and every exporter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cd99714b42
commit
04fa49413c
@@ -834,6 +834,16 @@ impl Renderer {
|
||||
&& let Some(region) = self.clip_of(clip)
|
||||
{
|
||||
let (fill, stroke) = self.clip_paint_of(clip);
|
||||
// An image inside its own frame painted straight into the clipping path, without
|
||||
// a clip layer (envelopes cut distorted images into many such pieces).
|
||||
if let ([image], None, None) = (rest, &fill, &stroke)
|
||||
&& let NodeKind::Image(im) = &image.kind
|
||||
&& plain_image(image, im, ®ion.0)
|
||||
{
|
||||
self.draw_image_in(ctx, f, im, Some(®ion));
|
||||
self.stats.drawn += 1;
|
||||
return;
|
||||
}
|
||||
let blends = self.blends_through(n);
|
||||
let bounds = if blends { Some(region.0.bounding_box()) } else { None };
|
||||
let comp = Composite { clip: Some((®ion.0, region.1)), blends, bounds, ..Default::default() };
|
||||
@@ -1264,6 +1274,12 @@ impl Renderer {
|
||||
}
|
||||
|
||||
fn draw_image(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject) {
|
||||
self.draw_image_in(ctx, f, im, None);
|
||||
}
|
||||
|
||||
/// Draw an image, or with `area` (a region in the document inside the image's frame, see
|
||||
/// [`plain_image`]) the image painted into that region.
|
||||
fn draw_image_in(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject, area: Option<&(BezPath, FillRule)>) {
|
||||
let rect = Rect::new(0.0, 0.0, im.width as f64, im.height as f64);
|
||||
// Outline mode draws the image's frame, and with Document Setup → Show Images in Outline
|
||||
// Mode its pixels in greyscale under the frame.
|
||||
@@ -1278,10 +1294,20 @@ impl Renderer {
|
||||
let pm = if outline { pm } else { self.ink_image(&cache_key, &pm, f.ink) };
|
||||
let sx = im.width as f64 / pm.width().max(1) as f64;
|
||||
let sy = im.height as f64 / pm.height().max(1) as f64;
|
||||
ctx.set_transform(f.view * im.xf);
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() });
|
||||
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
|
||||
ctx.fill_rect(&rect);
|
||||
match area {
|
||||
Some((bp, rule)) => {
|
||||
ctx.set_transform(f.view);
|
||||
ctx.set_paint_transform(im.xf * Affine::scale_non_uniform(sx, sy));
|
||||
ctx.set_fill_rule(fill_rule(*rule));
|
||||
ctx.fill_path(bp);
|
||||
}
|
||||
None => {
|
||||
ctx.set_transform(f.view * im.xf);
|
||||
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
|
||||
ctx.fill_rect(&rect);
|
||||
}
|
||||
}
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
if outline {
|
||||
@@ -1352,6 +1378,21 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Can image node `n` (`im`) clipped by `region` be painted straight into the region: visible,
|
||||
/// with no transparency, effects or paint of its own, and the region inside the image's frame
|
||||
/// (within a pixel), so no paint past the image's edge shows?
|
||||
fn plain_image(n: &Node, im: &vectorcraft_doc::ImageObject, region: &BezPath) -> bool {
|
||||
if !(n.visible && n.has_default_transparency() && n.appearance.items.is_empty() && n.appearance.effects.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
let det = im.xf.determinant();
|
||||
if !(det.abs() > 1e-12 && det.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
let b = im.xf.inverse().transform_rect_bbox(region.bounding_box());
|
||||
b.x0 >= -1.0 && b.y0 >= -1.0 && b.x1 <= im.width as f64 + 1.0 && b.y1 <= im.height as f64 + 1.0
|
||||
}
|
||||
|
||||
/// Opacity-mask coverage of one premultiplied pixel: luminance, with the area outside the mask
|
||||
/// art black (clip) or white (no clip), optionally inverted.
|
||||
fn mask_value(r: u8, g: u8, b: u8, a: u8, clip: bool, invert: bool) -> u8 {
|
||||
|
||||
+42
-12
@@ -1,8 +1,10 @@
|
||||
//! Live objects in the renderer.
|
||||
//!
|
||||
//! Blends and envelopes are evaluated by `vectorcraft_doc::live` and the resulting objects cached
|
||||
//! by `Arc` identity of the live node (so the steps keep stable `Arc`s and hit the geometry cache
|
||||
//! frame after frame). Gradient meshes are tessellated into many small solid-colour quads (vello
|
||||
//! Blends and envelopes are evaluated by `vectorcraft_doc::live` (through
|
||||
//! `vectorcraft_effects::expand_live`, as the exporters do) and the resulting objects cached by
|
||||
//! `Arc` identity of the live node and of the symbols and patterns it draws (so the steps keep
|
||||
//! stable `Arc`s and hit the geometry cache frame after frame, and editing a symbol redraws an
|
||||
//! envelope around an instance of it). Gradient meshes are tessellated into many small solid-colour quads (vello
|
||||
//! has no mesh shading), with the density chosen from the on-screen patch size; each quad is
|
||||
//! grown by half a device pixel along its edges so neighbours overlap and no antialiasing seams
|
||||
//! show.
|
||||
@@ -11,7 +13,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::live::{GradientMesh, MeshQuad};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
use vectorcraft_geom::{BezPath, Vec2};
|
||||
use vello_cpu::RenderContext;
|
||||
use vello_cpu::peniko;
|
||||
@@ -24,7 +26,7 @@ type MeshEntry = (Arc<Node>, Arc<Vec<MeshQuad>>, u64);
|
||||
/// Per-renderer cache of evaluated live objects.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct LiveCache {
|
||||
expanded: HashMap<usize, (Arc<Node>, Expanded, u64)>,
|
||||
expanded: HashMap<usize, (Arc<Node>, Expanded, u64, u64)>,
|
||||
meshes: HashMap<(usize, usize), MeshEntry>,
|
||||
stamp: u64,
|
||||
}
|
||||
@@ -42,8 +44,34 @@ impl LiveCache {
|
||||
}
|
||||
|
||||
/// One level of evaluation of a live node (blend steps, envelope result with type outlined run by
|
||||
/// run, mesh → flat pieces); the exporters evaluate them alike. Non-live nodes return themselves.
|
||||
pub use crate::effects::expand_live;
|
||||
/// run, mesh → flat pieces) without a document (symbol instances and pattern tiles in envelopes
|
||||
/// stay as they are). Non-live nodes return themselves.
|
||||
pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
crate::effects::expand_live(None, n)
|
||||
}
|
||||
|
||||
/// What an evaluated live node depends on besides itself: the art of the symbols and the patterns
|
||||
/// it draws (their `Arc`s outlive document copies, so this changes only when one is edited).
|
||||
fn resources_key(doc: &Document, n: &Node) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
n.walk(&mut |c| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &c.kind
|
||||
&& let Some(s) = doc.symbols.iter().find(|s| s.name == *symbol)
|
||||
{
|
||||
(Arc::as_ptr(&s.art) as usize).hash(&mut h);
|
||||
}
|
||||
for i in &c.appearance.items {
|
||||
if let vectorcraft_color::Paint::Pattern { pattern, .. } = i.paint()
|
||||
&& let Some(def) = doc.pattern(pattern)
|
||||
{
|
||||
def.art.iter().for_each(|a| (Arc::as_ptr(a) as usize).hash(&mut h));
|
||||
[def.tile.x0, def.tile.y0, def.tile.x1, def.tile.y1].iter().for_each(|v| v.to_bits().hash(&mut h));
|
||||
}
|
||||
}
|
||||
});
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Subdivisions per patch for a patch about `size_px` device pixels across.
|
||||
fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize {
|
||||
@@ -52,21 +80,23 @@ fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize {
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
fn live_expanded(&mut self, a: &Arc<Node>, cache: bool) -> Expanded {
|
||||
fn live_expanded(&mut self, doc: &Document, a: &Arc<Node>, cache: bool) -> Expanded {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
let resources = resources_key(doc, a);
|
||||
if cache
|
||||
&& let Some(e) = self.live.expanded.get_mut(&key)
|
||||
&& Arc::ptr_eq(&e.0, a)
|
||||
&& e.3 == resources
|
||||
{
|
||||
e.2 = self.live.stamp;
|
||||
return e.1.clone();
|
||||
}
|
||||
let v: Expanded = match crate::effects::pathfinder_children(a, crate::effects::text_outliner()) {
|
||||
Some(children) => Arc::new(children),
|
||||
None => Arc::new(expand_live(a).into_iter().map(Arc::new).collect()),
|
||||
None => Arc::new(crate::effects::expand_live(Some(doc), a).into_iter().map(Arc::new).collect()),
|
||||
};
|
||||
if cache {
|
||||
self.live.expanded.insert(key, (a.clone(), v.clone(), self.live.stamp));
|
||||
self.live.expanded.insert(key, (a.clone(), v.clone(), self.live.stamp, resources));
|
||||
}
|
||||
v
|
||||
}
|
||||
@@ -96,7 +126,7 @@ impl Renderer {
|
||||
let knockout = !f.opts.outline
|
||||
&& matches!(a.kind, NodeKind::Blend { .. })
|
||||
&& a.knockout.resolve(self.knockout)
|
||||
&& vectorcraft_doc::live::steps_knockout_shows(&self.live_expanded(a, true));
|
||||
&& vectorcraft_doc::live::steps_knockout_shows(&self.live_expanded(f.doc, a, true));
|
||||
let enclosing = std::mem::replace(&mut self.knockout, knockout);
|
||||
if !f.opts.outline && (opacity < 1.0 || a.blend != vectorcraft_color::BlendMode::Normal || a.isolate || knockout) {
|
||||
let blends = self.blends_through(a);
|
||||
@@ -115,7 +145,7 @@ impl Renderer {
|
||||
match &a.kind {
|
||||
NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
||||
let items = self.live_expanded(a, cache);
|
||||
let items = self.live_expanded(f.doc, a, cache);
|
||||
// The flag holds while this blend is drawn (see `draw_live`).
|
||||
if self.knockout && matches!(a.kind, NodeKind::Blend { .. }) {
|
||||
self.draw_knockout(ctx, f, &items);
|
||||
|
||||
Reference in New Issue
Block a user