1use kurbo::{Affine, Point, Rect};
12use oxframe_core::{Image, Signal};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum Input {
23 Frame,
25 Backdrop,
28}
29
30impl Input {
31 pub fn frame() -> Self {
33 Self::Frame
34 }
35
36 pub fn backdrop() -> Self {
38 Self::Backdrop
39 }
40
41 pub fn name(self) -> &'static str {
43 match self {
44 Self::Frame => "frame",
45 Self::Backdrop => "backdrop",
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum ShadeOp {
53 LensDrops {
55 wetness: f64,
57 seed: u64,
59 scale: f64,
61 time: f64,
63 },
64 Ripple {
67 center: (f64, f64),
69 strength: f64,
71 speed: f64,
73 time: f64,
75 },
76}
77
78impl ShadeOp {
79 pub fn name(&self) -> &'static str {
81 match self {
82 Self::LensDrops { .. } => "lens_drops",
83 Self::Ripple { .. } => "ripple",
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct Shade {
91 pub op: ShadeOp,
93 pub opacity: f64,
95 pub area: Option<Rect>,
98}
99
100pub 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 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
284fn 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
321fn 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
333pub 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#[doc(hidden)]
361#[derive(Clone)]
362pub struct EffectData {
363 pub kind: EffectKind,
365 pub strength: Signal<f64>,
367 pub speed: Signal<f64>,
369}
370
371#[doc(hidden)]
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum EffectKind {
375 Ripple,
377}
378
379impl EffectData {
380 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 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}