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    #[cfg(not(target_arch = "wasm32"))]
133    Custom(Box<crate::shader::CustomShade>),
134}
135
136impl ShadeOp {
137    /// `"lens_drops"`, `"ripple"` or `"custom"`.
138    pub fn name(&self) -> &'static str {
139        match self {
140            Self::LensDrops { .. } => "lens_drops",
141            Self::Ripple { .. } => "ripple",
142            #[cfg(not(target_arch = "wasm32"))]
143            Self::Custom(_) => "custom",
144        }
145    }
146}
147
148/// A [`ShadeOp`] and how much of it shows.
149#[derive(Debug, Clone, PartialEq)]
150pub struct Shade {
151    /// The operation.
152    pub op: ShadeOp,
153    /// 0 keeps the original pixels, 1 shows the result.
154    pub opacity: f64,
155    /// Part of the scene the operation may change, in scene coordinates.
156    /// `None` is the whole image.
157    pub area: Option<Rect>,
158}
159
160impl Shade {
161    /// Draw lists of the nodes this shade reads (`Input::node`), to draw
162    /// alone and pass to [`apply_with`]. Empty for the presets.
163    pub fn offscreen_lists(&self) -> Vec<&[crate::DrawItem]> {
164        match &self.op {
165            #[cfg(not(target_arch = "wasm32"))]
166            ShadeOp::Custom(c) => c.offscreen_lists(),
167            _ => Vec::new(),
168        }
169    }
170}
171
172/// Runs `shade` on `img`. `to_px` maps scene coordinates to the pixels of `img`.
173pub fn apply(shade: &Shade, img: &mut Image, to_px: Affine) {
174    apply_with(shade, img, to_px, &[]);
175}
176
177/// [`apply`] for a shade that reads nodes: `drawn` holds the images of
178/// [`Shade::offscreen_lists`], each drawn alone at the size of `img`.
179#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
180pub fn apply_with(shade: &Shade, img: &mut Image, to_px: Affine, drawn: &[Image]) {
181    let opacity = shade.opacity.clamp(0.0, 1.0) as f32;
182    if opacity <= 0.0 || img.width == 0 || img.height == 0 {
183        return;
184    }
185    // A custom shade samples the whole frame and limits itself to its box.
186    #[cfg(not(target_arch = "wasm32"))]
187    if let ShadeOp::Custom(c) = &shade.op {
188        c.apply(img, to_px, opacity, drawn);
189        return;
190    }
191    let Some(area) = shade.area else {
192        run(shade, img, to_px, opacity);
193        return;
194    };
195    let r = to_px.transform_rect_bbox(area);
196    let x0 = r.x0.floor().clamp(0.0, f64::from(img.width)) as u32;
197    let y0 = r.y0.floor().clamp(0.0, f64::from(img.height)) as u32;
198    let x1 = r.x1.ceil().clamp(0.0, f64::from(img.width)) as u32;
199    let y1 = r.y1.ceil().clamp(0.0, f64::from(img.height)) as u32;
200    if x1 <= x0 || y1 <= y0 {
201        return;
202    }
203    if (x0, y0, x1, y1) == (0, 0, img.width, img.height) {
204        run(shade, img, to_px, opacity);
205        return;
206    }
207    let (cw, ch) = (x1 - x0, y1 - y0);
208    let width = img.width;
209    let row = |y: u32| ((y * width + x0) * 4) as usize..((y * width + x1) * 4) as usize;
210    let mut crop = Image {
211        width: cw,
212        height: ch,
213        data: Vec::with_capacity((cw * ch * 4) as usize),
214    };
215    for y in y0..y1 {
216        crop.data.extend_from_slice(&img.data[row(y)]);
217    }
218    let shift = Affine::translate((-f64::from(x0), -f64::from(y0))) * to_px;
219    run(shade, &mut crop, shift, opacity);
220    let stride = (cw * 4) as usize;
221    for (i, y) in (y0..y1).enumerate() {
222        img.data[row(y)].copy_from_slice(&crop.data[i * stride..(i + 1) * stride]);
223    }
224}
225
226fn run(shade: &Shade, img: &mut Image, to_px: Affine, opacity: f32) {
227    match &shade.op {
228        ShadeOp::LensDrops {
229            wetness,
230            seed,
231            scale,
232            time,
233        } => lens_drops(img, *wetness, *seed, *scale, *time, opacity),
234        ShadeOp::Ripple {
235            center,
236            strength,
237            speed,
238            time,
239        } => ripple(img, to_px, *center, *strength, *speed, *time, opacity),
240        #[cfg(not(target_arch = "wasm32"))]
241        ShadeOp::Custom(c) => c.apply(img, to_px, opacity, &[]),
242    }
243}
244
245const MAX_DROPS: usize = 28;
246
247fn lens_drops(img: &mut Image, wetness: f64, seed: u64, scale: f64, time: f64, opacity: f32) {
248    let wet = wetness.clamp(0.0, 1.0);
249    if wet <= 0.0 || !scale.is_finite() || scale <= 0.0 {
250        return;
251    }
252    let (w, h) = (img.width as usize, img.height as usize);
253    let (fw, fh) = (w as f32, h as f32);
254    let side = fw.min(fh);
255    let src = img.data.clone();
256    let count = wet * MAX_DROPS as f64;
257    for i in 0..MAX_DROPS {
258        let grow = (count - i as f64).clamp(0.0, 1.0) as f32;
259        if grow <= 0.0 {
260            break;
261        }
262        let (a, b, c) = (
263            unit(seed, i as u64, 0),
264            unit(seed, i as u64, 1),
265            unit(seed, i as u64, 2),
266        );
267        let r = (0.035 + 0.05 * c) * side * scale as f32 * smooth(0.0, 1.0, grow);
268        let speed = 0.01 + 0.03 * b;
269        let cx = a * fw;
270        let cy = (b * 1.7 % 1.0 + (time as f32).max(0.0) * speed).rem_euclid(1.0) * fh;
271        if r < 1.0 {
272            continue;
273        }
274        let x0 = ((cx - r - 1.0).floor().max(0.0)) as usize;
275        let x1 = (((cx + r + 1.0).ceil()) as usize).min(w);
276        let y0 = ((cy - r * 1.15 - 1.0).floor().max(0.0)) as usize;
277        let y1 = (((cy + r * 1.15 + 1.0).ceil()) as usize).min(h);
278        for y in y0..y1 {
279            for x in x0..x1 {
280                let dx = x as f32 + 0.5 - cx;
281                let dy = y as f32 + 0.5 - cy;
282                let d = (dx * dx + (dy / 1.15) * (dy / 1.15)).sqrt() / r;
283                if d >= 1.0 {
284                    continue;
285                }
286                let edge = 1.0 - smooth(0.9, 1.0, d);
287                let mut px = sample(&src, w, h, cx - dx * 0.8, cy - dy * 0.8);
288                let rim = smooth(0.7, 0.97, d);
289                let light = if d > 1.0e-4 {
290                    (dx / (r * d)) * -0.7 + (dy / (r * d)) * -0.7
291                } else {
292                    0.0
293                };
294                let highlight = light.max(0.0).powi(3) * rim * 0.4;
295                let shade = (-light).max(0.0) * rim * 0.25;
296                for ch in px.iter_mut().take(3) {
297                    *ch = *ch * (1.0 - shade) + (1.0 - *ch) * highlight;
298                }
299                let i = (y * w + x) * 4;
300                let mix = edge * opacity;
301                for (k, v) in px.iter().enumerate() {
302                    let old = f32::from(img.data[i + k]) / 255.0;
303                    img.data[i + k] = byte(old + (v - old) * mix);
304                }
305            }
306        }
307    }
308}
309
310fn ripple(
311    img: &mut Image,
312    to_px: Affine,
313    center: (f64, f64),
314    strength: f64,
315    speed: f64,
316    time: f64,
317    opacity: f32,
318) {
319    let strength = strength.clamp(0.0, 1.0) as f32;
320    if strength <= 0.0 {
321        return;
322    }
323    let (w, h) = (img.width as usize, img.height as usize);
324    let c = to_px * Point::new(center.0, center.1);
325    let k = to_px.determinant().abs().sqrt().max(1.0e-6) as f32;
326    let (cx, cy) = (c.x as f32, c.y as f32);
327    let amp = 18.0 * strength * k;
328    let wavelength = 64.0 * k;
329    let reach = 520.0 * k;
330    let phase = (time * speed) as f32;
331    let src = img.data.clone();
332    let span = reach * 2.5;
333    let x0 = ((cx - span).floor().max(0.0)) as usize;
334    let x1 = (((cx + span).ceil().max(0.0)) as usize).min(w);
335    let y0 = ((cy - span).floor().max(0.0)) as usize;
336    let y1 = (((cy + span).ceil().max(0.0)) as usize).min(h);
337    for y in y0..y1 {
338        for x in x0..x1 {
339            let vx = x as f32 + 0.5 - cx;
340            let vy = y as f32 + 0.5 - cy;
341            let d = (vx * vx + vy * vy).sqrt();
342            if d < 1.0e-3 {
343                continue;
344            }
345            let env = (-d / reach).exp() * (1.0 - (-d / (12.0 * k)).exp());
346            let shift = amp * (d / wavelength * std::f32::consts::TAU - phase).sin() * env;
347            if shift.abs() < 1.0e-3 {
348                continue;
349            }
350            let px = sample(
351                &src,
352                w,
353                h,
354                x as f32 + 0.5 - vx / d * shift,
355                y as f32 + 0.5 - vy / d * shift,
356            );
357            // Only colors move: a pixel pulled from outside the node
358            // would cut a hole in it, so the outline (alpha) stays.
359            if px[3] < 1.0e-3 {
360                continue;
361            }
362            let i = (y * w + x) * 4;
363            for (k, v) in px.iter().enumerate().take(3) {
364                let old = f32::from(img.data[i + k]) / 255.0;
365                img.data[i + k] = byte(old + (v - old) * opacity);
366            }
367        }
368    }
369}
370
371/// Bilinear sample at pixel coordinates (centers at .5), straight alpha out.
372fn sample(data: &[u8], w: usize, h: usize, x: f32, y: f32) -> [f32; 4] {
373    let fx = (x - 0.5).clamp(0.0, (w - 1) as f32);
374    let fy = (y - 0.5).clamp(0.0, (h - 1) as f32);
375    let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
376    let (x1, y1) = ((x0 + 1).min(w - 1), (y0 + 1).min(h - 1));
377    let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
378    let mut acc = [0.0f32; 4];
379    for (ix, iy, wgt) in [
380        (x0, y0, (1.0 - tx) * (1.0 - ty)),
381        (x1, y0, tx * (1.0 - ty)),
382        (x0, y1, (1.0 - tx) * ty),
383        (x1, y1, tx * ty),
384    ] {
385        let i = (iy * w + ix) * 4;
386        let a = f32::from(data[i + 3]) / 255.0;
387        acc[0] += f32::from(data[i]) / 255.0 * a * wgt;
388        acc[1] += f32::from(data[i + 1]) / 255.0 * a * wgt;
389        acc[2] += f32::from(data[i + 2]) / 255.0 * a * wgt;
390        acc[3] += a * wgt;
391    }
392    if acc[3] > 1.0e-6 {
393        [acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3], acc[3]]
394    } else {
395        [0.0, 0.0, 0.0, 0.0]
396    }
397}
398
399fn smooth(a: f32, b: f32, x: f32) -> f32 {
400    let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
401    t * t * (3.0 - 2.0 * t)
402}
403
404fn byte(v: f32) -> u8 {
405    (v.clamp(0.0, 1.0) * 255.0).round() as u8
406}
407
408/// Deterministic value in `0..1` for `(seed, index, lane)`.
409fn unit(seed: u64, index: u64, lane: u64) -> f32 {
410    let mut z = seed
411        .wrapping_add(index.wrapping_mul(0x9E37_79B9_7F4A_7C15))
412        .wrapping_add(lane.wrapping_mul(0xBF58_476D_1CE4_E5B9))
413        .wrapping_add(0x94D0_49BB_1331_11EB);
414    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
415    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
416    z ^= z >> 31;
417    (z >> 40) as f32 / (1u64 << 24) as f32
418}
419
420/// Paints `top` over `dst` (both straight alpha, same size).
421///
422/// Drawing a list in two parts and laying the second over the first gives
423/// the same picture as drawing it at once, up to rounding, for every node
424/// with the normal blend.
425pub fn over(dst: &mut Image, top: &Image) {
426    debug_assert_eq!((dst.width, dst.height), (top.width, top.height));
427    for (d, t) in dst.data.chunks_exact_mut(4).zip(top.data.chunks_exact(4)) {
428        if t[3] == 0 {
429            continue;
430        }
431        if t[3] == 255 {
432            d.copy_from_slice(t);
433            continue;
434        }
435        let ta = f32::from(t[3]) / 255.0;
436        let da = f32::from(d[3]) / 255.0;
437        let a = ta + da * (1.0 - ta);
438        for k in 0..3 {
439            let c = f32::from(t[k]) / 255.0 * ta + f32::from(d[k]) / 255.0 * da * (1.0 - ta);
440            d[k] = byte(c / a);
441        }
442        d[3] = byte(a);
443    }
444}
445
446/// A shader effect stored on a node. Built with [`ripple`](crate::ripple).
447#[doc(hidden)]
448#[derive(Clone)]
449pub struct EffectData {
450    /// Which operation.
451    pub kind: EffectKind,
452    /// 0 leaves the node alone, 1 is the full effect.
453    pub strength: Signal<f64>,
454    /// How fast the effect moves.
455    pub speed: Signal<f64>,
456}
457
458/// Effects a node can carry.
459#[doc(hidden)]
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum EffectKind {
462    /// Rings from the node's center.
463    Ripple,
464}
465
466impl EffectData {
467    /// The marker for a node whose center is `center` (scene coordinates).
468    pub(crate) fn shade(&self, center: Point, time: f64) -> Shade {
469        match self.kind {
470            EffectKind::Ripple => Shade {
471                op: ShadeOp::Ripple {
472                    center: (center.x, center.y),
473                    strength: self.strength.get(),
474                    speed: self.speed.get(),
475                    time,
476                },
477                opacity: 1.0,
478                area: None,
479            },
480        }
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    fn gradient(w: u32, h: u32) -> Image {
489        let mut data = Vec::new();
490        for y in 0..h {
491            for x in 0..w {
492                data.extend_from_slice(&[(x * 255 / w) as u8, (y * 255 / h) as u8, 90, 255]);
493            }
494        }
495        Image {
496            width: w,
497            height: h,
498            data,
499        }
500    }
501
502    fn lens(wetness: f64, seed: u64, time: f64) -> Shade {
503        Shade {
504            op: ShadeOp::LensDrops {
505                wetness,
506                seed,
507                scale: 1.0,
508                time,
509            },
510            opacity: 1.0,
511            area: None,
512        }
513    }
514
515    #[test]
516    fn a_dry_lens_changes_nothing() {
517        let mut img = gradient(64, 48);
518        let before = img.data.clone();
519        apply(&lens(0.0, 1, 0.0), &mut img, Affine::IDENTITY);
520        assert_eq!(img.data, before);
521    }
522
523    #[test]
524    fn drops_change_pixels_and_follow_the_seed() {
525        let run = |seed| {
526            let mut img = gradient(160, 90);
527            apply(&lens(1.0, seed, 0.0), &mut img, Affine::IDENTITY);
528            img.data
529        };
530        let base = gradient(160, 90).data;
531        assert_ne!(run(7), base);
532        assert_eq!(run(7), run(7));
533        assert_ne!(run(7), run(8));
534    }
535
536    #[test]
537    fn a_wetter_lens_covers_more() {
538        let changed = |wetness| {
539            let mut img = gradient(160, 90);
540            let before = img.data.clone();
541            apply(&lens(wetness, 3, 0.0), &mut img, Affine::IDENTITY);
542            img.data.iter().zip(&before).filter(|(a, b)| a != b).count()
543        };
544        assert!(changed(1.0) > changed(0.3));
545    }
546
547    #[test]
548    fn ripple_moves_pixels_near_the_center_and_not_far_from_it() {
549        let mut img = gradient(200, 100);
550        let before = img.data.clone();
551        let shade = Shade {
552            op: ShadeOp::Ripple {
553                center: (0.0, 0.0),
554                strength: 1.0,
555                speed: 6.0,
556                time: 0.4,
557            },
558            opacity: 1.0,
559            area: None,
560        };
561        apply(&shade, &mut img, Affine::translate((100.0, 50.0)));
562        let at =
563            |d: &[u8], x: usize, y: usize| d[(y * 200 + x) * 4..(y * 200 + x) * 4 + 3].to_vec();
564        assert_ne!(at(&img.data, 130, 50), at(&before, 130, 50));
565    }
566
567    #[test]
568    fn an_area_keeps_the_rest_of_the_image() {
569        let mut img = gradient(160, 90);
570        let before = img.data.clone();
571        let mut shade = lens(1.0, 5, 0.0);
572        // Scene and image share pixels here; the area is the left half.
573        shade.area = Some(Rect::new(0.0, 0.0, 80.0, 90.0));
574        apply(&shade, &mut img, Affine::IDENTITY);
575        for y in 0..90usize {
576            for x in 80..160usize {
577                let i = (y * 160 + x) * 4;
578                assert_eq!(img.data[i..i + 4], before[i..i + 4], "({x}, {y})");
579            }
580        }
581        assert_ne!(img.data, before);
582    }
583
584    #[test]
585    fn over_an_opaque_pixel_with_a_clear_one_keeps_it() {
586        let mut dst = gradient(4, 4);
587        let before = dst.data.clone();
588        let clear = Image {
589            width: 4,
590            height: 4,
591            data: vec![0; 64],
592        };
593        over(&mut dst, &clear);
594        assert_eq!(dst.data, before);
595    }
596
597    #[test]
598    fn a_ripple_with_no_strength_is_a_no_op() {
599        let mut img = gradient(40, 40);
600        let before = img.data.clone();
601        let shade = Shade {
602            op: ShadeOp::Ripple {
603                center: (0.0, 0.0),
604                strength: 0.0,
605                speed: 6.0,
606                time: 1.0,
607            },
608            opacity: 1.0,
609            area: None,
610        };
611        apply(&shade, &mut img, Affine::translate((20.0, 20.0)));
612        assert_eq!(img.data, before);
613    }
614}