mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Merge pull request #6268 from dotCooCoo/fix/freeform-mixes-in-point-model
Gradients: freeform points that share a color model mix in that model
This commit is contained in:
@@ -10,6 +10,7 @@ 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};
|
||||
@@ -24,6 +25,10 @@ thread_local! {
|
||||
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 {
|
||||
@@ -38,18 +43,33 @@ fn key(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> u64 {
|
||||
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).
|
||||
/// 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 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();
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -58,8 +78,8 @@ pub(crate) fn rasterize(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -
|
||||
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 (cols, rows) = cells(&f, b, device, ink);
|
||||
let k = key(&f, b, cols, rows, ink);
|
||||
let pm = GRIDS
|
||||
.with(|c| {
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use vectorcraft_color::blend::{cmyk_planes, planes_cmyk};
|
||||
use vectorcraft_color::cms::{self, Cms, CmykLut, ProofLut, lab};
|
||||
use vectorcraft_color::{Color, Gradient};
|
||||
use vectorcraft_color::{Color, Freeform, Gradient};
|
||||
use vectorcraft_doc::cmyk::Inks;
|
||||
use vectorcraft_doc::{ColorMode, Document, ImageBlob};
|
||||
use vello_cpu::Pixmap;
|
||||
@@ -126,6 +126,23 @@ impl Ink {
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// Transform original freeform points before their colors mix, as [`Self::gradient`] does for
|
||||
/// stops.
|
||||
pub(crate) fn freeform(self, f: &Freeform) -> Option<Freeform> {
|
||||
let amount = self.darkness()?;
|
||||
let mut f = f.clone();
|
||||
for point in &mut f.points {
|
||||
point.color = Self::darkened_color(&point.color, amount, self.black_endpoint());
|
||||
}
|
||||
Some(f)
|
||||
}
|
||||
|
||||
/// Whether this ink paints a point of `f` as RGB black: Appearance of Black's Rich setting and
|
||||
/// a point of pure CMYK black.
|
||||
pub(crate) fn blacks_a_point(self, f: &Freeform) -> bool {
|
||||
self.base() == Ink::RichBlack && f.points.iter().any(|p| matches!(p.color, Color::Cmyk { c, m, y, k } if is_pure_black([c, m, y, k])))
|
||||
}
|
||||
|
||||
/// The three values painted for colour `c`.
|
||||
pub(crate) fn rgb(self, c: &Color) -> [f32; 3] {
|
||||
if let Some(amount) = self.darkness() {
|
||||
@@ -138,6 +155,15 @@ impl Ink {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::rgb`] for colors sampled densely (a freeform gradient's grid): on an ink plane, RGB
|
||||
/// colors go through the separation tables placed images use.
|
||||
pub(crate) fn rgb_sampled(self, c: &Color) -> [f32; 3] {
|
||||
match (self, c) {
|
||||
(Ink::Cmy | Ink::K, Color::Rgb { .. }) => separation().0[(self == Ink::K) as usize].apply(c.to_rgb()),
|
||||
_ => self.rgb(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::rgb`] as a paint, at `alpha`.
|
||||
pub(crate) fn color(self, c: &Color, alpha: f32) -> peniko::Color {
|
||||
let [r, g, b] = self.rgb(c);
|
||||
|
||||
@@ -51,6 +51,114 @@ fn near(a: [u8; 4], b: [u8; 4], tol: i32) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (*x as i32 - y as i32).abs() <= tol)
|
||||
}
|
||||
|
||||
/// `a` and `b` on either side of (50.5, 50.5): the point a pixel center and a grid cell share.
|
||||
fn pair(a: Color, b: Color) -> Freeform {
|
||||
let p = |x, c| FreeformPoint::new(Point::new(x, 50.5), c);
|
||||
Freeform { points: vec![p(20.5, a), p(80.5, b)], ..Default::default() }
|
||||
}
|
||||
|
||||
const CYAN: Color = Color::Cmyk { c: 1.0, m: 0.0, y: 0.0, k: 0.0 };
|
||||
const BLACK: Color = Color::Cmyk { c: 0.0, m: 0.0, y: 0.0, k: 1.0 };
|
||||
|
||||
/// The middle pixel of a square painted with `pair(a, b)`.
|
||||
fn middle(a: Color, b: Color) -> [u8; 4] {
|
||||
render(&doc(shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), paint(Some(pair(a, b))), 1.0)).pixel(50, 50)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn points_that_share_a_model_mix_in_it() {
|
||||
let want = Color::cmyk(0.5, 0.0, 0.0, 0.5).to_rgba8(1.0);
|
||||
assert!(near(middle(CYAN, BLACK), want, 3), "CMYK: {:?} vs {want:?}", middle(CYAN, BLACK));
|
||||
let (lab1, lab2) = (Color::lab(90.0, -60.0, 40.0), Color::lab(30.0, 60.0, -40.0));
|
||||
let want = Color::lab(60.0, 0.0, 0.0).to_rgba8(1.0);
|
||||
assert!(near(middle(lab1, lab2), want, 3), "Lab: {:?} vs {want:?}", middle(lab1, lab2));
|
||||
let want = Color::gray(0.5).to_rgba8(1.0);
|
||||
assert!(near(middle(Color::gray(0.2), Color::gray(0.8)), want, 3), "Gray");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_points_and_points_in_different_models_mix_in_display_rgb() {
|
||||
let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
|
||||
let want = Color::rgb(0.5, 0.0, 0.5).to_rgba8(1.0);
|
||||
assert!(near(middle(red, blue), want, 3), "RGB: {:?} vs {want:?}", middle(red, blue));
|
||||
let gray = Color::gray(0.5);
|
||||
let [r, g, b] = [red.to_rgb(), gray.to_rgb()].into_iter().fold([0.0; 3], |m, c| [m[0] + c[0] / 2.0, m[1] + c[1] / 2.0, m[2] + c[2] / 2.0]);
|
||||
let want = Color::rgb(r, g, b).to_rgba8(1.0);
|
||||
assert!(near(middle(red, gray), want, 3), "mixed models: {:?} vs {want:?}", middle(red, gray));
|
||||
}
|
||||
|
||||
/// The middle grid cell of `pair(a, b)` rasterized on a 101-point box with `ink`, as RGB.
|
||||
fn middle_cell(a: Color, b: Color, ink: crate::ink::Ink) -> [u8; 3] {
|
||||
let cell = crate::freeform::rasterize(&pair(a, b), Rect::new(0.0, 0.0, 101.0, 101.0), 17, 17, ink).sample(8, 8);
|
||||
[cell.r, cell.g, cell.b]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_planes_separate_the_mix_of_lab_points() {
|
||||
use crate::ink::Ink;
|
||||
let (lab1, lab2) = (Color::lab(90.0, -60.0, 40.0), Color::lab(30.0, 60.0, -40.0));
|
||||
let mid = Color::lab(60.0, 0.0, 0.0);
|
||||
// A color far out of gamut stays as exact as a solid fill of it.
|
||||
let far = Color::lab(20.0, -125.0, 125.0);
|
||||
for ink in [Ink::Cmy, Ink::K] {
|
||||
for (a, b, c) in [(lab1, lab2, mid), (far, far, far)] {
|
||||
let want = ink.color(&c, 1.0).to_rgba8();
|
||||
let got = middle_cell(a, b, ink);
|
||||
assert!(near([got[0], got[1], got[2], 255], [want.r, want.g, want.b, 255], 2), "{ink:?} {c:?}: {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_planes_separate_the_mix_of_rgb_points() {
|
||||
use crate::ink::Ink;
|
||||
let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
|
||||
let mid = Color::rgb(0.5, 0.0, 0.5);
|
||||
for ink in [Ink::Cmy, Ink::K] {
|
||||
let want = ink.color(&mid, 1.0).to_rgba8();
|
||||
let got = middle_cell(red, blue, ink);
|
||||
assert!(near([got[0], got[1], got[2], 255], [want.r, want.g, want.b, 255], 3), "{ink:?}: {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lab_points_on_an_ink_plane_use_a_coarser_grid() {
|
||||
use crate::ink::Ink;
|
||||
let b = Rect::new(0.0, 0.0, 1000.0, 1000.0);
|
||||
let lab = pair(Color::lab(90.0, -60.0, 40.0), Color::lab(30.0, 60.0, -40.0));
|
||||
let (cols, rows) = crate::freeform::cells(&lab, b, 4000.0, Ink::Cmy);
|
||||
assert_eq!((cols, rows), (64, 64));
|
||||
assert_eq!(crate::freeform::cells(&lab, b, 4000.0, Ink::K), (64, 64));
|
||||
let (wide, _) = crate::freeform::cells(&lab, b, 4000.0, Ink::Display);
|
||||
assert!(wide > 64, "{wide}");
|
||||
let (cmyk, _) = crate::freeform::cells(&pair(CYAN, BLACK), b, 4000.0, Ink::Cmy);
|
||||
assert!(cmyk > 64, "{cmyk}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_black_points_fade_from_rgb_black_under_rich_black() {
|
||||
use crate::ink::Ink;
|
||||
let white = Color::cmyk(0.0, 0.0, 0.0, 0.0);
|
||||
let [r, g, b] = white.to_rgb();
|
||||
let want = [r, g, b].map(|v| (v * 0.5 * 255.0).round() as u8);
|
||||
let got = middle_cell(BLACK, white, Ink::RichBlack);
|
||||
assert!(near([got[0], got[1], got[2], 255], [want[0], want[1], want[2], 255], 2), "{got:?} vs {want:?}");
|
||||
// Without a pure black point, the points mix in their model.
|
||||
let (a, c) = (Color::cmyk(0.0, 0.0, 0.0, 0.9), white);
|
||||
let want = Color::cmyk(0.0, 0.0, 0.0, 0.45).to_rgba8(1.0);
|
||||
let got = middle_cell(a, c, Ink::RichBlack);
|
||||
assert!(near([got[0], got[1], got[2], 255], want, 2), "{got:?} vs {want:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn darkening_changes_the_points_before_they_mix() {
|
||||
use crate::ink::Ink;
|
||||
// Half black added to each point: K100 stays K100 and K0 becomes K50, so they meet at K75.
|
||||
let got = middle_cell(BLACK, Color::cmyk(0.0, 0.0, 0.0, 0.0), Ink::Display.darkened(0.5));
|
||||
let want = Color::cmyk(0.0, 0.0, 0.0, 0.75).to_rgba8(1.0);
|
||||
assert!(near([got[0], got[1], got[2], 255], want, 2), "{got:?} vs {want:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rendered_grid_follows_the_colour_field() {
|
||||
let f = freeform();
|
||||
|
||||
Reference in New Issue
Block a user