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