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 angle = d / wavelength * std::f32::consts::TAU - phase;
341 let shift = amp * angle.sin() * env;
342 let light = 0.28 * strength * angle.cos() * env;
346 if shift.abs() < 1.0e-3 && light.abs() < 1.0e-3 {
347 continue;
348 }
349 let mut px = sample(
350 &src,
351 w,
352 h,
353 x as f32 + 0.5 - vx / d * shift,
354 y as f32 + 0.5 - vy / d * shift,
355 );
356 if px[3] < 1.0e-3 {
359 continue;
360 }
361 for v in px.iter_mut().take(3) {
362 *v = (*v * (1.0 + light)).min(1.0);
363 }
364 let i = (y * w + x) * 4;
365 for (k, v) in px.iter().enumerate().take(3) {
366 let old = f32::from(img.data[i + k]) / 255.0;
367 img.data[i + k] = byte(old + (v - old) * opacity);
368 }
369 }
370 }
371}
372
373fn sample(data: &[u8], w: usize, h: usize, x: f32, y: f32) -> [f32; 4] {
375 let fx = (x - 0.5).clamp(0.0, (w - 1) as f32);
376 let fy = (y - 0.5).clamp(0.0, (h - 1) as f32);
377 let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
378 let (x1, y1) = ((x0 + 1).min(w - 1), (y0 + 1).min(h - 1));
379 let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
380 let mut acc = [0.0f32; 4];
381 for (ix, iy, wgt) in [
382 (x0, y0, (1.0 - tx) * (1.0 - ty)),
383 (x1, y0, tx * (1.0 - ty)),
384 (x0, y1, (1.0 - tx) * ty),
385 (x1, y1, tx * ty),
386 ] {
387 let i = (iy * w + ix) * 4;
388 let a = f32::from(data[i + 3]) / 255.0;
389 acc[0] += f32::from(data[i]) / 255.0 * a * wgt;
390 acc[1] += f32::from(data[i + 1]) / 255.0 * a * wgt;
391 acc[2] += f32::from(data[i + 2]) / 255.0 * a * wgt;
392 acc[3] += a * wgt;
393 }
394 if acc[3] > 1.0e-6 {
395 [acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3], acc[3]]
396 } else {
397 [0.0, 0.0, 0.0, 0.0]
398 }
399}
400
401fn smooth(a: f32, b: f32, x: f32) -> f32 {
402 let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
403 t * t * (3.0 - 2.0 * t)
404}
405
406fn byte(v: f32) -> u8 {
407 (v.clamp(0.0, 1.0) * 255.0).round() as u8
408}
409
410fn unit(seed: u64, index: u64, lane: u64) -> f32 {
412 let mut z = seed
413 .wrapping_add(index.wrapping_mul(0x9E37_79B9_7F4A_7C15))
414 .wrapping_add(lane.wrapping_mul(0xBF58_476D_1CE4_E5B9))
415 .wrapping_add(0x94D0_49BB_1331_11EB);
416 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
417 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
418 z ^= z >> 31;
419 (z >> 40) as f32 / (1u64 << 24) as f32
420}
421
422pub fn over(dst: &mut Image, top: &Image) {
428 debug_assert_eq!((dst.width, dst.height), (top.width, top.height));
429 for (d, t) in dst.data.chunks_exact_mut(4).zip(top.data.chunks_exact(4)) {
430 if t[3] == 0 {
431 continue;
432 }
433 if t[3] == 255 {
434 d.copy_from_slice(t);
435 continue;
436 }
437 let ta = f32::from(t[3]) / 255.0;
438 let da = f32::from(d[3]) / 255.0;
439 let a = ta + da * (1.0 - ta);
440 for k in 0..3 {
441 let c = f32::from(t[k]) / 255.0 * ta + f32::from(d[k]) / 255.0 * da * (1.0 - ta);
442 d[k] = byte(c / a);
443 }
444 d[3] = byte(a);
445 }
446}
447
448#[doc(hidden)]
450#[derive(Clone)]
451pub struct EffectData {
452 pub kind: EffectKind,
454 pub strength: Signal<f64>,
456 pub speed: Signal<f64>,
458}
459
460#[doc(hidden)]
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub enum EffectKind {
464 Ripple,
466}
467
468impl EffectData {
469 pub(crate) fn shade(&self, center: Point, time: f64) -> Shade {
471 match self.kind {
472 EffectKind::Ripple => Shade {
473 op: ShadeOp::Ripple {
474 center: (center.x, center.y),
475 strength: self.strength.get(),
476 speed: self.speed.get(),
477 time,
478 },
479 opacity: 1.0,
480 area: None,
481 },
482 }
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 fn gradient(w: u32, h: u32) -> Image {
491 let mut data = Vec::new();
492 for y in 0..h {
493 for x in 0..w {
494 data.extend_from_slice(&[(x * 255 / w) as u8, (y * 255 / h) as u8, 90, 255]);
495 }
496 }
497 Image {
498 width: w,
499 height: h,
500 data,
501 }
502 }
503
504 fn lens(wetness: f64, seed: u64, time: f64) -> Shade {
505 Shade {
506 op: ShadeOp::LensDrops {
507 wetness,
508 seed,
509 scale: 1.0,
510 time,
511 },
512 opacity: 1.0,
513 area: None,
514 }
515 }
516
517 #[test]
518 fn a_dry_lens_changes_nothing() {
519 let mut img = gradient(64, 48);
520 let before = img.data.clone();
521 apply(&lens(0.0, 1, 0.0), &mut img, Affine::IDENTITY);
522 assert_eq!(img.data, before);
523 }
524
525 #[test]
526 fn drops_change_pixels_and_follow_the_seed() {
527 let run = |seed| {
528 let mut img = gradient(160, 90);
529 apply(&lens(1.0, seed, 0.0), &mut img, Affine::IDENTITY);
530 img.data
531 };
532 let base = gradient(160, 90).data;
533 assert_ne!(run(7), base);
534 assert_eq!(run(7), run(7));
535 assert_ne!(run(7), run(8));
536 }
537
538 #[test]
539 fn a_wetter_lens_covers_more() {
540 let changed = |wetness| {
541 let mut img = gradient(160, 90);
542 let before = img.data.clone();
543 apply(&lens(wetness, 3, 0.0), &mut img, Affine::IDENTITY);
544 img.data.iter().zip(&before).filter(|(a, b)| a != b).count()
545 };
546 assert!(changed(1.0) > changed(0.3));
547 }
548
549 #[test]
550 fn ripple_moves_pixels_near_the_center_and_not_far_from_it() {
551 let mut img = gradient(200, 100);
552 let before = img.data.clone();
553 let shade = Shade {
554 op: ShadeOp::Ripple {
555 center: (0.0, 0.0),
556 strength: 1.0,
557 speed: 6.0,
558 time: 0.4,
559 },
560 opacity: 1.0,
561 area: None,
562 };
563 apply(&shade, &mut img, Affine::translate((100.0, 50.0)));
564 let at =
565 |d: &[u8], x: usize, y: usize| d[(y * 200 + x) * 4..(y * 200 + x) * 4 + 3].to_vec();
566 assert_ne!(at(&img.data, 130, 50), at(&before, 130, 50));
567 }
568
569 #[test]
570 fn ripple_rings_show_on_a_flat_fill() {
571 let mut img = Image {
572 width: 200,
573 height: 100,
574 data: [40u8, 90, 160, 255].repeat(200 * 100),
575 };
576 let shade = Shade {
577 op: ShadeOp::Ripple {
578 center: (0.0, 0.0),
579 strength: 1.0,
580 speed: 6.0,
581 time: 0.4,
582 },
583 opacity: 1.0,
584 area: None,
585 };
586 apply(&shade, &mut img, Affine::translate((100.0, 50.0)));
587 let row: Vec<u8> = (100..200)
589 .map(|x| img.data[(50 * 200 + x) * 4 + 2])
590 .collect();
591 assert!(row.iter().any(|&b| b > 175), "no lit ring: {row:?}");
592 assert!(row.iter().any(|&b| b < 145), "no shaded ring: {row:?}");
593 assert!(img.data.chunks(4).all(|p| p[3] == 255));
595 }
596
597 #[test]
598 fn an_area_keeps_the_rest_of_the_image() {
599 let mut img = gradient(160, 90);
600 let before = img.data.clone();
601 let mut shade = lens(1.0, 5, 0.0);
602 shade.area = Some(Rect::new(0.0, 0.0, 80.0, 90.0));
604 apply(&shade, &mut img, Affine::IDENTITY);
605 for y in 0..90usize {
606 for x in 80..160usize {
607 let i = (y * 160 + x) * 4;
608 assert_eq!(img.data[i..i + 4], before[i..i + 4], "({x}, {y})");
609 }
610 }
611 assert_ne!(img.data, before);
612 }
613
614 #[test]
615 fn over_an_opaque_pixel_with_a_clear_one_keeps_it() {
616 let mut dst = gradient(4, 4);
617 let before = dst.data.clone();
618 let clear = Image {
619 width: 4,
620 height: 4,
621 data: vec![0; 64],
622 };
623 over(&mut dst, &clear);
624 assert_eq!(dst.data, before);
625 }
626
627 #[test]
628 fn a_ripple_with_no_strength_is_a_no_op() {
629 let mut img = gradient(40, 40);
630 let before = img.data.clone();
631 let shade = Shade {
632 op: ShadeOp::Ripple {
633 center: (0.0, 0.0),
634 strength: 0.0,
635 speed: 6.0,
636 time: 1.0,
637 },
638 opacity: 1.0,
639 area: None,
640 };
641 apply(&shade, &mut img, Affine::translate((20.0, 20.0)));
642 assert_eq!(img.data, before);
643 }
644}