Skip to main content

oxframe_2d/
post.rs

1//! Pixel shaders that read what was drawn: the frame before a node, or the
2//! node's own layer.
3//!
4//! The engine draws the list in parts. It renders everything before a
5//! marker, runs the operation on those pixels, and draws the rest over the
6//! result. Both backends hand over pixels, so a frame with `lens_drops` or
7//! `ripple` looks the same on the CPU renderer and on the GPU one, and the
8//! operations themselves are plain functions of the pixels, the time and
9//! the parameters.
10
11use kurbo::{Affine, Point, Rect};
12use oxframe_core::{Image, Signal};
13
14/// What a shader reads besides its own parameters.
15///
16/// The frame and the backdrop are the pixels drawn before the node. Inside
17/// a layer (a group with opacity below 1, a blur, a clip, a blend, a shadow
18/// or an effect) both are what that layer drew before the node.
19///
20/// A custom `shade` reads an input named `"photo"` with
21/// `sample_photo(uv)`, which returns straight-alpha sRGB `vec4<f32>`;
22/// `uv` runs from 0 to 1 across the image and is clamped at the edges.
23#[derive(Debug, Clone)]
24#[non_exhaustive]
25pub enum Input {
26    /// Everything painted before this node, in screen coordinates.
27    Frame,
28    /// What the parent painted before this node. On a node that is not in a
29    /// layer this is the same as [`Input::Frame`].
30    Backdrop,
31    /// A PNG or JPEG image.
32    Image(std::rc::Rc<crate::image::RasterImage>),
33    /// Another node's pixels, usually one added with `.offscreen()`.
34    Node(crate::NodeId),
35}
36
37impl PartialEq for Input {
38    fn eq(&self, other: &Self) -> bool {
39        match (self, other) {
40            (Self::Frame, Self::Frame) | (Self::Backdrop, Self::Backdrop) => true,
41            (Self::Image(a), Self::Image(b)) => a.id == b.id,
42            (Self::Node(a), Self::Node(b)) => a == b,
43            _ => false,
44        }
45    }
46}
47
48impl Input {
49    /// The whole frame before this node.
50    pub fn frame() -> Self {
51        Self::Frame
52    }
53
54    /// What was painted before this node in its parent's layer.
55    pub fn backdrop() -> Self {
56        Self::Backdrop
57    }
58
59    /// A PNG or JPEG image from its bytes, e.g. `include_bytes!`.
60    ///
61    /// # Panics
62    ///
63    /// If the bytes are not a PNG or JPEG image.
64    pub fn image(bytes: &[u8]) -> Self {
65        match crate::image::ImageSource::decode(bytes) {
66            Ok(crate::image::ImageSource::Raster(r)) => Self::Image(r),
67            Ok(crate::image::ImageSource::Vector(_)) => {
68                panic!("a shader input is a PNG or JPEG image, not an SVG")
69            }
70            Err(e) => panic!("cannot decode image: {e}"),
71        }
72    }
73
74    /// Reads a PNG or JPEG file (native only).
75    ///
76    /// # Panics
77    ///
78    /// If the file cannot be read or decoded.
79    #[cfg(not(target_arch = "wasm32"))]
80    pub fn file(path: impl AsRef<std::path::Path>) -> Self {
81        let path = path.as_ref();
82        let bytes =
83            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
84        Self::image(&bytes)
85    }
86
87    /// The pixels of `node` and its children, drawn alone on a clear
88    /// background. `uv` runs from 0 to 1 across the node's box. Add the
89    /// node with `.offscreen()` to draw it only for the shader.
90    pub fn node(node: impl Into<crate::Node>) -> Self {
91        Self::Node(node.into().id())
92    }
93
94    /// `"frame"`, `"backdrop"`, `"image"` or `"node"`.
95    pub fn name(&self) -> &'static str {
96        match self {
97            Self::Frame => "frame",
98            Self::Backdrop => "backdrop",
99            Self::Image(_) => "image",
100            Self::Node(_) => "node",
101        }
102    }
103}
104
105/// What a marker in the draw list does to pixels.
106#[derive(Debug, Clone, PartialEq)]
107pub enum ShadeOp {
108    /// Drops on a lens that flip and magnify what is behind them.
109    LensDrops {
110        /// 0 is a dry lens, 1 is the most drops.
111        wetness: f64,
112        /// Same seed, same drops.
113        seed: u64,
114        /// Drop size multiplier.
115        scale: f64,
116        /// Scene time in seconds. Drops slide down slowly.
117        time: f64,
118    },
119    /// Rings that push the colors of a node outward from its center. The
120    /// node's outline stays where it is.
121    Ripple {
122        /// Center in scene coordinates.
123        center: (f64, f64),
124        /// 0 leaves the layer alone, 1 is a strong ring.
125        strength: f64,
126        /// Ring speed in radians per second.
127        speed: f64,
128        /// Scene time in seconds.
129        time: f64,
130    },
131    /// A custom `shade` that reads the frame drawn before it.
132    Custom(Box<crate::shader::CustomShade>),
133}
134
135impl ShadeOp {
136    /// `"lens_drops"`, `"ripple"` or `"custom"`.
137    pub fn name(&self) -> &'static str {
138        match self {
139            Self::LensDrops { .. } => "lens_drops",
140            Self::Ripple { .. } => "ripple",
141            Self::Custom(_) => "custom",
142        }
143    }
144}
145
146/// A [`ShadeOp`] and how much of it shows.
147#[derive(Debug, Clone, PartialEq)]
148pub struct Shade {
149    /// The operation.
150    pub op: ShadeOp,
151    /// 0 keeps the original pixels, 1 shows the result.
152    pub opacity: f64,
153    /// Part of the scene the operation may change, in scene coordinates.
154    /// `None` is the whole image.
155    pub area: Option<Rect>,
156}
157
158impl Shade {
159    /// Draw lists of the nodes this shade reads (`Input::node`), to draw
160    /// alone and pass to [`apply_with`]. Empty for the presets.
161    pub fn offscreen_lists(&self) -> Vec<&[crate::DrawItem]> {
162        match &self.op {
163            ShadeOp::Custom(c) => c.offscreen_lists(),
164            _ => Vec::new(),
165        }
166    }
167}
168
169/// Runs `shade` on `img`. `to_px` maps scene coordinates to the pixels of `img`.
170pub fn apply(shade: &Shade, img: &mut Image, to_px: Affine) {
171    apply_with(shade, img, to_px, &[]);
172}
173
174/// [`apply`] for a shade that reads nodes: `drawn` holds the images of
175/// [`Shade::offscreen_lists`], each drawn alone at the size of `img`.
176pub fn apply_with(shade: &Shade, img: &mut Image, to_px: Affine, drawn: &[Image]) {
177    let opacity = shade.opacity.clamp(0.0, 1.0) as f32;
178    if opacity <= 0.0 || img.width == 0 || img.height == 0 {
179        return;
180    }
181    // A custom shade samples the whole frame and limits itself to its box.
182    if let ShadeOp::Custom(c) = &shade.op {
183        c.apply(img, to_px, opacity, drawn);
184        return;
185    }
186    let Some(area) = shade.area else {
187        run(shade, img, to_px, opacity);
188        return;
189    };
190    let r = to_px.transform_rect_bbox(area);
191    let x0 = r.x0.floor().clamp(0.0, f64::from(img.width)) as u32;
192    let y0 = r.y0.floor().clamp(0.0, f64::from(img.height)) as u32;
193    let x1 = r.x1.ceil().clamp(0.0, f64::from(img.width)) as u32;
194    let y1 = r.y1.ceil().clamp(0.0, f64::from(img.height)) as u32;
195    if x1 <= x0 || y1 <= y0 {
196        return;
197    }
198    if (x0, y0, x1, y1) == (0, 0, img.width, img.height) {
199        run(shade, img, to_px, opacity);
200        return;
201    }
202    let (cw, ch) = (x1 - x0, y1 - y0);
203    let width = img.width;
204    let row = |y: u32| ((y * width + x0) * 4) as usize..((y * width + x1) * 4) as usize;
205    let mut crop = Image {
206        width: cw,
207        height: ch,
208        data: Vec::with_capacity((cw * ch * 4) as usize),
209    };
210    for y in y0..y1 {
211        crop.data.extend_from_slice(&img.data[row(y)]);
212    }
213    let shift = Affine::translate((-f64::from(x0), -f64::from(y0))) * to_px;
214    run(shade, &mut crop, shift, opacity);
215    let stride = (cw * 4) as usize;
216    for (i, y) in (y0..y1).enumerate() {
217        img.data[row(y)].copy_from_slice(&crop.data[i * stride..(i + 1) * stride]);
218    }
219}
220
221fn run(shade: &Shade, img: &mut Image, to_px: Affine, opacity: f32) {
222    match &shade.op {
223        ShadeOp::LensDrops {
224            wetness,
225            seed,
226            scale,
227            time,
228        } => lens_drops(img, *wetness, *seed, *scale, *time, opacity),
229        ShadeOp::Ripple {
230            center,
231            strength,
232            speed,
233            time,
234        } => ripple(img, to_px, *center, *strength, *speed, *time, opacity),
235        ShadeOp::Custom(c) => c.apply(img, to_px, opacity, &[]),
236    }
237}
238
239const MAX_DROPS: usize = 28;
240
241fn lens_drops(img: &mut Image, wetness: f64, seed: u64, scale: f64, time: f64, opacity: f32) {
242    let wet = wetness.clamp(0.0, 1.0);
243    if wet <= 0.0 || !scale.is_finite() || scale <= 0.0 {
244        return;
245    }
246    let (w, h) = (img.width as usize, img.height as usize);
247    let (fw, fh) = (w as f32, h as f32);
248    let side = fw.min(fh);
249    let src = img.data.clone();
250    let count = wet * MAX_DROPS as f64;
251    for i in 0..MAX_DROPS {
252        let grow = (count - i as f64).clamp(0.0, 1.0) as f32;
253        if grow <= 0.0 {
254            break;
255        }
256        let (a, b, c) = (
257            unit(seed, i as u64, 0),
258            unit(seed, i as u64, 1),
259            unit(seed, i as u64, 2),
260        );
261        let r = (0.035 + 0.05 * c) * side * scale as f32 * smooth(0.0, 1.0, grow);
262        let speed = 0.01 + 0.03 * b;
263        let cx = a * fw;
264        let cy = (b * 1.7 % 1.0 + (time as f32).max(0.0) * speed).rem_euclid(1.0) * fh;
265        if r < 1.0 {
266            continue;
267        }
268        let x0 = ((cx - r - 1.0).floor().max(0.0)) as usize;
269        let x1 = (((cx + r + 1.0).ceil()) as usize).min(w);
270        let y0 = ((cy - r * 1.15 - 1.0).floor().max(0.0)) as usize;
271        let y1 = (((cy + r * 1.15 + 1.0).ceil()) as usize).min(h);
272        for y in y0..y1 {
273            for x in x0..x1 {
274                let dx = x as f32 + 0.5 - cx;
275                let dy = y as f32 + 0.5 - cy;
276                let d = (dx * dx + (dy / 1.15) * (dy / 1.15)).sqrt() / r;
277                if d >= 1.0 {
278                    continue;
279                }
280                let edge = 1.0 - smooth(0.9, 1.0, d);
281                let mut px = sample(&src, w, h, cx - dx * 0.8, cy - dy * 0.8);
282                let rim = smooth(0.7, 0.97, d);
283                let light = if d > 1.0e-4 {
284                    (dx / (r * d)) * -0.7 + (dy / (r * d)) * -0.7
285                } else {
286                    0.0
287                };
288                let highlight = light.max(0.0).powi(3) * rim * 0.4;
289                let shade = (-light).max(0.0) * rim * 0.25;
290                for ch in px.iter_mut().take(3) {
291                    *ch = *ch * (1.0 - shade) + (1.0 - *ch) * highlight;
292                }
293                let i = (y * w + x) * 4;
294                let mix = edge * opacity;
295                for (k, v) in px.iter().enumerate() {
296                    let old = f32::from(img.data[i + k]) / 255.0;
297                    img.data[i + k] = byte(old + (v - old) * mix);
298                }
299            }
300        }
301    }
302}
303
304fn ripple(
305    img: &mut Image,
306    to_px: Affine,
307    center: (f64, f64),
308    strength: f64,
309    speed: f64,
310    time: f64,
311    opacity: f32,
312) {
313    let strength = strength.clamp(0.0, 1.0) as f32;
314    if strength <= 0.0 {
315        return;
316    }
317    let (w, h) = (img.width as usize, img.height as usize);
318    let c = to_px * Point::new(center.0, center.1);
319    let k = to_px.determinant().abs().sqrt().max(1.0e-6) as f32;
320    let (cx, cy) = (c.x as f32, c.y as f32);
321    let amp = 18.0 * strength * k;
322    let wavelength = 64.0 * k;
323    let reach = 520.0 * k;
324    let phase = (time * speed) as f32;
325    let src = img.data.clone();
326    let span = reach * 2.5;
327    let x0 = ((cx - span).floor().max(0.0)) as usize;
328    let x1 = (((cx + span).ceil().max(0.0)) as usize).min(w);
329    let y0 = ((cy - span).floor().max(0.0)) as usize;
330    let y1 = (((cy + span).ceil().max(0.0)) as usize).min(h);
331    for y in y0..y1 {
332        for x in x0..x1 {
333            let vx = x as f32 + 0.5 - cx;
334            let vy = y as f32 + 0.5 - cy;
335            let d = (vx * vx + vy * vy).sqrt();
336            if d < 1.0e-3 {
337                continue;
338            }
339            let env = (-d / reach).exp() * (1.0 - (-d / (12.0 * k)).exp());
340            let shift = amp * (d / wavelength * std::f32::consts::TAU - phase).sin() * env;
341            if shift.abs() < 1.0e-3 {
342                continue;
343            }
344            let px = sample(
345                &src,
346                w,
347                h,
348                x as f32 + 0.5 - vx / d * shift,
349                y as f32 + 0.5 - vy / d * shift,
350            );
351            // Only colors move: a pixel pulled from outside the node
352            // would cut a hole in it, so the outline (alpha) stays.
353            if px[3] < 1.0e-3 {
354                continue;
355            }
356            let i = (y * w + x) * 4;
357            for (k, v) in px.iter().enumerate().take(3) {
358                let old = f32::from(img.data[i + k]) / 255.0;
359                img.data[i + k] = byte(old + (v - old) * opacity);
360            }
361        }
362    }
363}
364
365/// Bilinear sample at pixel coordinates (centers at .5), straight alpha out.
366fn sample(data: &[u8], w: usize, h: usize, x: f32, y: f32) -> [f32; 4] {
367    let fx = (x - 0.5).clamp(0.0, (w - 1) as f32);
368    let fy = (y - 0.5).clamp(0.0, (h - 1) as f32);
369    let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
370    let (x1, y1) = ((x0 + 1).min(w - 1), (y0 + 1).min(h - 1));
371    let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
372    let mut acc = [0.0f32; 4];
373    for (ix, iy, wgt) in [
374        (x0, y0, (1.0 - tx) * (1.0 - ty)),
375        (x1, y0, tx * (1.0 - ty)),
376        (x0, y1, (1.0 - tx) * ty),
377        (x1, y1, tx * ty),
378    ] {
379        let i = (iy * w + ix) * 4;
380        let a = f32::from(data[i + 3]) / 255.0;
381        acc[0] += f32::from(data[i]) / 255.0 * a * wgt;
382        acc[1] += f32::from(data[i + 1]) / 255.0 * a * wgt;
383        acc[2] += f32::from(data[i + 2]) / 255.0 * a * wgt;
384        acc[3] += a * wgt;
385    }
386    if acc[3] > 1.0e-6 {
387        [acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3], acc[3]]
388    } else {
389        [0.0, 0.0, 0.0, 0.0]
390    }
391}
392
393fn smooth(a: f32, b: f32, x: f32) -> f32 {
394    let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
395    t * t * (3.0 - 2.0 * t)
396}
397
398fn byte(v: f32) -> u8 {
399    (v.clamp(0.0, 1.0) * 255.0).round() as u8
400}
401
402/// Deterministic value in `0..1` for `(seed, index, lane)`.
403fn unit(seed: u64, index: u64, lane: u64) -> f32 {
404    let mut z = seed
405        .wrapping_add(index.wrapping_mul(0x9E37_79B9_7F4A_7C15))
406        .wrapping_add(lane.wrapping_mul(0xBF58_476D_1CE4_E5B9))
407        .wrapping_add(0x94D0_49BB_1331_11EB);
408    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
409    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
410    z ^= z >> 31;
411    (z >> 40) as f32 / (1u64 << 24) as f32
412}
413
414/// Paints `top` over `dst` (both straight alpha, same size).
415///
416/// Drawing a list in two parts and laying the second over the first gives
417/// the same picture as drawing it at once, up to rounding, for every node
418/// with the normal blend.
419pub fn over(dst: &mut Image, top: &Image) {
420    debug_assert_eq!((dst.width, dst.height), (top.width, top.height));
421    for (d, t) in dst.data.chunks_exact_mut(4).zip(top.data.chunks_exact(4)) {
422        if t[3] == 0 {
423            continue;
424        }
425        if t[3] == 255 {
426            d.copy_from_slice(t);
427            continue;
428        }
429        let ta = f32::from(t[3]) / 255.0;
430        let da = f32::from(d[3]) / 255.0;
431        let a = ta + da * (1.0 - ta);
432        for k in 0..3 {
433            let c = f32::from(t[k]) / 255.0 * ta + f32::from(d[k]) / 255.0 * da * (1.0 - ta);
434            d[k] = byte(c / a);
435        }
436        d[3] = byte(a);
437    }
438}
439
440/// A shader effect stored on a node. Built with [`ripple`](crate::ripple).
441#[doc(hidden)]
442#[derive(Clone)]
443pub struct EffectData {
444    /// Which operation.
445    pub kind: EffectKind,
446    /// 0 leaves the node alone, 1 is the full effect.
447    pub strength: Signal<f64>,
448    /// How fast the effect moves.
449    pub speed: Signal<f64>,
450}
451
452/// Effects a node can carry.
453#[doc(hidden)]
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum EffectKind {
456    /// Rings from the node's center.
457    Ripple,
458}
459
460impl EffectData {
461    /// The marker for a node whose center is `center` (scene coordinates).
462    pub(crate) fn shade(&self, center: Point, time: f64) -> Shade {
463        match self.kind {
464            EffectKind::Ripple => Shade {
465                op: ShadeOp::Ripple {
466                    center: (center.x, center.y),
467                    strength: self.strength.get(),
468                    speed: self.speed.get(),
469                    time,
470                },
471                opacity: 1.0,
472                area: None,
473            },
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn gradient(w: u32, h: u32) -> Image {
483        let mut data = Vec::new();
484        for y in 0..h {
485            for x in 0..w {
486                data.extend_from_slice(&[(x * 255 / w) as u8, (y * 255 / h) as u8, 90, 255]);
487            }
488        }
489        Image {
490            width: w,
491            height: h,
492            data,
493        }
494    }
495
496    fn lens(wetness: f64, seed: u64, time: f64) -> Shade {
497        Shade {
498            op: ShadeOp::LensDrops {
499                wetness,
500                seed,
501                scale: 1.0,
502                time,
503            },
504            opacity: 1.0,
505            area: None,
506        }
507    }
508
509    #[test]
510    fn a_dry_lens_changes_nothing() {
511        let mut img = gradient(64, 48);
512        let before = img.data.clone();
513        apply(&lens(0.0, 1, 0.0), &mut img, Affine::IDENTITY);
514        assert_eq!(img.data, before);
515    }
516
517    #[test]
518    fn drops_change_pixels_and_follow_the_seed() {
519        let run = |seed| {
520            let mut img = gradient(160, 90);
521            apply(&lens(1.0, seed, 0.0), &mut img, Affine::IDENTITY);
522            img.data
523        };
524        let base = gradient(160, 90).data;
525        assert_ne!(run(7), base);
526        assert_eq!(run(7), run(7));
527        assert_ne!(run(7), run(8));
528    }
529
530    #[test]
531    fn a_wetter_lens_covers_more() {
532        let changed = |wetness| {
533            let mut img = gradient(160, 90);
534            let before = img.data.clone();
535            apply(&lens(wetness, 3, 0.0), &mut img, Affine::IDENTITY);
536            img.data.iter().zip(&before).filter(|(a, b)| a != b).count()
537        };
538        assert!(changed(1.0) > changed(0.3));
539    }
540
541    #[test]
542    fn ripple_moves_pixels_near_the_center_and_not_far_from_it() {
543        let mut img = gradient(200, 100);
544        let before = img.data.clone();
545        let shade = Shade {
546            op: ShadeOp::Ripple {
547                center: (0.0, 0.0),
548                strength: 1.0,
549                speed: 6.0,
550                time: 0.4,
551            },
552            opacity: 1.0,
553            area: None,
554        };
555        apply(&shade, &mut img, Affine::translate((100.0, 50.0)));
556        let at =
557            |d: &[u8], x: usize, y: usize| d[(y * 200 + x) * 4..(y * 200 + x) * 4 + 3].to_vec();
558        assert_ne!(at(&img.data, 130, 50), at(&before, 130, 50));
559    }
560
561    #[test]
562    fn an_area_keeps_the_rest_of_the_image() {
563        let mut img = gradient(160, 90);
564        let before = img.data.clone();
565        let mut shade = lens(1.0, 5, 0.0);
566        // Scene and image share pixels here; the area is the left half.
567        shade.area = Some(Rect::new(0.0, 0.0, 80.0, 90.0));
568        apply(&shade, &mut img, Affine::IDENTITY);
569        for y in 0..90usize {
570            for x in 80..160usize {
571                let i = (y * 160 + x) * 4;
572                assert_eq!(img.data[i..i + 4], before[i..i + 4], "({x}, {y})");
573            }
574        }
575        assert_ne!(img.data, before);
576    }
577
578    #[test]
579    fn over_an_opaque_pixel_with_a_clear_one_keeps_it() {
580        let mut dst = gradient(4, 4);
581        let before = dst.data.clone();
582        let clear = Image {
583            width: 4,
584            height: 4,
585            data: vec![0; 64],
586        };
587        over(&mut dst, &clear);
588        assert_eq!(dst.data, before);
589    }
590
591    #[test]
592    fn a_ripple_with_no_strength_is_a_no_op() {
593        let mut img = gradient(40, 40);
594        let before = img.data.clone();
595        let shade = Shade {
596            op: ShadeOp::Ripple {
597                center: (0.0, 0.0),
598                strength: 0.0,
599                speed: 6.0,
600                time: 1.0,
601            },
602            opacity: 1.0,
603            area: None,
604        };
605        apply(&shade, &mut img, Affine::translate((20.0, 20.0)));
606        assert_eq!(img.data, before);
607    }
608}