1use crate::media::Frame;
29use crate::model::{Effect, EffectKind};
30
31fn px(v: f64, scale: f32, min: f64) -> usize {
35 if v <= 0.0 {
36 0
37 } else {
38 (v * scale as f64).round().max(min) as usize
39 }
40}
41
42pub fn gpu_only(kind: EffectKind) -> bool {
46 matches!(
47 kind,
48 EffectKind::JpegCompress
49 | EffectKind::MotionBlur
50 | EffectKind::Plane3d
51 | EffectKind::EdgeGlow
52 | EffectKind::BlobTrack
53 | EffectKind::Vhs
54 | EffectKind::Shader
55 )
56}
57
58pub fn apply(effect: &Effect, t: f64, scale: f32, img: &mut Frame, scratch: &mut Frame) {
60 if img.is_empty() || !effect.enabled || gpu_only(effect.kind) {
61 return;
62 }
63 let e = |i: usize| effect.at(i, t);
64 match effect.kind {
65 EffectKind::Blur => {
66 let r = px(e(0) * 0.5, scale, 1.0);
68 if r > 0 {
69 for _ in 0..3 {
70 blur_pass(img, scratch, r);
71 }
72 }
73 }
74 EffectKind::Pixelate => pixelate(img, if e(0) >= 2.0 { px(e(0), scale, 2.0) as u32 } else { 1 }),
76 EffectKind::Tint => {
77 let a = e(3).clamp(0.0, 1.0);
78 let target = [e(0), e(1), e(2)];
79 let mut lut = [[0u8; 256]; 3];
80 for c in 0..3 {
81 for (v, o) in lut[c].iter_mut().enumerate() {
82 *o = (v as f64 + (target[c].clamp(0.0, 255.0) - v as f64) * a + 0.5) as u8;
83 }
84 }
85 for px in img.rgba.chunks_exact_mut(4) {
86 px[0] = lut[0][px[0] as usize];
87 px[1] = lut[1][px[1] as usize];
88 px[2] = lut[2][px[2] as usize];
89 }
90 }
91 EffectKind::Color => color(img, e(0), e(1), e(2), e(3), e(4)),
92 EffectKind::Vignette => vignette(img, e(0) as f32, e(1) as f32, e(2) as f32),
93 EffectKind::Sharpen => sharpen(img, scratch, e(0) as f32, px(e(1), scale, 1.0)),
94 EffectKind::Invert => {
95 let a = e(0).clamp(0.0, 1.0);
96 let mut lut = [0u8; 256];
97 for (v, o) in lut.iter_mut().enumerate() {
98 *o = (v as f64 + (255.0 - 2.0 * v as f64) * a + 0.5) as u8;
99 }
100 for px in img.rgba.chunks_exact_mut(4) {
101 px[0] = lut[px[0] as usize];
102 px[1] = lut[px[1] as usize];
103 px[2] = lut[px[2] as usize];
104 }
105 }
106 EffectKind::Grayscale => {
107 let ai = (e(0).clamp(0.0, 1.0) * 256.0) as i32;
108 for px in img.rgba.chunks_exact_mut(4) {
109 let l = ((px[0] as u32 * 54 + px[1] as u32 * 183 + px[2] as u32 * 19) >> 8) as i32;
111 for c in 0..3 {
112 let v = px[c] as i32;
113 px[c] = (v + (((l - v) * ai) >> 8)) as u8;
114 }
115 }
116 }
117 EffectKind::Flip => flip(img, e(0) >= 0.5, e(1) >= 0.5),
118 EffectKind::Crop => crop(img, e(0) as f32, e(1) as f32, e(2) as f32, e(3) as f32, e(4) as f32),
119 EffectKind::Threshold => threshold(img, e(0) as f32, e(1) as f32, e(2) >= 0.5),
120 EffectKind::HueShift => hue_shift(img, e(0) as f32, e(1) as f32, e(2) as f32),
121 EffectKind::Levels => levels(img, e(0), e(1), e(2), e(3), e(4)),
122 EffectKind::Curves => curves(img, &std::array::from_fn::<f64, 12, _>(|i| e(i))),
123 EffectKind::ChromaKey => {
124 chroma_key(img, [e(0) as f32, e(1) as f32, e(2) as f32], e(3) as f32, e(4) as f32, e(5) >= 0.5, e(6) as f32)
125 }
126 EffectKind::ColorReplace => color_replace(
127 img,
128 [e(0) as f32, e(1) as f32, e(2) as f32],
129 [e(3) as f32, e(4) as f32, e(5) as f32],
130 e(6) as f32,
131 e(7) as f32,
132 ),
133 EffectKind::RecDot => rec_dot(img, t, scale, e(0), e(1), e(2), e(3) >= 0.5, e(4)),
134 EffectKind::Wobble | EffectKind::Plane3d => {}
136 EffectKind::JpegCompress
138 | EffectKind::MotionBlur
139 | EffectKind::EdgeGlow
140 | EffectKind::BlobTrack
141 | EffectKind::Vhs
142 | EffectKind::Shader => {}
143 }
144}
145
146pub fn wobble(effect: &Effect, t: f64) -> (f64, f64, f64, f64, f64) {
154 if effect.kind == EffectKind::Plane3d {
155 return (0.0, 0.0, effect.at(2, t), effect.at(0, t), effect.at(1, t));
156 }
157 let seed = effect.at(6, t);
158 let method = if effect.params.len() > 7 { effect.at(7, t) } else { 1.0 };
163 let smooth = if effect.params.len() > 8 { effect.at(8, t).clamp(0.0, 1.0) } else { 0.0 };
164 let freq = effect.at(5, t).max(0.0) / (1.0 + 7.0 * smooth);
165 let w = std::f64::consts::TAU * freq * t;
166 let n = |axis: f64| -> f64 { wobble_wave(method, w, seed * 1.7 + axis * 13.37) };
167 (
168 effect.at(0, t) * n(1.0),
169 effect.at(1, t) * n(2.0),
170 effect.at(2, t) * n(3.0),
171 effect.at(3, t) * n(4.0),
172 effect.at(4, t) * n(5.0),
173 )
174}
175
176fn wobble_wave(method: f64, w: f64, p: f64) -> f64 {
179 let hash = |i: f64| -> f64 {
181 let x = (i * 127.1 + p * 311.7).sin() * 43758.545;
182 2.0 * (x - x.floor()) - 1.0
183 };
184 let x = w / std::f64::consts::TAU;
185 let (i, f) = (x.floor(), x - x.floor());
186 match method.round() as i32 {
187 0 => (w + p).sin(),
189 2 => {
191 let (a, b, c, d) = (hash(i - 1.0), hash(i), hash(i + 1.0), hash(i + 2.0));
192 let (f2, f3) = (f * f, f * f * f);
193 0.5 * ((2.0 * b) + (c - a) * f + (2.0 * a - 5.0 * b + 4.0 * c - d) * f2 + (3.0 * (b - c) + d - a) * f3)
194 }
195 3 => {
197 let u = (x + p * 0.1).fract().abs();
198 4.0 * (u - 0.5).abs() - 1.0
199 }
200 4 => hash(i),
202 _ => 0.5 * (w + p).sin() + 0.35 * (w * 1.618_034 + p * 2.236).sin() + 0.15 * (w * 2.718_281_8 + p * 3.19).sin(),
204 }
205}
206
207pub fn track(frame: &Frame, params: &[f64]) -> Option<(f64, f64, f64)> {
214 if frame.is_empty() || params.len() < 4 {
215 return None;
216 }
217 let target = [params[0] as f32 / 255.0, params[1] as f32 / 255.0, params[2] as f32 / 255.0];
218 let tol = (params[3].clamp(0.0, 1.0) as f32) * 1.2;
219 let tol2 = tol * tol;
220 let (w, h) = (frame.width as usize, frame.height as usize);
221 let (mut sx, mut sy, mut n) = (0f64, 0f64, 0usize);
222 for y in 0..h {
223 let row = y * frame.stride();
224 for x in 0..w {
225 let p = &frame.rgba[row + x * 4..row + x * 4 + 4];
226 if p[3] < 8 {
227 continue;
228 }
229 let mut d2 = 0.0f32;
230 for c in 0..3 {
231 let d = p[c] as f32 / 255.0 - target[c];
232 d2 += d * d;
233 }
234 if d2 <= tol2 {
235 sx += x as f64 + 0.5;
236 sy += y as f64 + 0.5;
237 n += 1;
238 }
239 }
240 }
241 if n == 0 {
242 return None;
243 }
244 let nf = n as f64;
245 Some((sx / nf / w as f64, sy / nf / h as f64, nf / (w * h) as f64))
246}
247
248fn blur_pass(img: &mut Frame, scratch: &mut Frame, r: usize) {
250 scratch.resize(img.width, img.height);
251 blur_dir(img, scratch, true, r);
252 blur_dir(scratch, img, false, r);
253}
254
255fn blur_dir(src: &Frame, dst: &mut Frame, horiz: bool, r: usize) {
257 let (w, h) = (src.width as usize, src.height as usize);
258 if w == 0 || h == 0 {
259 return;
260 }
261 let (lines, len, ls, es) = if horiz { (h, w, w, 1) } else { (w, h, 1, w) };
262 let r = r.min(len - 1);
263 if r == 0 {
264 dst.rgba.copy_from_slice(&src.rgba);
265 return;
266 }
267 let norm = (2 * r + 1) as u32;
268 for l in 0..lines {
269 let at = |j: usize| (l * ls + j * es) * 4;
270 let mut sum = [0u32; 4];
271 let a0 = at(0);
272 for (k, s) in sum.iter_mut().enumerate() {
273 *s = (r as u32 + 1) * src.rgba[a0 + k] as u32;
274 }
275 for j in 1..=r {
276 let a = at(j.min(len - 1));
277 for (k, s) in sum.iter_mut().enumerate() {
278 *s += src.rgba[a + k] as u32;
279 }
280 }
281 for j in 0..len {
282 let d = at(j);
283 for (k, s) in sum.iter().enumerate() {
284 dst.rgba[d + k] = ((s + norm / 2) / norm) as u8;
285 }
286 let add = at((j + r + 1).min(len - 1));
287 let sub = at(j.saturating_sub(r));
288 for (k, s) in sum.iter_mut().enumerate() {
289 *s += src.rgba[add + k] as u32;
290 *s -= src.rgba[sub + k] as u32;
291 }
292 }
293 }
294}
295
296fn pixelate(img: &mut Frame, block: u32) {
298 if block <= 1 {
299 return;
300 }
301 let (w, h) = (img.width as usize, img.height as usize);
302 let b = block as usize;
303 let stride = img.stride();
304 for by in (0..h).step_by(b) {
305 let y1 = (by + b).min(h);
306 for bx in (0..w).step_by(b) {
307 let x1 = (bx + b).min(w);
308 let mut sum = [0u32; 4];
309 for y in by..y1 {
310 let row = y * stride;
311 for x in bx..x1 {
312 let p = row + x * 4;
313 for (k, s) in sum.iter_mut().enumerate() {
314 *s += img.rgba[p + k] as u32;
315 }
316 }
317 }
318 let n = ((y1 - by) * (x1 - bx)) as u32;
319 let avg = [
320 ((sum[0] + n / 2) / n) as u8,
321 ((sum[1] + n / 2) / n) as u8,
322 ((sum[2] + n / 2) / n) as u8,
323 ((sum[3] + n / 2) / n) as u8,
324 ];
325 for y in by..y1 {
326 let row = y * stride;
327 for x in bx..x1 {
328 img.rgba[row + x * 4..row + x * 4 + 4].copy_from_slice(&avg);
329 }
330 }
331 }
332 }
333}
334
335fn color(img: &mut Frame, bright: f64, contrast: f64, sat: f64, hue_deg: f64, gamma: f64) {
337 let g = if gamma > 0.001 { gamma } else { 1.0 };
338 let mut lut = [0u8; 256];
339 for (v, o) in lut.iter_mut().enumerate() {
340 let f = ((v as f64 / 255.0 - 0.5) * contrast + 0.5 + bright).clamp(0.0, 1.0);
341 *o = (f.powf(1.0 / g) * 255.0 + 0.5) as u8;
342 }
343 let use_mat = (sat - 1.0).abs() > 1e-3 || hue_deg.abs() > 1e-3;
344 if !use_mat {
345 for px in img.rgba.chunks_exact_mut(4) {
346 px[0] = lut[px[0] as usize];
347 px[1] = lut[px[1] as usize];
348 px[2] = lut[px[2] as usize];
349 }
350 return;
351 }
352 const L: [f64; 3] = [0.2126, 0.7152, 0.0722];
353 let mut sm = [[0.0f64; 3]; 3];
355 for (i, row) in sm.iter_mut().enumerate() {
356 for (j, m) in row.iter_mut().enumerate() {
357 *m = (1.0 - sat) * L[j] + if i == j { sat } else { 0.0 };
358 }
359 }
360 let (s, c) = hue_deg.to_radians().sin_cos();
362 let hm = [
363 [0.213 + c * 0.787 - s * 0.213, 0.715 - c * 0.715 - s * 0.715, 0.072 - c * 0.072 + s * 0.928],
364 [0.213 - c * 0.213 + s * 0.143, 0.715 + c * 0.285 + s * 0.140, 0.072 - c * 0.072 - s * 0.283],
365 [0.213 - c * 0.213 - s * 0.787, 0.715 - c * 0.715 + s * 0.715, 0.072 + c * 0.928 + s * 0.072],
366 ];
367 let mut m = [[0.0f32; 3]; 3];
369 for i in 0..3 {
370 for j in 0..3 {
371 m[i][j] = (hm[i][0] * sm[0][j] + hm[i][1] * sm[1][j] + hm[i][2] * sm[2][j]) as f32;
372 }
373 }
374 for px in img.rgba.chunks_exact_mut(4) {
375 let (r, gr, b) = (px[0] as f32, px[1] as f32, px[2] as f32);
376 for (i, row) in m.iter().enumerate() {
377 let v = (row[0] * r + row[1] * gr + row[2] * b).clamp(0.0, 255.0) as usize;
378 px[i] = lut[v];
379 }
380 }
381}
382
383fn smoothstep(x: f32) -> f32 {
384 let x = x.clamp(0.0, 1.0);
385 x * x * (3.0 - 2.0 * x)
386}
387
388fn vignette(img: &mut Frame, radius: f32, softness: f32, strength: f32) {
390 let (w, h) = (img.width as usize, img.height as usize);
391 let (cx, cy) = (w as f32 / 2.0, h as f32 / 2.0);
392 let inv_hd = 1.0 / (cx * cx + cy * cy).sqrt().max(1.0);
393 let soft = softness.max(1e-3);
394 let stride = img.stride();
395 for y in 0..h {
396 let dy = y as f32 + 0.5 - cy;
397 let row = y * stride;
398 for x in 0..w {
399 let dx = x as f32 + 0.5 - cx;
400 let d = (dx * dx + dy * dy).sqrt() * inv_hd;
401 let fall = smoothstep((d - radius) / soft);
402 if fall <= 0.0 {
403 continue;
404 }
405 let k = 1.0 - strength.clamp(0.0, 1.0) * fall;
406 let px = &mut img.rgba[row + x * 4..row + x * 4 + 4];
407 px[0] = (px[0] as f32 * k) as u8;
408 px[1] = (px[1] as f32 * k) as u8;
409 px[2] = (px[2] as f32 * k) as u8;
410 }
411 }
412}
413
414fn sharpen(img: &mut Frame, scratch: &mut Frame, amount: f32, r: usize) {
417 let (w, h) = (img.width as usize, img.height as usize);
418 if w == 0 || h == 0 || amount <= 0.0 {
419 return;
420 }
421 scratch.resize(img.width, img.height);
422 blur_dir(img, scratch, true, r);
423 let r = r.min(h - 1);
424 let norm = (2 * r + 1) as f32;
425 for x in 0..w {
426 let at = |y: usize| (y * w + x) * 4;
427 let mut sum = [0f32; 4];
428 let a0 = at(0);
429 for (k, s) in sum.iter_mut().enumerate() {
430 *s = (r as f32 + 1.0) * scratch.rgba[a0 + k] as f32;
431 }
432 for j in 1..=r {
433 let a = at(j.min(h - 1));
434 for (k, s) in sum.iter_mut().enumerate() {
435 *s += scratch.rgba[a + k] as f32;
436 }
437 }
438 for y in 0..h {
439 let d = at(y);
440 for k in 0..3 {
441 let blurred = sum[k] / norm;
442 let o = img.rgba[d + k] as f32;
443 img.rgba[d + k] = (o + amount * (o - blurred)).clamp(0.0, 255.0) as u8;
444 }
445 let add = at((y + r + 1).min(h - 1));
446 let sub = at(y.saturating_sub(r));
447 for (k, s) in sum.iter_mut().enumerate() {
448 *s += scratch.rgba[add + k] as f32;
449 *s -= scratch.rgba[sub + k] as f32;
450 }
451 }
452 }
453}
454
455fn flip(img: &mut Frame, horizontal: bool, vertical: bool) {
456 let (w, h) = (img.width as usize, img.height as usize);
457 let stride = img.stride();
458 if horizontal {
459 for y in 0..h {
460 let row = &mut img.rgba[y * stride..y * stride + w * 4];
461 for x in 0..w / 2 {
462 let (a, b) = (x * 4, (w - 1 - x) * 4);
463 for k in 0..4 {
464 row.swap(a + k, b + k);
465 }
466 }
467 }
468 }
469 if vertical {
470 for y in 0..h / 2 {
471 let (top, rest) = img.rgba.split_at_mut((h - 1 - y) * stride);
472 top[y * stride..y * stride + stride].swap_with_slice(&mut rest[..stride]);
473 }
474 }
475}
476
477fn crop(img: &mut Frame, l: f32, r: f32, tp: f32, b: f32, feather: f32) {
479 let (w, h) = (img.width as f32, img.height as f32);
480 let (x0, x1) = (l.clamp(0.0, 0.5) * w, w - r.clamp(0.0, 0.5) * w);
481 let (y0, y1) = (tp.clamp(0.0, 0.5) * h, h - b.clamp(0.0, 0.5) * h);
482 let fe = feather.clamp(0.0, 0.5) * w.min(h);
483 let stride = img.stride();
484 for y in 0..img.height as usize {
485 let yc = y as f32 + 0.5;
486 let dy = (yc - y0).min(y1 - yc);
487 let row = y * stride;
488 for x in 0..img.width as usize {
489 let xc = x as f32 + 0.5;
490 let d = (xc - x0).min(x1 - xc).min(dy);
491 let a = if d <= 0.0 {
492 0.0
493 } else if fe > 0.0 {
494 (d / fe).min(1.0)
495 } else {
496 1.0
497 };
498 if a >= 1.0 {
499 continue;
500 }
501 let p = row + x * 4 + 3;
502 img.rgba[p] = (img.rgba[p] as f32 * a) as u8;
503 }
504 }
505}
506
507fn lut_from(f: impl Fn(f64) -> f64) -> [u8; 256] {
509 std::array::from_fn(|v| (f(v as f64 / 255.0).clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
510}
511
512fn apply_lut(img: &mut Frame, lut: &[u8; 256]) {
513 for px in img.rgba.chunks_exact_mut(4) {
514 px[0] = lut[px[0] as usize];
515 px[1] = lut[px[1] as usize];
516 px[2] = lut[px[2] as usize];
517 }
518}
519
520fn threshold(img: &mut Frame, level: f32, softness: f32, per_channel: bool) {
522 let sm = softness.max(1e-4) * 0.5;
523 let lut: [u8; 256] = lut_from(|x| smoothstep((x as f32 - (level - sm)) / (2.0 * sm)) as f64);
524 if per_channel {
525 apply_lut(img, &lut);
526 return;
527 }
528 for px in img.rgba.chunks_exact_mut(4) {
529 let l = ((px[0] as u32 * 54 + px[1] as u32 * 183 + px[2] as u32 * 19) >> 8).min(255) as usize;
530 let v = lut[l];
531 px[0] = v;
532 px[1] = v;
533 px[2] = v;
534 }
535}
536
537fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
539 let mx = r.max(g).max(b);
540 let mn = r.min(g).min(b);
541 let l = (mx + mn) * 0.5;
542 let d = mx - mn;
543 if d <= 1e-5 {
544 return (0.0, 0.0, l);
545 }
546 let s = d / (1.0 - (2.0 * l - 1.0).abs()).max(1e-5);
547 let h = if mx == r {
548 ((g - b) / d).rem_euclid(6.0)
549 } else if mx == g {
550 (b - r) / d + 2.0
551 } else {
552 (r - g) / d + 4.0
553 };
554 (h / 6.0, s, l)
555}
556
557fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
558 let (s, l) = (s.clamp(0.0, 1.0), l.clamp(0.0, 1.0));
559 if s <= 0.0 {
560 return (l, l, l);
561 }
562 let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
563 let p = 2.0 * l - q;
564 let c = |t: f32| {
565 let u = t.rem_euclid(1.0);
566 if u < 1.0 / 6.0 {
567 p + (q - p) * 6.0 * u
568 } else if u < 0.5 {
569 q
570 } else if u < 2.0 / 3.0 {
571 p + (q - p) * (2.0 / 3.0 - u) * 6.0
572 } else {
573 p
574 }
575 };
576 (c(h + 1.0 / 3.0), c(h), c(h - 1.0 / 3.0))
577}
578
579fn hue_shift(img: &mut Frame, deg: f32, sat: f32, light: f32) {
581 let turn = deg / 360.0;
582 for px in img.rgba.chunks_exact_mut(4) {
583 let (h, s, l) = rgb_to_hsl(px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0);
584 let (r, g, b) = hsl_to_rgb(h + turn, (s * sat.max(0.0)).clamp(0.0, 1.0), l + light);
585 px[0] = (r.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
586 px[1] = (g.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
587 px[2] = (b.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
588 }
589}
590
591fn levels(img: &mut Frame, inb: f64, inw: f64, gamma: f64, outb: f64, outw: f64) {
593 let span = (inw - inb).max(1e-4);
594 let g = gamma.max(0.01);
595 let lut = lut_from(|x| outb + ((x - inb) / span).clamp(0.0, 1.0).powf(1.0 / g) * (outw - outb));
596 apply_lut(img, &lut);
597}
598
599pub fn curve_at(x: f64, a: f64, b: f64, c: f64) -> f64 {
602 let y = [0.0, a, b, c, 1.0];
603 let mut d = [0.0f64; 4];
604 for i in 0..4 {
605 d[i] = (y[i + 1] - y[i]) * 4.0;
606 }
607 let mut m = [0.0f64; 5];
608 m[0] = d[0];
609 m[4] = d[3];
610 for i in 1..4 {
611 m[i] = if d[i - 1] * d[i] <= 0.0 { 0.0 } else { (d[i - 1] + d[i]) * 0.5 };
612 }
613 for i in 0..4 {
614 if d[i].abs() < 1e-6 {
615 m[i] = 0.0;
616 m[i + 1] = 0.0;
617 } else {
618 let (ai, bi) = (m[i] / d[i], m[i + 1] / d[i]);
619 let s = ai * ai + bi * bi;
620 if s > 9.0 {
621 let k = 3.0 / s.sqrt();
622 m[i] = k * ai * d[i];
623 m[i + 1] = k * bi * d[i];
624 }
625 }
626 }
627 let xc = x.clamp(0.0, 1.0);
628 let i = ((xc * 4.0).floor() as usize).min(3);
629 let t = xc * 4.0 - i as f64;
630 let (t2, t3) = (t * t, t * t * t);
631 (2.0 * t3 - 3.0 * t2 + 1.0) * y[i]
632 + (t3 - 2.0 * t2 + t) * 0.25 * m[i]
633 + (-2.0 * t3 + 3.0 * t2) * y[i + 1]
634 + (t3 - t2) * 0.25 * m[i + 1]
635}
636
637fn curves(img: &mut Frame, k: &[f64; 12]) {
639 let master = lut_from(|x| curve_at(x, k[0], k[1], k[2]));
640 let chans: [[u8; 256]; 3] = [
641 lut_from(|x| curve_at(x, k[3], k[4], k[5])),
642 lut_from(|x| curve_at(x, k[6], k[7], k[8])),
643 lut_from(|x| curve_at(x, k[9], k[10], k[11])),
644 ];
645 let fold: [[u8; 256]; 3] = std::array::from_fn(|c| std::array::from_fn(|v| master[chans[c][v] as usize]));
647 for px in img.rgba.chunks_exact_mut(4) {
648 px[0] = fold[0][px[0] as usize];
649 px[1] = fold[1][px[1] as usize];
650 px[2] = fold[2][px[2] as usize];
651 }
652}
653
654fn to_ycc(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
655 (0.299 * r + 0.587 * g + 0.114 * b, -0.168736 * r - 0.331264 * g + 0.5 * b, 0.5 * r - 0.418688 * g - 0.081312 * b)
656}
657
658fn color_replace(img: &mut Frame, from: [f32; 3], to: [f32; 3], tol: f32, soft: f32) {
663 let f = from.map(|c| (c / 255.0).clamp(0.0, 1.0));
664 let t = to.map(|c| (c / 255.0).clamp(0.0, 1.0));
665 let tol = tol.clamp(0.0, 1.0);
666 let soft = soft.clamp(0.0, 1.0).max(0.0005);
667 for px in img.rgba.chunks_exact_mut(4) {
668 let c = [px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0];
669 let d = ((c[0] - f[0]).powi(2) + (c[1] - f[1]).powi(2) + (c[2] - f[2]).powi(2)).sqrt() / 1.732_050_8;
670 let hit = 1.0 - smoothstep(((d - tol) / soft).clamp(0.0, 1.0));
671 if hit <= 0.0 {
672 continue;
673 }
674 for i in 0..3 {
675 px[i] = ((c[i] + (t[i] - c[i]) * hit).clamp(0.0, 1.0) * 255.0).round() as u8;
676 }
677 }
678}
679
680fn chroma_key(img: &mut Frame, key: [f32; 3], similarity: f32, smoothness: f32, show_mask: bool, spill: f32) {
681 let (_, kb, kr) = to_ycc(key[0] / 255.0, key[1] / 255.0, key[2] / 255.0);
682 let klen = (kb * kb + kr * kr).sqrt();
683 let sim = similarity.clamp(0.0, 1.0) * 0.5;
684 let width = (smoothness.clamp(0.0, 1.0) * 0.5).max(0.0005);
685 let spill = spill.clamp(0.0, 1.0);
686 for px in img.rgba.chunks_exact_mut(4) {
687 let (r, g, b) = (px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0);
688 let (y, cb, cr) = to_ycc(r, g, b);
689 let a = smoothstep((((cb - kb).powi(2) + (cr - kr).powi(2)).sqrt() - sim) / width);
690 if show_mask {
691 let v = (a * 255.0 + 0.5) as u8;
692 px[0] = v;
693 px[1] = v;
694 px[2] = v;
695 px[3] = 255;
696 continue;
697 }
698 if spill > 0.0 && klen > 1e-3 {
699 let proj = (cb * kb + cr * kr) / klen;
700 if proj > 0.0 {
701 let (nb, nr) = (cb - kb / klen * proj * spill, cr - kr / klen * proj * spill);
702 let rgb = [y + 1.402 * nr, y - 0.344136 * nb - 0.714136 * nr, y + 1.772 * nb];
703 for c in 0..3 {
704 px[c] = (rgb[c].clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
705 }
706 }
707 }
708 px[3] = (px[3] as f32 * a) as u8;
709 }
710}
711
712const SEVEN_SEG: [u8; 10] = [0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F];
714const SEG_T: f32 = 0.11;
716const SEGMENTS: [(f32, f32, f32, f32); 7] = [
717 (0.5, SEG_T, 0.5 - SEG_T, SEG_T),
718 (1.0 - SEG_T, 0.25, SEG_T, 0.25 - SEG_T),
719 (1.0 - SEG_T, 0.75, SEG_T, 0.25 - SEG_T),
720 (0.5, 1.0 - SEG_T, 0.5 - SEG_T, SEG_T),
721 (SEG_T, 0.75, SEG_T, 0.25 - SEG_T),
722 (SEG_T, 0.25, SEG_T, 0.25 - SEG_T),
723 (0.5, 0.5, 0.5 - SEG_T, SEG_T),
724];
725
726fn fill_rect(img: &mut Frame, x0: f32, y0: f32, x1: f32, y1: f32, rgb: [u8; 3]) {
728 let (w, h) = (img.width as i64, img.height as i64);
729 let xs = (x0.round() as i64).clamp(0, w);
730 let xe = (x1.round() as i64).clamp(0, w);
731 let ys = (y0.round() as i64).clamp(0, h);
732 let ye = (y1.round() as i64).clamp(0, h);
733 let stride = img.stride();
734 for y in ys..ye {
735 let row = y as usize * stride;
736 for x in xs..xe {
737 let p = row + x as usize * 4;
738 img.rgba[p..p + 3].copy_from_slice(&rgb);
739 img.rgba[p + 3] = 255;
740 }
741 }
742}
743
744fn draw_digit(img: &mut Frame, x: f32, y: f32, w: f32, h: f32, digit: usize, rgb: [u8; 3]) {
745 let mask = SEVEN_SEG[digit.min(9)];
746 for (i, (cx, cy, hx, hy)) in SEGMENTS.iter().enumerate() {
747 if mask & (1 << i) != 0 {
748 fill_rect(img, x + (cx - hx) * w, y + (cy - hy) * h, x + (cx + hx) * w, y + (cy + hy) * h, rgb);
749 }
750 }
751}
752
753#[allow(clippy::too_many_arguments)]
755fn rec_dot(img: &mut Frame, t: f64, scale: f32, size: f64, hz: f64, corner: f64, timecode: bool, margin: f64) {
756 let sz = (size.max(2.0) * scale as f64) as f32;
757 let mg = (margin.max(0.0) * scale as f64) as f32;
758 let corner = corner.clamp(0.0, 3.0).round() as u8;
759 let (right, bottom) = (corner == 1 || corner == 3, corner == 2 || corner == 3);
760 let (w, h) = (img.width as f32, img.height as f32);
761 let ax = if right { w - mg - sz * 0.5 } else { mg + sz * 0.5 };
762 let ay = if bottom { h - mg - sz * 0.5 } else { mg + sz * 0.5 };
763 let on = hz <= 0.0 || (t * hz).rem_euclid(1.0) < 0.5;
764 if on {
765 let r = sz * 0.5;
766 let stride = img.stride();
767 for y in ((ay - r).max(0.0) as usize)..((ay + r).ceil().min(h) as usize) {
768 let row = y * stride;
769 for x in ((ax - r).max(0.0) as usize)..((ax + r).ceil().min(w) as usize) {
770 let (dx, dy) = (x as f32 + 0.5 - ax, y as f32 + 0.5 - ay);
771 let d = (dx * dx + dy * dy).sqrt();
772 let a = (r + 0.5 - d).clamp(0.0, 1.0);
773 if a <= 0.0 {
774 continue;
775 }
776 let p = row + x * 4;
777 for (c, v) in [230u8, 30, 30].into_iter().enumerate() {
778 img.rgba[p + c] = (img.rgba[p + c] as f32 + (v as f32 - img.rgba[p + c] as f32) * a) as u8;
779 }
780 img.rgba[p + 3] = img.rgba[p + 3].max((a * 255.0) as u8);
781 }
782 }
783 }
784 if !timecode {
785 return;
786 }
787 let (dh, dw, gap) = (sz, sz * 0.55, sz * 0.12);
788 let (cw, colw) = (dw + gap, sz * 0.3 + gap);
789 let total = 6.0 * cw + 2.0 * colw;
790 let mut x = if right { ax - sz * 0.5 - gap * 2.0 - total } else { ax + sz * 0.5 + gap * 2.0 };
791 let y0 = ay - dh * 0.5;
792 let secs = t.max(0.0) as u64;
793 let digits = [
794 (secs / 3600 % 100) / 10,
795 (secs / 3600 % 100) % 10,
796 (secs / 60 % 60) / 10,
797 (secs / 60 % 60) % 10,
798 (secs % 60) / 10,
799 (secs % 60) % 10,
800 ];
801 for (i, d) in digits.into_iter().enumerate() {
802 draw_digit(img, x, y0, dw, dh, d as usize, [235, 235, 235]);
803 x += cw;
804 if i == 1 || i == 3 {
805 let cx = x + colw * 0.5;
806 let r = sz * 0.07;
807 fill_rect(img, cx - r, y0 + dh * 0.33 - r, cx + r, y0 + dh * 0.33 + r, [235, 235, 235]);
808 fill_rect(img, cx - r, y0 + dh * 0.7 - r, cx + r, y0 + dh * 0.7 + r, [235, 235, 235]);
809 x += colw;
810 }
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use crate::model::EffectKind as K;
818
819 fn eff(kind: K, params: &[f64]) -> Effect {
820 let mut e = Effect::new(kind);
821 for (i, v) in params.iter().enumerate() {
822 e.params[i].value = *v;
823 }
824 e
825 }
826
827 fn noisy(w: u32, h: u32) -> Frame {
829 let mut f = Frame::new(w, h);
830 let mut s = 0x12345678u32;
831 for px in f.rgba.chunks_exact_mut(4) {
832 for c in px.iter_mut().take(3) {
833 s = s.wrapping_mul(1664525).wrapping_add(1013904223);
834 *c = (s >> 24) as u8;
835 }
836 px[3] = 255;
837 }
838 f
839 }
840
841 fn variance(f: &Frame) -> f64 {
842 let vals: Vec<f64> = f.rgba.chunks_exact(4).map(|p| p[0] as f64).collect();
843 let m = vals.iter().sum::<f64>() / vals.len() as f64;
844 vals.iter().map(|v| (v - m) * (v - m)).sum::<f64>() / vals.len() as f64
845 }
846
847 fn mean(f: &Frame, c: usize) -> f64 {
848 let n = (f.width * f.height) as f64;
849 f.rgba.chunks_exact(4).map(|p| p[c] as f64).sum::<f64>() / n
850 }
851
852 #[test]
853 fn blur_lowers_variance() {
854 let mut img = noisy(16, 16);
855 let v0 = variance(&img);
856 let mut scratch = Frame::default();
857 apply(&eff(K::Blur, &[4.0]), 0.0, 1.0, &mut img, &mut scratch);
858 assert!(variance(&img) < v0 * 0.5, "{} -> {}", v0, variance(&img));
859 let mut img2 = noisy(16, 16);
861 let before = img2.rgba.clone();
862 apply(&eff(K::Blur, &[0.0]), 0.0, 1.0, &mut img2, &mut scratch);
863 assert_eq!(img2.rgba, before);
864 for s in [0.05f32, 0.36, 0.5] {
866 let mut small = noisy(16, 16);
867 let v0 = variance(&small);
868 apply(&eff(K::Blur, &[1.0]), 0.0, s, &mut small, &mut scratch);
869 assert!(variance(&small) < v0, "scale {s} did not blur");
870 }
871 }
872
873 #[test]
874 fn pixelate_makes_blocks_uniform() {
875 let mut img = noisy(8, 8);
876 let mut scratch = Frame::default();
877 let mut small = noisy(8, 8);
879 let before = small.rgba.clone();
880 apply(&eff(K::Pixelate, &[4.0]), 0.0, 0.1, &mut small, &mut scratch);
881 assert_ne!(small.rgba, before, "block rounded down to a no-op");
882 let mut one = noisy(8, 8);
884 let before = one.rgba.clone();
885 apply(&eff(K::Pixelate, &[1.0]), 0.0, 1.0, &mut one, &mut scratch);
886 assert_eq!(one.rgba, before);
887 apply(&eff(K::Pixelate, &[4.0]), 0.0, 1.0, &mut img, &mut scratch);
888 for by in [0u32, 4] {
889 for bx in [0u32, 4] {
890 let i0 = ((by * 8 + bx) * 4) as usize;
891 let first = img.rgba[i0..i0 + 4].to_vec();
892 for y in by..by + 4 {
893 for x in bx..bx + 4 {
894 let i = ((y * 8 + x) * 4) as usize;
895 assert_eq!(&img.rgba[i..i + 4], &first[..], "block {bx},{by} at {x},{y}");
896 }
897 }
898 }
899 }
900 }
901
902 #[test]
903 fn tint_moves_towards_colour() {
904 let mut img = noisy(8, 8);
905 let d0 = mean(&img, 0);
906 let mut scratch = Frame::default();
907 apply(&eff(K::Tint, &[255.0, 0.0, 0.0, 0.5]), 0.0, 1.0, &mut img, &mut scratch);
908 assert!(mean(&img, 0) > d0, "red should rise");
909 assert!(mean(&img, 1) < 128.0, "green should fall towards 0");
910 apply(&eff(K::Tint, &[10.0, 20.0, 30.0, 1.0]), 0.0, 1.0, &mut img, &mut scratch);
912 for p in img.rgba.chunks_exact(4) {
913 assert_eq!(&p[..3], &[10, 20, 30]);
914 }
915 }
916
917 #[test]
918 fn color_brightness_contrast_saturation() {
919 let mut scratch = Frame::default();
920 let base = noisy(8, 8);
922 let mut prev = 0.0;
923 for b in [-0.5, 0.0, 0.5] {
924 let mut img = base.clone();
925 apply(&eff(K::Color, &[b, 1.0, 1.0, 0.0, 1.0]), 0.0, 1.0, &mut img, &mut scratch);
926 let m = mean(&img, 0);
927 assert!(m >= prev, "brightness {b}: {m} < {prev}");
928 prev = m;
929 }
930 let mut low = base.clone();
932 apply(&eff(K::Color, &[0.0, 0.5, 1.0, 0.0, 1.0]), 0.0, 1.0, &mut low, &mut scratch);
933 let mut high = base.clone();
934 apply(&eff(K::Color, &[0.0, 2.0, 1.0, 0.0, 1.0]), 0.0, 1.0, &mut high, &mut scratch);
935 assert!(variance(&low) < variance(&base));
936 assert!(variance(&high) > variance(&low));
937 let mut gray = base.clone();
939 apply(&eff(K::Color, &[0.0, 1.0, 0.0, 0.0, 1.0]), 0.0, 1.0, &mut gray, &mut scratch);
940 for p in gray.rgba.chunks_exact(4) {
941 assert!((p[0] as i32 - p[1] as i32).abs() <= 1 && (p[1] as i32 - p[2] as i32).abs() <= 1, "{p:?}");
942 }
943 let mut g = Frame::new(2, 2);
945 g.fill([128, 128, 128, 255]);
946 apply(&eff(K::Color, &[0.0, 1.0, 1.0, 0.0, 2.0]), 0.0, 1.0, &mut g, &mut scratch);
947 assert!(g.rgba[0] > 150, "{}", g.rgba[0]);
948 }
949
950 #[test]
951 fn invert_and_grayscale() {
952 let mut img = Frame::new(2, 1);
953 img.rgba.copy_from_slice(&[200, 50, 100, 255, 0, 255, 30, 255]);
954 let mut scratch = Frame::default();
955 apply(&eff(K::Invert, &[1.0]), 0.0, 1.0, &mut img, &mut scratch);
956 assert_eq!(&img.rgba[..4], &[55, 205, 155, 255]);
957 assert_eq!(&img.rgba[4..8], &[255, 0, 225, 255]);
958 let before = img.rgba.clone();
960 apply(&eff(K::Invert, &[0.0]), 0.0, 1.0, &mut img, &mut scratch);
961 assert_eq!(img.rgba, before);
962 let mut img = Frame::new(1, 1);
964 img.rgba.copy_from_slice(&[255, 0, 0, 255]);
965 apply(&eff(K::Grayscale, &[1.0]), 0.0, 1.0, &mut img, &mut scratch);
966 let p = img.rgba[..3].to_vec();
967 assert!((p[0] as i32 - p[1] as i32).abs() <= 1, "{p:?}");
968 assert!(p[0] > 40 && p[0] < 70, "Rec709 red luma ~54: {p:?}");
969 }
970
971 #[test]
972 fn flip_mirrors() {
973 let mut img = Frame::new(2, 2);
974 img.rgba.copy_from_slice(&[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]);
976 let mut scratch = Frame::default();
977 apply(&eff(K::Flip, &[1.0, 0.0]), 0.0, 1.0, &mut img, &mut scratch);
978 assert_eq!(&img.rgba[..4], &[0, 255, 0, 255], "horizontal: TL is old TR");
979 apply(&eff(K::Flip, &[0.0, 1.0]), 0.0, 1.0, &mut img, &mut scratch);
980 assert_eq!(&img.rgba[..4], &[255, 255, 255, 255], "vertical after horizontal: TL is old BR");
981 }
982
983 #[test]
984 fn crop_zeroes_alpha_outside() {
985 let mut img = Frame::new(10, 10);
986 img.fill([200, 200, 200, 255]);
987 let mut scratch = Frame::default();
988 apply(&eff(K::Crop, &[0.2, 0.2, 0.2, 0.2, 0.0]), 0.0, 1.0, &mut img, &mut scratch);
989 let a = |img: &Frame, x: u32, y: u32| img.rgba[((y * 10 + x) * 4 + 3) as usize];
990 assert_eq!(a(&img, 0, 5), 0);
991 assert_eq!(a(&img, 9, 5), 0);
992 assert_eq!(a(&img, 5, 0), 0);
993 assert_eq!(a(&img, 5, 9), 0);
994 assert_eq!(a(&img, 5, 5), 255);
995 let mut img = Frame::new(10, 10);
997 img.fill([200, 200, 200, 255]);
998 apply(&eff(K::Crop, &[0.2, 0.2, 0.2, 0.2, 0.3]), 0.0, 1.0, &mut img, &mut scratch);
999 let edge = a(&img, 2, 5);
1000 assert!(edge > 0 && edge < 255, "feathered edge: {edge}");
1001 assert_eq!(a(&img, 0, 5), 0);
1002 }
1003
1004 #[test]
1005 fn vignette_darkens_corners_not_centre() {
1006 let mut img = Frame::new(16, 16);
1007 img.fill([200, 200, 200, 255]);
1008 let mut scratch = Frame::default();
1009 apply(&eff(K::Vignette, &[0.5, 0.3, 1.0]), 0.0, 1.0, &mut img, &mut scratch);
1010 let v = |x: u32, y: u32| img.rgba[((y * 16 + x) * 4) as usize];
1011 assert_eq!(v(8, 8), 200, "centre untouched");
1012 assert!(v(0, 0) < 100, "corner darkened: {}", v(0, 0));
1013 }
1014
1015 #[test]
1016 fn sharpen_raises_edge_contrast() {
1017 let mut img = Frame::new(16, 8);
1019 for y in 0..8u32 {
1020 for x in 0..16u32 {
1021 let c = if x < 8 { 50 } else { 200 };
1022 let i = ((y * 16 + x) * 4) as usize;
1023 img.rgba[i..i + 4].copy_from_slice(&[c, c, c, 255]);
1024 }
1025 }
1026 let mut scratch = Frame::default();
1027 apply(&eff(K::Sharpen, &[1.5, 2.0]), 0.0, 1.0, &mut img, &mut scratch);
1028 let v = |x: u32| img.rgba[((4 * 16 + x) * 4) as usize];
1029 assert!(v(7) < 50, "dark side overshoots darker: {}", v(7));
1030 assert!(v(8) > 200, "bright side overshoots brighter: {}", v(8));
1031 assert_eq!(v(0), 50, "flat areas untouched");
1032 }
1033
1034 #[test]
1035 fn wobble_deterministic_and_zero_at_zero_amplitude() {
1036 let e = eff(K::Wobble, &[20.0, 10.0, 2.0, 3.0, 3.0, 2.0, 7.0]);
1037 let a = wobble(&e, 0.4);
1038 let b = wobble(&e, 0.4);
1039 assert_eq!(a, b, "deterministic");
1040 assert!(a.0.abs() <= 20.0 && a.1.abs() <= 10.0 && a.2.abs() <= 2.0);
1041 let moved = (0..50).any(|i| wobble(&e, i as f64 * 0.1).0.abs() > 1.0);
1043 assert!(moved);
1044 let e2 = eff(K::Wobble, &[20.0, 10.0, 2.0, 3.0, 3.0, 2.0, 8.0]);
1046 assert_ne!(wobble(&e, 0.4), wobble(&e2, 0.4));
1047 let z = eff(K::Wobble, &[0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 7.0]);
1049 assert_eq!(wobble(&z, 1.23), (0.0, 0.0, 0.0, 0.0, 0.0));
1050 }
1051
1052 #[test]
1053 fn geometric_flag() {
1054 assert!(EffectKind::Wobble.is_geometric());
1055 assert!(EffectKind::Plane3d.is_geometric());
1056 assert!(!EffectKind::Blur.is_geometric());
1057 assert!(gpu_only(EffectKind::Vhs) && gpu_only(EffectKind::Shader));
1059 assert!(!gpu_only(EffectKind::Blur) && !gpu_only(EffectKind::Wobble));
1060 }
1061
1062 fn flat(w: u32, h: u32, rgba: [u8; 4]) -> Frame {
1064 let mut f = Frame::new(w, h);
1065 f.fill(rgba);
1066 f
1067 }
1068
1069 fn run(kind: K, params: &[f64], img: &mut Frame) {
1070 apply(&eff(kind, params), 0.0, 1.0, img, &mut Frame::default());
1071 }
1072
1073 #[test]
1074 fn threshold_splits_at_the_level() {
1075 let mut img = Frame::new(2, 1);
1077 img.rgba.copy_from_slice(&[100, 100, 100, 255, 155, 155, 155, 255]);
1078 run(K::Threshold, &[0.5, 0.0, 1.0], &mut img);
1079 assert_eq!(&img.rgba[..3], &[0, 0, 0]);
1080 assert_eq!(&img.rgba[4..7], &[255, 255, 255]);
1081 let mut img = flat(2, 1, [100, 100, 100, 255]);
1083 run(K::Threshold, &[0.3, 0.0, 1.0], &mut img);
1084 assert_eq!(&img.rgba[..3], &[255, 255, 255]);
1085 let mut img = flat(1, 1, [255, 0, 0, 255]);
1087 run(K::Threshold, &[0.1, 0.0, 0.0], &mut img);
1088 assert_eq!(&img.rgba[..3], &[255, 255, 255]);
1089 let mut img = flat(1, 1, [128, 128, 128, 255]);
1091 run(K::Threshold, &[0.5, 0.5, 1.0], &mut img);
1092 assert!((100..=160).contains(&img.rgba[0]), "{}", img.rgba[0]);
1093 }
1094
1095 #[test]
1096 fn hue_shift_turns_red_into_green() {
1097 let mut img = flat(1, 1, [255, 0, 0, 255]);
1098 run(K::HueShift, &[120.0, 1.0, 0.0], &mut img);
1099 assert_eq!(&img.rgba[..3], &[0, 255, 0]);
1100 run(K::HueShift, &[120.0, 1.0, 0.0], &mut img);
1102 assert_eq!(&img.rgba[..3], &[0, 0, 255]);
1103 let mut img = flat(1, 1, [255, 0, 0, 255]);
1105 run(K::HueShift, &[0.0, 0.0, 0.0], &mut img);
1106 assert_eq!(&img.rgba[..3], &[128, 128, 128]);
1107 run(K::HueShift, &[0.0, 1.0, 0.5], &mut img);
1108 assert!(img.rgba[0] > 200, "{}", img.rgba[0]);
1109 }
1110
1111 #[test]
1112 fn levels_clamp_and_remap() {
1113 let mut img = Frame::new(3, 1);
1114 img.rgba.copy_from_slice(&[0, 0, 0, 255, 100, 100, 100, 255, 255, 255, 255, 255]);
1115 run(K::Levels, &[0.5, 1.0, 1.0, 0.0, 1.0], &mut img);
1116 assert_eq!(img.rgba[0], 0, "below in-black clamps to 0");
1117 assert_eq!(img.rgba[4], 0, "0.39 is below in-black 0.5");
1118 assert_eq!(img.rgba[8], 255, "in-white stays white");
1119 let mut img = flat(2, 1, [0, 255, 0, 255]);
1121 run(K::Levels, &[0.0, 1.0, 1.0, 0.2, 0.8], &mut img);
1122 assert_eq!(img.rgba[0], 51);
1123 assert_eq!(img.rgba[1], 204);
1124 let mut img = flat(1, 1, [128, 128, 128, 255]);
1126 run(K::Levels, &[0.0, 1.0, 2.0, 0.0, 1.0], &mut img);
1127 assert!(img.rgba[0] > 150, "{}", img.rgba[0]);
1128 }
1129
1130 #[test]
1131 fn curves_are_identity_at_the_default_knots() {
1132 for x in [0.0, 0.25, 0.5, 0.75, 1.0, 0.1, 0.9] {
1133 assert!((curve_at(x, 0.25, 0.5, 0.75) - x).abs() < 1e-9, "x = {x}");
1134 }
1135 let base = noisy(8, 8);
1136 let mut img = base.clone();
1137 run(K::Curves, &[0.25, 0.5, 0.75, 0.25, 0.5, 0.75, 0.25, 0.5, 0.75, 0.25, 0.5, 0.75], &mut img);
1138 assert_eq!(img.rgba, base.rgba, "default knots must be a no-op");
1139 let mut img = flat(1, 1, [128, 128, 128, 255]);
1141 run(K::Curves, &[0.4, 0.7, 0.9, 0.25, 0.5, 0.75, 0.25, 0.5, 0.75, 0.25, 0.5, 0.75], &mut img);
1142 assert!(img.rgba[0] > 160, "{}", img.rgba[0]);
1143 let mut img = flat(1, 1, [200, 200, 200, 255]);
1145 run(K::Curves, &[0.25, 0.5, 0.75, 0.05, 0.1, 0.2, 0.25, 0.5, 0.75, 0.25, 0.5, 0.75], &mut img);
1146 assert!(img.rgba[0] < 100, "{}", img.rgba[0]);
1147 assert_eq!(img.rgba[1], 200);
1148 for i in 0..=100 {
1150 let v = curve_at(i as f64 / 100.0, 0.9, 0.1, 0.95);
1151 assert!((-1e-9..=1.0 + 1e-9).contains(&v), "out of range at {i}: {v}");
1152 }
1153 let mut prev = f64::MIN;
1155 for i in 0..=100 {
1156 let v = curve_at(i as f64 / 100.0, 0.1, 0.4, 0.8);
1157 assert!(v >= prev - 1e-9, "not monotone at {i}: {v} < {prev}");
1158 prev = v;
1159 }
1160 }
1161
1162 #[test]
1163 fn color_replace_swaps_only_matching_pixels() {
1164 let mut img = Frame::new(3, 1);
1165 img.rgba.copy_from_slice(&[255, 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]);
1167 run(K::ColorReplace, &[255.0, 0.0, 0.0, 0.0, 255.0, 0.0, 0.05, 0.02], &mut img);
1169 assert_eq!(&img.rgba[..4], &[0, 255, 0, 255], "the source colour is replaced");
1170 assert_eq!(&img.rgba[4..8], &[0, 0, 255, 255], "blue is untouched");
1171 assert_eq!(&img.rgba[8..12], &[255, 255, 255, 255], "white is untouched");
1172 let mut img = flat(1, 1, [230, 20, 20, 255]);
1174 run(K::ColorReplace, &[255.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.1], &mut img);
1175 assert!(img.rgba[0] < 60, "near-red should have been replaced: {:?}", &img.rgba[..3]);
1176 }
1177
1178 #[test]
1179 fn chroma_key_removes_the_key_colour_only() {
1180 let mut img = Frame::new(3, 1);
1181 img.rgba.copy_from_slice(&[0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255]);
1183 run(K::ChromaKey, &[0.0, 255.0, 0.0, 0.4, 0.1, 0.0, 0.5, 0.0], &mut img);
1184 assert_eq!(img.rgba[3], 0, "the key colour is gone");
1185 assert_eq!(img.rgba[7], 255, "red survives");
1186 assert_eq!(&img.rgba[4..7], &[255, 0, 0], "red is not despilled");
1187 assert_eq!(img.rgba[11], 255, "white survives");
1188 let mut img = Frame::new(2, 1);
1190 img.rgba.copy_from_slice(&[0, 255, 0, 255, 255, 0, 0, 255]);
1191 run(K::ChromaKey, &[0.0, 255.0, 0.0, 0.4, 0.1, 1.0, 0.5, 0.0], &mut img);
1192 assert_eq!(&img.rgba[..4], &[0, 0, 0, 255]);
1193 assert_eq!(&img.rgba[4..8], &[255, 255, 255, 255]);
1194 let mut img = flat(1, 1, [200, 190, 150, 255]);
1196 run(K::ChromaKey, &[0.0, 255.0, 0.0, 0.4, 0.1, 0.0, 1.0, 0.0], &mut img);
1197 assert_eq!(img.rgba[3], 255);
1198 }
1199
1200 #[test]
1201 fn blob_tracking_finds_a_red_square() {
1202 let mut f = flat(20, 20, [0, 0, 0, 255]);
1203 for y in 8..12u32 {
1204 for x in 8..12u32 {
1205 let i = ((y * 20 + x) * 4) as usize;
1206 f.rgba[i..i + 4].copy_from_slice(&[255, 0, 0, 255]);
1207 }
1208 }
1209 let p = K::BlobTrack.params().iter().map(|s| s.default).collect::<Vec<_>>();
1210 let (cx, cy, area) = track(&f, &p).expect("found");
1211 assert!((cx - 0.5).abs() < 1e-9 && (cy - 0.5).abs() < 1e-9, "{cx} {cy}");
1212 assert!((area - 16.0 / 400.0).abs() < 1e-9, "{area}");
1213 let mut f2 = flat(20, 20, [0, 0, 0, 255]);
1215 for y in 0..4u32 {
1216 for x in 0..4u32 {
1217 let i = ((y * 20 + x) * 4) as usize;
1218 f2.rgba[i..i + 4].copy_from_slice(&[255, 0, 0, 255]);
1219 }
1220 }
1221 let (cx2, cy2, _) = track(&f2, &p).expect("found");
1222 assert!(cx2 < 0.2 && cy2 < 0.2, "{cx2} {cy2}");
1223 assert!(track(&flat(4, 4, [0, 0, 255, 255]), &p).is_none());
1225 assert!(track(&Frame::default(), &p).is_none());
1226 assert!(track(&f, &[]).is_none());
1227 }
1228
1229 #[test]
1230 fn plane3d_drives_the_placement_on_the_cpu() {
1231 let e = eff(K::Plane3d, &[30.0, -10.0, 5.0, 2.0, 45.0, 0.0]);
1232 assert_eq!(wobble(&e, 0.0), (0.0, 0.0, 5.0, 30.0, -10.0));
1233 }
1234
1235 #[test]
1236 fn rec_dot_paints_a_dot_and_a_timecode() {
1237 let mut img = flat(96, 40, [0, 0, 0, 255]);
1238 apply(&eff(K::RecDot, &[12.0, 0.0, 0.0, 1.0, 4.0]), 0.0, 1.0, &mut img, &mut Frame::default());
1240 let at = |img: &Frame, x: u32, y: u32| {
1241 let i = ((y * 96 + x) * 4) as usize;
1242 [img.rgba[i], img.rgba[i + 1], img.rgba[i + 2]]
1243 };
1244 let dot = at(&img, 10, 10);
1245 assert!(dot[0] > 150 && dot[1] < 80, "dot at the top-left: {dot:?}");
1246 let ink = img.rgba.chunks_exact(4).filter(|p| p[0] > 200 && p[1] > 200 && p[2] > 200).count();
1247 assert!(ink > 40, "timecode digits drawn: {ink}");
1248 let mut img = flat(96, 40, [0, 0, 0, 255]);
1250 apply(&eff(K::RecDot, &[12.0, 1.0, 0.0, 0.0, 4.0]), 0.6, 1.0, &mut img, &mut Frame::default());
1251 assert_eq!(at(&img, 10, 10), [0, 0, 0], "dot is blinked off");
1252 let mut img = flat(96, 40, [0, 0, 0, 255]);
1254 apply(&eff(K::RecDot, &[12.0, 0.0, 3.0, 0.0, 4.0]), 0.0, 1.0, &mut img, &mut Frame::default());
1255 assert_eq!(at(&img, 10, 10), [0, 0, 0]);
1256 assert!(at(&img, 88, 30)[0] > 150, "dot at the bottom-right: {:?}", at(&img, 88, 30));
1257 }
1258
1259 #[test]
1260 fn wobble_motion_methods_and_smoothness() {
1261 use crate::model::{Effect, EffectKind};
1262 let mut e = Effect::new(EffectKind::Wobble);
1263 assert_eq!(e.params.len(), 9, "Motion + Smoothness knobs");
1264 for m in 0..5 {
1266 e.params[7] = crate::model::Animated::new(m as f64);
1267 for i in 0..40 {
1268 let t = i as f64 * 0.05;
1269 let (dx, dy, roll, ..) = wobble(&e, t);
1270 assert!(dx.abs() <= 20.0 + 1e-6 && dy.abs() <= 20.0 + 1e-6, "method {m} amplitude {dx} {dy}");
1271 assert!(roll.abs() <= 2.0 + 1e-6);
1272 assert_eq!(wobble(&e, t).0, dx, "method {m} is deterministic");
1273 }
1274 }
1275 let travel = |e: &Effect| -> f64 {
1277 (1..200)
1278 .map(|i| {
1279 let (a, _, _, _, _) = wobble(e, (i - 1) as f64 * 0.005);
1280 let (b, _, _, _, _) = wobble(e, i as f64 * 0.005);
1281 (b - a).abs()
1282 })
1283 .sum()
1284 };
1285 e.params[7] = crate::model::Animated::new(1.0);
1286 e.params[8] = crate::model::Animated::new(0.0);
1287 let fast = travel(&e);
1288 e.params[8] = crate::model::Animated::new(1.0);
1289 let slow = travel(&e);
1290 assert!(slow < fast * 0.4, "smoothness should calm the motion: {slow} vs {fast}");
1291 e.params[7] = crate::model::Animated::new(0.0);
1293 e.params[8] = crate::model::Animated::new(0.0);
1294 e.params[5] = crate::model::Animated::new(1.0);
1295 let signs: Vec<bool> = (0..200).map(|i| wobble(&e, i as f64 * 0.005).0 >= 0.0).collect();
1296 let flips = signs.windows(2).filter(|w| w[0] != w[1]).count();
1297 assert_eq!(flips, 2, "one sine period has two zero crossings");
1298 }
1299
1300 #[test]
1301 fn gpu_only_kinds_are_still_no_ops() {
1302 for k in [K::Vhs, K::MotionBlur, K::EdgeGlow, K::JpegCompress, K::BlobTrack, K::Shader] {
1303 let mut img = noisy(8, 8);
1304 let before = img.rgba.clone();
1305 let mut e = Effect::new(k);
1306 for p in e.params.iter_mut() {
1307 p.value = 1.0;
1308 }
1309 apply(&e, 0.0, 1.0, &mut img, &mut Frame::default());
1310 assert_eq!(img.rgba, before, "{k:?} must leave the CPU path alone");
1311 }
1312 }
1313}