1use kurbo::{Affine, Point, Rect};
12use oxframe_core::{Image, Signal};
13
14#[derive(Debug, Clone)]
24#[non_exhaustive]
25pub enum Input {
26 Frame,
28 Backdrop,
31 Image(std::rc::Rc<crate::image::RasterImage>),
33 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 pub fn frame() -> Self {
51 Self::Frame
52 }
53
54 pub fn backdrop() -> Self {
56 Self::Backdrop
57 }
58
59 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 #[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 pub fn node(node: impl Into<crate::Node>) -> Self {
91 Self::Node(node.into().id())
92 }
93
94 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#[derive(Debug, Clone, PartialEq)]
107pub enum ShadeOp {
108 LensDrops {
110 wetness: f64,
112 seed: u64,
114 scale: f64,
116 time: f64,
118 },
119 Ripple {
122 center: (f64, f64),
124 strength: f64,
126 speed: f64,
128 time: f64,
130 },
131 Custom(Box<crate::shader::CustomShade>),
133}
134
135impl ShadeOp {
136 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#[derive(Debug, Clone, PartialEq)]
148pub struct Shade {
149 pub op: ShadeOp,
151 pub opacity: f64,
153 pub area: Option<Rect>,
156}
157
158impl Shade {
159 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
169pub fn apply(shade: &Shade, img: &mut Image, to_px: Affine) {
171 apply_with(shade, img, to_px, &[]);
172}
173
174pub 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 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 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
365fn 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
402fn 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
414pub 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#[doc(hidden)]
442#[derive(Clone)]
443pub struct EffectData {
444 pub kind: EffectKind,
446 pub strength: Signal<f64>,
448 pub speed: Signal<f64>,
450}
451
452#[doc(hidden)]
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum EffectKind {
456 Ripple,
458}
459
460impl EffectData {
461 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 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}