mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
SVG has no freeform shading: an object's freeform fill or stroke is now written as an <image> of the colour field over its painted box, sampled on the grid the canvas uses (twice the box's size in points), clipped to the shape or to the stroke's outline, with the fill or stroke opacity and blend mode on the group. Linked image mode writes the PNG next to the SVG. Character paints keep the linear approximation (with a warning); gradients along or across strokes stay vector slices. The grid sizing moved to the colour crate so the canvas and the export share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
4.0 KiB
Rust
91 lines
4.0 KiB
Rust
//! Freeform gradients in the renderer.
|
||
//!
|
||
//! vello has no freeform shading, so the colour field ([`vectorcraft_color::freeform`]) is
|
||
//! sampled on a grid of cells over the painted box and drawn as an image paint (bilinear, edges
|
||
//! padded): the fill or stroke being painted clips it. The grid is sized from the box's size on
|
||
//! screen (a cell per few device pixels, in power-of-two steps so zooming reuses it) and cached by
|
||
//! the gradient's content, so panning and redraws don't resample it.
|
||
|
||
use std::cell::RefCell;
|
||
use std::collections::HashMap;
|
||
use std::hash::{Hash, Hasher};
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::freeform::{grid_size, painted_box, spread_scale};
|
||
use vectorcraft_color::{Color, Freeform, GradientPaint};
|
||
use vectorcraft_geom::{Affine, Rect};
|
||
use vello_cpu::peniko::{Extend, ImageQuality, ImageSampler};
|
||
use vello_cpu::{Pixmap, RenderContext};
|
||
|
||
use crate::ink::Ink;
|
||
|
||
/// Grids kept before the cache starts over.
|
||
const MAX_GRIDS: usize = 128;
|
||
|
||
thread_local! {
|
||
static GRIDS: RefCell<HashMap<u64, Arc<Pixmap>>> = RefCell::new(HashMap::new());
|
||
}
|
||
|
||
fn hash_color(c: &Color, h: &mut impl Hasher) {
|
||
match *c {
|
||
Color::Rgb { r, g, b } => (0u8, r.to_bits(), g.to_bits(), b.to_bits()).hash(h),
|
||
Color::Cmyk { c, m, y, k } => (1u8, c.to_bits(), m.to_bits(), y.to_bits(), k.to_bits()).hash(h),
|
||
Color::Gray { k } => (2u8, k.to_bits()).hash(h),
|
||
Color::Lab { l, a, b } => (3u8, l.to_bits(), a.to_bits(), b.to_bits()).hash(h),
|
||
}
|
||
}
|
||
|
||
/// Cache key of the grid of `f` on box `b` at `cols` × `rows` painted with `ink` (and the active
|
||
/// colour settings, which turn colours into display RGB or inks).
|
||
fn key(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> u64 {
|
||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||
for p in &f.points {
|
||
(p.at.x.to_bits(), p.at.y.to_bits(), p.opacity.to_bits(), p.spread.to_bits()).hash(&mut h);
|
||
hash_color(&p.color, &mut h);
|
||
}
|
||
f.lines.hash(&mut h);
|
||
(b.x0.to_bits(), b.y0.to_bits(), b.x1.to_bits(), b.y1.to_bits(), cols, rows).hash(&mut h);
|
||
(Arc::as_ptr(&vectorcraft_color::cms::active()) as usize, ink).hash(&mut h);
|
||
h.finish()
|
||
}
|
||
|
||
/// Sample `f` at the centres of a `cols` × `rows` grid over `b` (premultiplied, colours as `ink`
|
||
/// paints them).
|
||
pub(crate) fn rasterize(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> Pixmap {
|
||
let field = f.field_with(spread_scale(b), &|c| ink.rgb(c));
|
||
let data = field
|
||
.grid(b, cols, rows)
|
||
.map(|([r, g, bl], a)| {
|
||
let a = a.clamp(0.0, 1.0);
|
||
let q = |v: f32| (v.clamp(0.0, 1.0) * a * 255.0).round() as u8;
|
||
vello_cpu::color::PremulRgba8 { r: q(r), g: q(g), b: q(bl), a: (a * 255.0).round() as u8 }
|
||
})
|
||
.collect();
|
||
Pixmap::from_parts(data, cols, rows)
|
||
}
|
||
|
||
/// Set freeform gradient `g` on box `bounds` (in the paint's space, which the context's current
|
||
/// transform maps to pixels) as the context paint. Returns false for an empty box.
|
||
pub(crate) fn set_freeform_paint(ctx: &mut RenderContext, g: &GradientPaint, bounds: Rect, ink: Ink) -> bool {
|
||
let Some(b) = painted_box(bounds) else { return false };
|
||
let device = b.width().max(b.height()) * ctx.transform().determinant().abs().sqrt();
|
||
let (cols, rows) = grid_size(b, device);
|
||
let f = g.freeform_on(b);
|
||
let k = key(&f, b, cols, rows, ink);
|
||
let pm = GRIDS.with(|c| c.borrow().get(&k).cloned()).unwrap_or_else(|| {
|
||
let pm = Arc::new(rasterize(&f, b, cols, rows, ink));
|
||
GRIDS.with(|c| {
|
||
let mut c = c.borrow_mut();
|
||
if c.len() >= MAX_GRIDS {
|
||
c.clear();
|
||
}
|
||
c.insert(k, pm.clone());
|
||
});
|
||
pm
|
||
});
|
||
let sampler = ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad, quality: ImageQuality::Medium, alpha: 1.0 };
|
||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler });
|
||
ctx.set_paint_transform(Affine::translate(b.origin().to_vec2()) * Affine::scale_non_uniform(b.width() / cols as f64, b.height() / rows as f64));
|
||
true
|
||
}
|