| use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet}; |
| use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect}; |
| use vello::peniko::*; |
| use vello::*; |
| |
| const FLOWER_IMAGE: &[u8] = include_bytes!("../../assets/splash-flower.jpg"); |
| |
| macro_rules! scene { |
| ($name: ident) => { |
| scene!($name: false) |
| }; |
| ($name: ident: animated) => { |
| scene!($name: true) |
| }; |
| ($name: ident: $animated: literal) => { |
| ExampleScene { |
| config: SceneConfig { |
| animated: $animated, |
| name: stringify!($name).to_owned(), |
| }, |
| function: Box::new($name), |
| } |
| }; |
| } |
| |
| pub fn test_scenes() -> SceneSet { |
| // For WASM below, must be mutable |
| #[allow(unused_mut)] |
| let mut scenes = vec![ |
| scene!(funky_paths), |
| scene!(cardioid_and_friends), |
| scene!(animated_text: animated), |
| scene!(brush_transform: animated), |
| scene!(blend_grid), |
| scene!(conflation_artifacts), |
| scene!(labyrinth), |
| scene!(base_color_test: animated), |
| ]; |
| #[cfg(any(target_arch = "wasm32", target_os = "android"))] |
| scenes.push(ExampleScene { |
| config: SceneConfig { |
| animated: false, |
| name: "included_tiger".to_owned(), |
| }, |
| function: Box::new(included_tiger()), |
| }); |
| |
| SceneSet { scenes } |
| } |
| |
| // Scenes |
| |
| fn funky_paths(sb: &mut SceneBuilder, _: &mut SceneParams) { |
| use PathEl::*; |
| let missing_movetos = [ |
| LineTo((100.0, 100.0).into()), |
| LineTo((100.0, 200.0).into()), |
| ClosePath, |
| LineTo((0.0, 400.0).into()), |
| LineTo((100.0, 400.0).into()), |
| ]; |
| let only_movetos = [MoveTo((0.0, 0.0).into()), MoveTo((100.0, 100.0).into())]; |
| let empty: [PathEl; 0] = []; |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((100.0, 100.0)), |
| Color::rgb8(0, 0, 255), |
| None, |
| &missing_movetos, |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgb8(0, 0, 255), |
| None, |
| &empty, |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgb8(0, 0, 255), |
| None, |
| &only_movetos, |
| ); |
| sb.stroke( |
| &Stroke::new(8.0), |
| Affine::translate((100.0, 100.0)), |
| Color::rgb8(0, 255, 255), |
| None, |
| &missing_movetos, |
| ); |
| } |
| |
| fn cardioid_and_friends(sb: &mut SceneBuilder, _: &mut SceneParams) { |
| render_cardioid(sb); |
| render_clip_test(sb); |
| render_alpha_test(sb); |
| //render_tiger(sb, false); |
| } |
| |
| fn animated_text(sb: &mut SceneBuilder, params: &mut SceneParams) { |
| // Uses the static array address as a cache key for expedience. Real code |
| // should use a better strategy. |
| let piet_logo = params |
| .images |
| .from_bytes(FLOWER_IMAGE.as_ptr() as usize, FLOWER_IMAGE) |
| .unwrap(); |
| |
| use PathEl::*; |
| let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0)); |
| let star = [ |
| MoveTo((50.0, 0.0).into()), |
| LineTo((21.0, 90.0).into()), |
| LineTo((98.0, 35.0).into()), |
| LineTo((2.0, 35.0).into()), |
| LineTo((79.0, 90.0).into()), |
| ClosePath, |
| ]; |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| &Brush::Solid(Color::rgb8(128, 128, 128)), |
| None, |
| &rect, |
| ); |
| let text_size = 60.0 + 40.0 * (params.time as f32).sin(); |
| let s = "\u{1f600}hello vello text!"; |
| params.text.add( |
| sb, |
| None, |
| text_size, |
| None, |
| Affine::translate((110.0, 600.0)), |
| s, |
| ); |
| params.text.add_run( |
| sb, |
| None, |
| text_size, |
| Color::WHITE, |
| Affine::translate((110.0, 700.0)), |
| // Add a skew to simulate an oblique font. |
| Some(Affine::skew(20f64.to_radians().tan(), 0.0)), |
| &Stroke::new(1.0), |
| s, |
| ); |
| let t = ((params.time).sin() * 0.5 + 0.5) as f32; |
| let weight = t * 700.0 + 200.0; |
| let width = t * 150.0 + 50.0; |
| params.text.add_var_run( |
| sb, |
| None, |
| 72.0, |
| &[("wght", weight), ("wdth", width)], |
| Color::WHITE, |
| Affine::translate((110.0, 800.0)), |
| // Add a skew to simulate an oblique font. |
| None, |
| Fill::NonZero, |
| "And some vello\ntext with a newline", |
| ); |
| let th = params.time as f64; |
| let center = Point::new(500.0, 500.0); |
| let mut p1 = center; |
| p1.x += 400.0 * th.cos(); |
| p1.y += 400.0 * th.sin(); |
| sb.stroke( |
| &Stroke::new(5.0), |
| Affine::IDENTITY, |
| &Brush::Solid(Color::rgb8(128, 0, 0)), |
| None, |
| &[PathEl::MoveTo(center), PathEl::LineTo(p1)], |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((150.0, 150.0)) * Affine::scale(0.2), |
| Color::RED, |
| None, |
| &rect, |
| ); |
| let alpha = (params.time as f64).sin() as f32 * 0.5 + 0.5; |
| sb.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect); |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((100.0, 100.0)) * Affine::scale(0.2), |
| Color::BLUE, |
| None, |
| &rect, |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((200.0, 200.0)) * Affine::scale(0.2), |
| Color::GREEN, |
| None, |
| &rect, |
| ); |
| sb.pop_layer(); |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((400.0, 100.0)), |
| Color::PURPLE, |
| None, |
| &star, |
| ); |
| sb.fill( |
| Fill::EvenOdd, |
| Affine::translate((500.0, 100.0)), |
| Color::PURPLE, |
| None, |
| &star, |
| ); |
| sb.draw_image( |
| &piet_logo, |
| Affine::translate((800.0, 50.0)) * Affine::rotate(20f64.to_radians()), |
| ); |
| } |
| |
| fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { |
| let th = params.time; |
| let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0)).with_stops([ |
| Color::RED, |
| Color::GREEN, |
| Color::BLUE, |
| ]); |
| sb.fill( |
| Fill::NonZero, |
| Affine::rotate(25f64.to_radians()) * Affine::scale_non_uniform(2.0, 1.0), |
| &Gradient::new_radial((200.0, 200.0), 80.0).with_stops([ |
| Color::RED, |
| Color::GREEN, |
| Color::BLUE, |
| ]), |
| None, |
| &Rect::from_origin_size((100.0, 100.0), (200.0, 200.0)), |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((200.0, 600.0)), |
| &linear, |
| Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), |
| &Rect::from_origin_size(Point::default(), (400.0, 200.0)), |
| ); |
| sb.stroke( |
| &Stroke::new(40.0), |
| Affine::translate((800.0, 600.0)), |
| &linear, |
| Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), |
| &Rect::from_origin_size(Point::default(), (400.0, 200.0)), |
| ); |
| } |
| |
| fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { |
| const BLEND_MODES: &[Mix] = &[ |
| Mix::Normal, |
| Mix::Multiply, |
| Mix::Darken, |
| Mix::Screen, |
| Mix::Lighten, |
| Mix::Overlay, |
| Mix::ColorDodge, |
| Mix::ColorBurn, |
| Mix::HardLight, |
| Mix::SoftLight, |
| Mix::Difference, |
| Mix::Exclusion, |
| Mix::Hue, |
| Mix::Saturation, |
| Mix::Color, |
| Mix::Luminosity, |
| ]; |
| for (ix, &blend) in BLEND_MODES.iter().enumerate() { |
| let i = ix % 4; |
| let j = ix / 4; |
| let transform = Affine::translate((i as f64 * 225., j as f64 * 225.)); |
| let square = blend_square(blend.into()); |
| sb.append(&square, Some(transform)); |
| } |
| } |
| |
| #[cfg(any(target_arch = "wasm32", target_os = "android"))] |
| fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { |
| let contents = include_str!(concat!( |
| env!("CARGO_MANIFEST_DIR"), |
| "/../assets/Ghostscript_Tiger.svg" |
| )); |
| crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents) |
| } |
| |
| // Support functions |
| |
| fn render_cardioid(sb: &mut SceneBuilder) { |
| let n = 601; |
| let dth = std::f64::consts::PI * 2.0 / (n as f64); |
| let center = Point::new(1024.0, 768.0); |
| let r = 750.0; |
| let mut path = BezPath::new(); |
| for i in 1..n { |
| let mut p0 = center; |
| let a0 = i as f64 * dth; |
| p0.x += a0.cos() * r; |
| p0.y += a0.sin() * r; |
| let mut p1 = center; |
| let a1 = ((i * 2) % n) as f64 * dth; |
| p1.x += a1.cos() * r; |
| p1.y += a1.sin() * r; |
| path.push(PathEl::MoveTo(p0)); |
| path.push(PathEl::LineTo(p1)); |
| } |
| sb.stroke( |
| &Stroke::new(2.0), |
| Affine::IDENTITY, |
| Color::rgb8(0, 0, 255), |
| None, |
| &path, |
| ); |
| } |
| |
| fn render_clip_test(sb: &mut SceneBuilder) { |
| const N: usize = 16; |
| const X0: f64 = 50.0; |
| const Y0: f64 = 450.0; |
| // Note: if it gets much larger, it will exceed the 1MB scratch buffer. |
| // But this is a pretty demanding test. |
| const X1: f64 = 550.0; |
| const Y1: f64 = 950.0; |
| let step = 1.0 / ((N + 1) as f64); |
| for i in 0..N { |
| let t = ((i + 1) as f64) * step; |
| let path = [ |
| PathEl::MoveTo((X0, Y0).into()), |
| PathEl::LineTo((X1, Y0).into()), |
| PathEl::LineTo((X1, Y0 + t * (Y1 - Y0)).into()), |
| PathEl::LineTo((X1 + t * (X0 - X1), Y1).into()), |
| PathEl::LineTo((X0, Y1).into()), |
| PathEl::ClosePath, |
| ]; |
| sb.push_layer(Mix::Clip, 1.0, Affine::IDENTITY, &path); |
| } |
| let rect = Rect::new(X0, Y0, X1, Y1); |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| &Brush::Solid(Color::rgb8(0, 255, 0)), |
| None, |
| &rect, |
| ); |
| for _ in 0..N { |
| sb.pop_layer(); |
| } |
| } |
| |
| fn render_alpha_test(sb: &mut SceneBuilder) { |
| // Alpha compositing tests. |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgb8(255, 0, 0), |
| None, |
| &make_diamond(1024.0, 100.0), |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgba8(0, 255, 0, 0x80), |
| None, |
| &make_diamond(1024.0, 125.0), |
| ); |
| sb.push_layer( |
| Mix::Clip, |
| 1.0, |
| Affine::IDENTITY, |
| &make_diamond(1024.0, 150.0), |
| ); |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgba8(0, 0, 255, 0x80), |
| None, |
| &make_diamond(1024.0, 175.0), |
| ); |
| sb.pop_layer(); |
| } |
| |
| fn render_blend_square(sb: &mut SceneBuilder, blend: BlendMode, transform: Affine) { |
| // Inspired by https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode |
| let rect = Rect::from_origin_size(Point::new(0., 0.), (200., 200.)); |
| let linear = |
| Gradient::new_linear((0.0, 0.0), (200.0, 0.0)).with_stops([Color::BLACK, Color::WHITE]); |
| sb.fill(Fill::NonZero, transform, &linear, None, &rect); |
| const GRADIENTS: &[(f64, f64, Color)] = &[ |
| (150., 0., Color::rgb8(255, 240, 64)), |
| (175., 100., Color::rgb8(255, 96, 240)), |
| (125., 200., Color::rgb8(64, 192, 255)), |
| ]; |
| for (x, y, c) in GRADIENTS { |
| let mut color2 = c.clone(); |
| color2.a = 0; |
| let radial = Gradient::new_radial((*x, *y), 100.0).with_stops([*c, color2]); |
| sb.fill(Fill::NonZero, transform, &radial, None, &rect); |
| } |
| const COLORS: &[Color] = &[ |
| Color::rgb8(255, 0, 0), |
| Color::rgb8(0, 255, 0), |
| Color::rgb8(0, 0, 255), |
| ]; |
| sb.push_layer(Mix::Normal, 1.0, transform, &rect); |
| for (i, c) in COLORS.iter().enumerate() { |
| let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0)).with_stops([Color::WHITE, *c]); |
| sb.push_layer(blend, 1.0, transform, &rect); |
| // squash the ellipse |
| let a = transform |
| * Affine::translate((100., 100.)) |
| * Affine::rotate(std::f64::consts::FRAC_PI_3 * (i * 2 + 1) as f64) |
| * Affine::scale_non_uniform(1.0, 0.357) |
| * Affine::translate((-100., -100.)); |
| sb.fill( |
| Fill::NonZero, |
| a, |
| &linear, |
| None, |
| &Ellipse::new((100., 100.), (90., 90.), 0.), |
| ); |
| sb.pop_layer(); |
| } |
| sb.pop_layer(); |
| } |
| |
| fn blend_square(blend: BlendMode) -> SceneFragment { |
| let mut fragment = SceneFragment::default(); |
| let mut sb = SceneBuilder::for_fragment(&mut fragment); |
| render_blend_square(&mut sb, blend, Affine::IDENTITY); |
| fragment |
| } |
| |
| fn conflation_artifacts(sb: &mut SceneBuilder, _: &mut SceneParams) { |
| use PathEl::*; |
| const N: f64 = 50.0; |
| const S: f64 = 4.0; |
| |
| let scale = Affine::scale(S); |
| let x = N + 0.5; // Fractional pixel offset reveals the problem on axis-aligned edges. |
| let mut y = N; |
| |
| let bg_color = Color::rgb8(255, 194, 19); |
| let fg_color = Color::rgb8(12, 165, 255); |
| |
| // Two adjacent triangles touching at diagonal edge with opposing winding numbers |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((x, y)) * scale, |
| fg_color, |
| None, |
| &[ |
| // triangle 1 |
| MoveTo((0.0, 0.0).into()), |
| LineTo((N, N).into()), |
| LineTo((0.0, N).into()), |
| LineTo((0.0, 0.0).into()), |
| // triangle 2 |
| MoveTo((0.0, 0.0).into()), |
| LineTo((N, N).into()), |
| LineTo((N, 0.0).into()), |
| LineTo((0.0, 0.0).into()), |
| ], |
| ); |
| |
| // Adjacent rects, opposite winding |
| y += S * N + 10.0; |
| sb.fill( |
| Fill::EvenOdd, |
| Affine::translate((x, y)) * scale, |
| bg_color, |
| None, |
| &Rect::new(0.0, 0.0, N, N), |
| ); |
| sb.fill( |
| Fill::EvenOdd, |
| Affine::translate((x, y)) * scale, |
| fg_color, |
| None, |
| &[ |
| // left rect |
| MoveTo((0.0, 0.0).into()), |
| LineTo((0.0, N).into()), |
| LineTo((N * 0.5, N).into()), |
| LineTo((N * 0.5, 0.0).into()), |
| // right rect |
| MoveTo((N * 0.5, 0.0).into()), |
| LineTo((N, 0.0).into()), |
| LineTo((N, N).into()), |
| LineTo((N * 0.5, N).into()), |
| ], |
| ); |
| |
| // Adjacent rects, same winding |
| y += S * N + 10.0; |
| sb.fill( |
| Fill::EvenOdd, |
| Affine::translate((x, y)) * scale, |
| bg_color, |
| None, |
| &Rect::new(0.0, 0.0, N, N), |
| ); |
| sb.fill( |
| Fill::EvenOdd, |
| Affine::translate((x, y)) * scale, |
| fg_color, |
| None, |
| &[ |
| // left rect |
| MoveTo((0.0, 0.0).into()), |
| LineTo((0.0, N).into()), |
| LineTo((N * 0.5, N).into()), |
| LineTo((N * 0.5, 0.0).into()), |
| // right rect |
| MoveTo((N * 0.5, 0.0).into()), |
| LineTo((N * 0.5, N).into()), |
| LineTo((N, N).into()), |
| LineTo((N, 0.0).into()), |
| ], |
| ); |
| } |
| |
| fn labyrinth(sb: &mut SceneBuilder, _: &mut SceneParams) { |
| use PathEl::*; |
| |
| let rows: &[[u8; 12]] = &[ |
| [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1], |
| [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1], |
| [1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0], |
| [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1], |
| [1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0], |
| [0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], |
| [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1], |
| [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1], |
| [0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0], |
| [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| ]; |
| let cols: &[[u8; 10]] = &[ |
| [1, 1, 1, 1, 0, 1, 1, 1, 1, 1], |
| [0, 0, 1, 0, 0, 0, 1, 1, 1, 0], |
| [0, 1, 1, 0, 1, 1, 1, 0, 0, 1], |
| [1, 1, 0, 0, 0, 0, 1, 0, 1, 0], |
| [0, 0, 1, 0, 1, 0, 0, 0, 0, 1], |
| [0, 0, 1, 1, 1, 0, 0, 0, 1, 0], |
| [0, 1, 0, 1, 1, 1, 0, 0, 0, 0], |
| [1, 1, 1, 0, 1, 1, 1, 0, 1, 0], |
| [1, 1, 0, 1, 1, 0, 0, 0, 1, 0], |
| [0, 0, 1, 0, 0, 0, 0, 0, 0, 1], |
| [0, 0, 1, 1, 0, 0, 0, 0, 1, 0], |
| [0, 0, 0, 0, 0, 0, 1, 0, 0, 1], |
| [1, 1, 1, 1, 1, 1, 0, 1, 1, 1], |
| ]; |
| let mut path = BezPath::new(); |
| for (y, row) in rows.iter().enumerate() { |
| for (x, flag) in row.iter().enumerate() { |
| let x = x as f64; |
| let y = y as f64; |
| if *flag == 1 { |
| path.push(MoveTo((x - 0.1, y + 0.1).into())); |
| path.push(LineTo((x + 1.1, y + 0.1).into())); |
| path.push(LineTo((x + 1.1, y - 0.1).into())); |
| path.push(LineTo((x - 0.1, y - 0.1).into())); |
| |
| // The above is equivalent to the following stroke with width 0.2 and square |
| // caps. |
| //path.push(MoveTo((x, y).into())); |
| //path.push(LineTo((x + 1.0, y).into())); |
| } |
| } |
| } |
| for (x, col) in cols.iter().enumerate() { |
| for (y, flag) in col.iter().enumerate() { |
| let x = x as f64; |
| let y = y as f64; |
| if *flag == 1 { |
| path.push(MoveTo((x - 0.1, y - 0.1).into())); |
| path.push(LineTo((x - 0.1, y + 1.1).into())); |
| path.push(LineTo((x + 0.1, y + 1.1).into())); |
| path.push(LineTo((x + 0.1, y - 0.1).into())); |
| // The above is equivalent to the following stroke with width 0.2 and square |
| // caps. |
| //path.push(MoveTo((x, y).into())); |
| //path.push(LineTo((x, y + 1.0).into())); |
| } |
| } |
| } |
| |
| // Note the artifacts are clearly visible at a fractional pixel offset/translation. They |
| // disappear if the translation amount below is a whole number. |
| sb.fill( |
| Fill::NonZero, |
| Affine::translate((20.5, 20.5)) * Affine::scale(80.0), |
| Color::rgba8(0x70, 0x80, 0x80, 0xff), |
| None, |
| &path, |
| ) |
| } |
| |
| fn base_color_test(sb: &mut SceneBuilder, params: &mut SceneParams) { |
| // Cycle through the hue value every 5 seconds (t % 5) * 360/5 |
| let color = Color::hlc((params.time % 5.0) * 72.0, 80.0, 80.0); |
| params.base_color = Some(color); |
| |
| // Blend a white square over it. |
| sb.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| Color::rgba8(255, 255, 255, 128), |
| None, |
| &Rect::new(50.0, 50.0, 500.0, 500.0), |
| ); |
| } |
| |
| fn around_center(xform: Affine, center: Point) -> Affine { |
| Affine::translate(center.to_vec2()) * xform * Affine::translate(-center.to_vec2()) |
| } |
| |
| fn make_diamond(cx: f64, cy: f64) -> [PathEl; 5] { |
| const SIZE: f64 = 50.0; |
| [ |
| PathEl::MoveTo(Point::new(cx, cy - SIZE)), |
| PathEl::LineTo(Point::new(cx + SIZE, cy)), |
| PathEl::LineTo(Point::new(cx, cy + SIZE)), |
| PathEl::LineTo(Point::new(cx - SIZE, cy)), |
| PathEl::ClosePath, |
| ] |
| } |