97 lines
4.2 KiB
Rust
97 lines
4.2 KiB
Rust
//! Freeform gradients: the rendered colour grid against the colour field.
|
||
|
||
use super::*;
|
||
use vectorcraft_color::freeform::spread_scale;
|
||
use vectorcraft_color::{Color, Freeform, FreeformPoint, Gradient, GradientKind, GradientPaint, Paint};
|
||
use vectorcraft_doc::{Appearance, Node};
|
||
use vectorcraft_geom::{Point, shapes};
|
||
|
||
/// Red at the top left, blue at the bottom right, a half-transparent green at the top right.
|
||
fn freeform() -> Freeform {
|
||
let p = |x, y, c, opacity| FreeformPoint { opacity, spread: 0.1, ..FreeformPoint::new(Point::new(x, y), c) };
|
||
Freeform {
|
||
points: vec![
|
||
p(20.0, 20.0, Color::rgb(1.0, 0.0, 0.0), 1.0),
|
||
p(80.0, 80.0, Color::rgb(0.0, 0.0, 1.0), 1.0),
|
||
p(80.0, 20.0, Color::rgb(0.0, 1.0, 0.0), 0.5),
|
||
],
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn paint(f: Option<Freeform>) -> Paint {
|
||
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Freeform, ..Default::default() });
|
||
g.freeform = f;
|
||
Paint::Gradient(Box::new(g))
|
||
}
|
||
|
||
/// A 100 × 100 document with `shape` (on 0..100) painted `fill`, at `opacity`.
|
||
fn doc(shape: vectorcraft_geom::PathData, fill: Paint, opacity: f32) -> Document {
|
||
let mut d = Document::new(100.0, 100.0);
|
||
let id = d.alloc_id();
|
||
let mut n = Node::path(id, shape, Appearance::basic(fill, Paint::None, 0.0));
|
||
n.opacity = opacity;
|
||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||
d
|
||
}
|
||
|
||
fn render(d: &Document) -> Rendered {
|
||
Renderer::new().render(d, 100, 100, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() })
|
||
}
|
||
|
||
/// The field's colour at `p` composited over white (8-bit).
|
||
fn expected(f: &Freeform, p: Point, opacity: f32) -> [u8; 4] {
|
||
let ([r, g, b], a) = f.field(spread_scale(Rect::new(0.0, 0.0, 100.0, 100.0))).sample(p);
|
||
let a = a * opacity;
|
||
let q = |v: f32| ((v * a + (1.0 - a)) * 255.0).round() as u8;
|
||
[q(r), q(g), q(b), 255]
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
#[test]
|
||
fn the_rendered_grid_follows_the_colour_field() {
|
||
let f = freeform();
|
||
let r = render(&doc(shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), paint(Some(f.clone())), 1.0));
|
||
assert_eq!(r.pixel(20, 20), [255, 0, 0, 255], "the red point's colour");
|
||
assert_eq!(r.pixel(80, 80), [0, 0, 255, 255], "the blue point's colour");
|
||
for (x, y) in [(50, 50), (35, 60), (65, 30), (10, 90), (95, 5), (50, 20)] {
|
||
let want = expected(&f, Point::new(x as f64 + 0.5, y as f64 + 0.5), 1.0);
|
||
assert!(near(r.pixel(x, y), want, 6), "({x}, {y}): {:?} vs {want:?}", r.pixel(x, y));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_grid_is_clipped_to_the_path_and_takes_the_object_opacity() {
|
||
let f = freeform();
|
||
let r = render(&doc(shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)), paint(Some(f.clone())), 0.5));
|
||
assert_eq!(r.pixel(2, 2), [255, 255, 255, 255], "outside the ellipse");
|
||
let want = expected(&f, Point::new(50.5, 50.5), 0.5);
|
||
assert!(near(r.pixel(50, 50), want, 6), "{:?} vs {want:?}", r.pixel(50, 50));
|
||
}
|
||
|
||
#[test]
|
||
fn unplaced_freeform_gradients_seed_points_on_the_box() {
|
||
// Without points: the automatic ones, coloured white → black along the default stops.
|
||
let r = render(&doc(shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), paint(None), 1.0));
|
||
let auto = Freeform::auto(Rect::new(0.0, 0.0, 100.0, 100.0), &Gradient::default(), &|_| true);
|
||
let at = |i: usize| auto.points[i].at;
|
||
// (Pixel centres sit half a pixel off the points.)
|
||
assert!(near(r.pixel(at(0).x as u32, at(0).y as u32), [255, 255, 255, 255], 3));
|
||
assert!(near(r.pixel(at(3).x as u32, at(3).y as u32), [0, 0, 0, 255], 3));
|
||
}
|
||
|
||
#[test]
|
||
fn strokes_take_freeform_paint() {
|
||
let mut d = Document::new(100.0, 100.0);
|
||
let id = d.alloc_id();
|
||
let line = shapes::rectangle(Rect::new(20.0, 20.0, 80.0, 80.0));
|
||
let n = Node::path(id, line, Appearance::basic(Paint::None, paint(Some(freeform())), 10.0));
|
||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||
let r = render(&d);
|
||
assert_eq!(r.pixel(20, 20), [255, 0, 0, 255], "the stroke's corner sits on the red point");
|
||
assert_eq!(r.pixel(50, 50), [255, 255, 255, 255], "unfilled inside");
|
||
}
|