mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 10:47:27 +00:00
The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mixed freeform points in display RGB, while Object > Expand and Create Gradient Mesh mixed them in the model the points share. Points that are all CMYK, Gray, Lab or RGB now mix in that model in those places too. Points in different models mix as before: in display RGB, and on ink planes as the inks of each point. This changes how existing art looks: - CMYK and Lab points. With the default Generic CMYK working space and no ink planes, halfway between C100 and K100 the canvas drew (17, 106, 139) and now draws (54, 118, 143). - RGB points in a working RGB space other than sRGB mix in that space. - On ink planes (a CMYK document that shows transparency), RGB and Lab points mix first and then separate. Gray mixes keep their values, and CMYK and Gray points on ink planes draw as before. RGB points in an sRGB working space draw as before except on ink planes. - PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image. PDF's Output settings convert it as they convert any CMYK image. - Darkening applies to the points before they mix. - With Appearance of Black set to Rich, the canvas mixes a gradient with a pure CMYK black point in display RGB, and the point fades from RGB black.
104 lines
4.9 KiB
Rust
104 lines
4.9 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::hash::{Hash, Hasher};
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::cms::Model;
|
||
use vectorcraft_color::freeform::{grid_size, painted_box, spread_scale};
|
||
use vectorcraft_color::{Freeform, GradientPaint};
|
||
use vectorcraft_geom::{Affine, Rect};
|
||
use vello_cpu::peniko::{Extend, ImageQuality, ImageSampler};
|
||
use vello_cpu::{Pixmap, RenderContext};
|
||
|
||
use crate::ink::Ink;
|
||
use crate::lru::{self, FrameCache};
|
||
|
||
thread_local! {
|
||
/// Grids by [`key`], within [`lru::FREEFORM_BUDGET`], least recently used first out.
|
||
static GRIDS: RefCell<FrameCache<u64, Arc<Pixmap>>> = RefCell::new(FrameCache::new(lru::FREEFORM_BUDGET, usize::MAX));
|
||
}
|
||
|
||
/// Most cells along a side of a grid of Lab points on an ink plane, where each cell separates its
|
||
/// color through the profile; the paint smooths the grid.
|
||
const MAX_LAB_PLANE_CELLS: u16 = 64;
|
||
|
||
/// 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);
|
||
p.color.key().hash(&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()
|
||
}
|
||
|
||
/// The grid size of `f` on box `b` drawn `device` pixels long with `ink`.
|
||
pub(crate) fn cells(f: &Freeform, b: Rect, device: f64, ink: Ink) -> (u16, u16) {
|
||
let (cols, rows) = grid_size(b, device);
|
||
if f.shared_model() == Some(Model::Lab) && matches!(ink.base(), Ink::Cmy | Ink::K) {
|
||
return (cols.min(MAX_LAB_PLANE_CELLS), rows.min(MAX_LAB_PLANE_CELLS));
|
||
}
|
||
(cols, rows)
|
||
}
|
||
|
||
/// Sample `f` at the centres of a `cols` × `rows` grid over `b` (premultiplied, colours as `ink`
|
||
/// paints them). Points that share a color model mix in that model ([`Freeform::model_field`]);
|
||
/// points in different models, and points that `ink` paints as RGB black, mix as `ink` paints each
|
||
/// of them.
|
||
pub(crate) fn rasterize(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> Pixmap {
|
||
let adjusted = ink.freeform(f);
|
||
let (f, ink) = adjusted.as_ref().map_or((f, ink), |a| (a, ink.base()));
|
||
let cell = |[r, g, bl]: [f32; 3], a: f32| {
|
||
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 }
|
||
};
|
||
let scale = spread_scale(b);
|
||
let data = if f.shared_model().is_some() && !ink.blacks_a_point(f) {
|
||
f.model_field(scale).grid(b, cols, rows).map(|(c, a)| cell(ink.rgb_sampled(&c), a)).collect()
|
||
} else {
|
||
f.field_with(scale, &|c| ink.rgb(c)).grid(b, cols, rows).map(|(rgb, a)| cell(rgb, a)).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 f = g.freeform_on(b);
|
||
let (cols, rows) = cells(&f, b, device, ink);
|
||
let k = key(&f, b, cols, rows, ink);
|
||
let pm = GRIDS
|
||
.with(|c| {
|
||
let mut c = c.borrow_mut();
|
||
let t = c.tick();
|
||
c.get(&k, t).cloned()
|
||
})
|
||
.unwrap_or_else(|| {
|
||
let pm = Arc::new(rasterize(&f, b, cols, rows, ink));
|
||
GRIDS.with(|c| {
|
||
let mut c = c.borrow_mut();
|
||
let t = c.tick();
|
||
c.insert(k, pm.clone(), cols as usize * rows as usize * 4, t);
|
||
});
|
||
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
|
||
}
|