1use crate::bloom::{Bloom, NEUTRAL_GAINS, NEUTRAL_RADII};
2use crate::chain::{Chain, Pass};
3use crate::color;
4use crate::comic::ModernComic;
5use crate::fx::{self, NoiseKind};
6use crate::grade::{Contrast, Dither, Grain, Saturation, Vignette, Warmth};
7use crate::preset::{self, Style};
8use crate::tone::Tone;
9use toml::{Table, Value};
10
11pub const KEYS: &[&str] = &[
12 "aberration",
13 "angle",
14 "black",
15 "black_threshold",
16 "bloom",
17 "cavity",
18 "cavity_distance",
19 "cel_bands",
20 "cel_shadow",
21 "cel_softness",
22 "cel_spec",
23 "cel_spec_roughness",
24 "cel_spec_threshold",
25 "cel_threshold",
26 "contrast",
27 "crease_angle",
28 "crush",
29 "curvature",
30 "darkening",
31 "dither",
32 "dither_kind",
33 "distortion",
34 "dot",
35 "dust",
36 "encode",
37 "exposure",
38 "flicker",
39 "focus",
40 "frame",
41 "gain",
42 "grain",
43 "halation",
44 "highlight",
45 "highlight_threshold",
46 "ink",
47 "interior_line_strength",
48 "keep",
49 "keep_range",
50 "levels",
51 "line_weight",
52 "lut",
53 "mask",
54 "outline",
55 "outline_alpha",
56 "outline_color",
57 "outline_thickness",
58 "palette",
59 "paper_grain",
60 "pixel",
61 "posterize",
62 "radius",
63 "rim",
64 "rim_color",
65 "rim_width",
66 "saturation",
67 "saturation_lift",
68 "scale",
69 "scanlines",
70 "scratches",
71 "seed",
72 "sepia",
73 "shadow",
74 "style",
75 "tape",
76 "threshold",
77 "tilt_range",
78 "tone",
79 "tone_steps",
80 "vignette",
81 "warmth",
82 "weave",
83 "weights",
84];
85
86const BLOOM_KEYS: &[&str] = &[
87 "clamp",
88 "down",
89 "gains",
90 "kind",
91 "knee",
92 "knee_width",
93 "linear_taps",
94 "radii",
95 "radius",
96 "sigma",
97 "soft",
98 "strength",
99 "threshold",
100 "unit",
101];
102
103const VIGNETTE_KEYS: &[&str] = &[
104 "aspect", "inner", "kind", "outer", "power", "scale", "strength",
105];
106
107const GRAIN_KEYS: &[&str] = &["clamp", "response", "strength"];
108
109const LUT_KEYS: &[&str] = &["size", "values"];
110
111const OUTLINE_KEYS: &[&str] = &[
112 "alpha",
113 "color",
114 "crease_angle",
115 "enabled",
116 "local_color",
117 "thickness",
118];
119
120const SCRATCH_KEYS: &[&str] = &["count", "strength"];
121
122const KINDS: &[&str] = &["box", "rings", "tent"];
123
124const VIGNETTE_KINDS: &[&str] = &["power", "smoothstep"];
125
126const WARMTH_KEYS: &[&str] = &["amount", "high", "highlight", "kind", "low", "shadow"];
127
128const TONE_KEYS: &[&str] = &["clamp", "kind", "start"];
129
130const TONES: &[&str] = &["aces", "agx", "neutral"];
131
132const DITHER_KINDS: &[&str] = &["blue", "ordered"];
133
134#[derive(Clone, Debug)]
135pub struct ParseError {
136 message: String,
137}
138
139impl ParseError {
140 fn msg(message: impl Into<String>) -> Self {
141 Self {
142 message: message.into(),
143 }
144 }
145}
146
147impl std::fmt::Display for ParseError {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.write_str(&self.message)
150 }
151}
152
153impl std::error::Error for ParseError {}
154
155pub fn parse_str(text: &str) -> Result<Chain, ParseError> {
156 let table: Table = text
157 .parse()
158 .map_err(|err| ParseError::msg(format!("toml: {err}")))?;
159 parse(&table)
160}
161
162pub fn parse(table: &Table) -> Result<Chain, ParseError> {
163 if let Some(bad) = table.keys().find(|key| !KEYS.contains(&key.as_str())) {
164 return Err(ParseError::msg(format!(
165 "unknown key {bad}; allowed: {}",
166 KEYS.join(", ")
167 )));
168 }
169 let mut chain = match table.get("style") {
170 None => Chain::new(),
171 Some(value) => {
172 let name = value
173 .as_str()
174 .ok_or_else(|| ParseError::msg("style is a string"))?;
175 Style::parse(name)
176 .ok_or_else(|| {
177 ParseError::msg(format!(
178 "unknown style {name}; allowed: {}",
179 preset::names().join(", ")
180 ))
181 })?
182 .chain()
183 }
184 };
185 if let Some(value) = table.get("seed") {
186 chain.seed = integer(value, "seed")?;
187 }
188 if let Some(value) = table.get("frame") {
189 chain.frame = integer(value, "frame")?;
190 }
191 for (key, value) in table {
192 match key.as_str() {
193 "style" | "seed" | "frame" => {}
194 "exposure" => set_exposure(&mut chain, number(value, key)?),
195 "black" => set_black(&mut chain, number(value, key)?),
196 "black_threshold" => {
197 let n = number(value, key)?;
198 set_modern_comic(&mut chain, |p| p.black_threshold = n);
199 }
200 "saturation" => set_saturation(&mut chain, number(value, key)?),
201 "saturation_lift" => {
202 let n = number(value, key)?;
203 set_modern_comic(&mut chain, |p| p.saturation_lift = n);
204 }
205 "contrast" => set_contrast(&mut chain, number(value, key)?),
206 "warmth" => set_warmth(&mut chain, value)?,
207 "gain" => set_gain(&mut chain, number(value, key)?),
208 "tone" => set_tone(&mut chain, value)?,
209 "tone_steps" => {
210 let n = number_u(value, key)?;
211 if !(3..=5).contains(&n) {
212 return Err(ParseError::msg("tone_steps is 3 to 5"));
213 }
214 set_modern_comic(&mut chain, |p| p.tone_steps = n);
215 }
216 "highlight_threshold" => {
217 let n = number(value, key)?;
218 set_modern_comic(&mut chain, |p| p.highlight_threshold = n);
219 }
220 "line_weight" => {
221 let n = number(value, key)?;
222 set_modern_comic(&mut chain, |p| p.line_weight = n);
223 }
224 "interior_line_strength" => {
225 let n = number(value, key)?;
226 set_modern_comic(&mut chain, |p| p.interior_line_strength = n);
227 }
228 "threshold" => set_threshold(&mut chain, number(value, key)?),
229 "bloom" => set_bloom(&mut chain, value)?,
230 "vignette" => set_vignette(&mut chain, value)?,
231 "grain" => set_grain(&mut chain, value)?,
232 "dither" => set_dither(&mut chain, value)?,
233 "dither_kind" => set_dither_kind(&mut chain, value)?,
234 "encode" => {
235 if boolean(value, key)? {
236 push_missing(
237 &mut chain,
238 |pass| matches!(pass, Pass::Encode),
239 Pass::Encode,
240 );
241 }
242 }
243 "lut" => set_lut(&mut chain, value)?,
244 "weights" => set_weights(&mut chain, value)?,
245 "cel_bands" => {
246 let n = number_u(value, key)?;
247 set_cel(&mut chain, |cel| cel.bands = n);
248 }
249 "cel_shadow" => {
250 let n = number(value, key)?;
251 set_cel(&mut chain, |cel| cel.shadow = n);
252 }
253 "cel_threshold" => {
254 let n = number(value, key)?;
255 set_cel(&mut chain, |cel| cel.threshold = n);
256 }
257 "cel_softness" => {
258 let n = number(value, key)?;
259 set_cel(&mut chain, |cel| cel.softness = n);
260 }
261 "cel_spec" => {
262 let n = number(value, key)?;
263 set_cel(&mut chain, |cel| cel.spec = n);
264 }
265 "cel_spec_roughness" => {
266 let n = number(value, key)?;
267 set_cel(&mut chain, |cel| cel.spec_roughness = n);
268 }
269 "cel_spec_threshold" => {
270 let n = number(value, key)?;
271 set_cel(&mut chain, |cel| cel.spec_threshold = n);
272 }
273 "cavity" => {
274 let n = number(value, key)?;
275 set_cavity(&mut chain, |c| c.strength = n);
276 }
277 "cavity_distance" => {
278 let n = number(value, key)?;
279 set_cavity(&mut chain, |c| c.distance = n);
280 }
281 "rim" => {
282 let n = number(value, key)?;
283 set_rim(&mut chain, |r| r.strength = n);
284 }
285 "rim_width" => {
286 let n = number(value, key)?;
287 set_rim(&mut chain, |r| r.width = n);
288 }
289 "rim_color" => {
290 let c = color(value, key)?;
291 set_rim(&mut chain, |r| r.color = c);
292 }
293 "outline" => set_outline(&mut chain, value)?,
294 "outline_thickness" => {
295 let n = number(value, key)?;
296 set_outline_field(&mut chain, |o| o.thickness = n);
297 }
298 "outline_alpha" => {
299 let n = number(value, key)?;
300 set_outline_field(&mut chain, |o| o.alpha = n);
301 }
302 "outline_color" => {
303 let c = color(value, key)?;
304 set_outline_field(&mut chain, |o| o.color = c);
305 }
306 "crease_angle" => {
307 let n = number(value, key)?;
308 set_outline_field(&mut chain, |o| o.crease_angle = n);
309 }
310 "keep" => {
311 let c = color(value, key)?;
312 set_noir(&mut chain, |n| n.keep = Some(c));
313 }
314 "keep_range" => {
315 let n = number(value, key)?;
316 set_noir(&mut chain, |noir| noir.keep_range = n);
317 }
318 "crush" => {
319 let n = number(value, key)?;
320 set_noir(&mut chain, |noir| noir.crush = n);
321 }
322 "sepia" => set_sepia(&mut chain, number(value, key)?),
323 "posterize" | "levels" => set_levels(&mut chain, number(value, key)?),
324 "flicker" => set_f32(
325 &mut chain,
326 number(value, key)?,
327 |p| matches!(p, Pass::Flicker(_)),
328 Pass::Flicker,
329 ),
330 "weave" => set_f32(
331 &mut chain,
332 number(value, key)?,
333 |p| matches!(p, Pass::Weave(_)),
334 Pass::Weave,
335 ),
336 "dust" => set_f32(
337 &mut chain,
338 number(value, key)?,
339 |p| matches!(p, Pass::Dust(_)),
340 Pass::Dust,
341 ),
342 "scratches" => set_scratches(&mut chain, value)?,
343 "halation" => set_halation(&mut chain, number(value, key)?),
344 "aberration" => set_f32(
345 &mut chain,
346 number(value, key)?,
347 |p| matches!(p, Pass::Aberration(_)),
348 Pass::Aberration,
349 ),
350 "distortion" => set_f32(
351 &mut chain,
352 number(value, key)?,
353 |p| matches!(p, Pass::Distortion(_)),
354 Pass::Distortion,
355 ),
356 "curvature" => set_f32(
357 &mut chain,
358 number(value, key)?,
359 |p| matches!(p, Pass::Curvature(_)),
360 Pass::Curvature,
361 ),
362 "scanlines" => set_scanlines(&mut chain, number(value, key)?),
363 "mask" => set_f32(
364 &mut chain,
365 number(value, key)?,
366 |p| matches!(p, Pass::Aperture(_)),
367 Pass::Aperture,
368 ),
369 "dot" => {
370 let n = number(value, key)?;
371 set_halftone(&mut chain, |h| h.cell = n);
372 }
373 "angle" => {
374 let n = number(value, key)?;
375 set_halftone(&mut chain, |h| h.angle = n);
376 }
377 "ink" => set_ink(&mut chain, value)?,
378 "darkening" => {
379 let n = number(value, key)?;
380 set_water(&mut chain, |w| w.darkening = n);
381 }
382 "paper_grain" => set_paper_grain(&mut chain, number(value, key)?),
383 "scale" => set_scale(&mut chain, number(value, key)?),
384 "pixel" => set_pixel_size(&mut chain, integer(value, key)?),
385 "palette" => set_palette(&mut chain, boolean(value, key)?),
386 "shadow" => {
387 let c = color(value, key)?;
388 set_duotone(&mut chain, |d| d.shadow = c);
389 }
390 "highlight" => {
391 let c = color(value, key)?;
392 set_duotone(&mut chain, |d| d.highlight = c);
393 }
394 "radius" => set_radius(&mut chain, number(value, key)?),
395 "tape" => {
396 let tape = crate::tape::read(value, chain.tape()).map_err(ParseError::msg)?;
397 chain.set_tape(tape);
398 }
399 "focus" => {
400 let n = number(value, key)?;
401 set_tilt(&mut chain, |t| t.focus = n);
402 }
403 "tilt_range" => {
404 let n = number(value, key)?;
405 set_tilt(&mut chain, |t| t.range = n);
406 }
407 other => {
408 return Err(ParseError::msg(format!(
409 "unknown key {other}; allowed: {}",
410 KEYS.join(", ")
411 )));
412 }
413 }
414 }
415 Ok(chain)
416}
417
418fn number_u(value: &Value, key: &str) -> Result<u32, ParseError> {
419 let n = number(value, key)?;
420 if n < 0.0 {
421 return Err(ParseError::msg(format!("{key} is a positive number")));
422 }
423 Ok(n.round() as u32)
424}
425
426fn number(value: &Value, key: &str) -> Result<f32, ParseError> {
427 let n = if let Some(n) = value.as_float() {
428 n
429 } else if let Some(n) = value.as_integer() {
430 n as f64
431 } else {
432 return Err(ParseError::msg(format!("{key} is a number")));
433 };
434 if !n.is_finite() {
435 return Err(ParseError::msg(format!("{key} is a number")));
436 }
437 Ok(n as f32)
438}
439
440fn integer(value: &Value, key: &str) -> Result<u32, ParseError> {
441 let Some(n) = value.as_integer() else {
442 return Err(ParseError::msg(format!("{key} is an integer")));
443 };
444 u32::try_from(n).map_err(|_| ParseError::msg(format!("{key} is an integer")))
445}
446
447fn boolean(value: &Value, key: &str) -> Result<bool, ParseError> {
448 value
449 .as_bool()
450 .ok_or_else(|| ParseError::msg(format!("{key} is a bool")))
451}
452
453fn color(value: &Value, key: &str) -> Result<[f32; 3], ParseError> {
454 if let Some(text) = value.as_str() {
455 return color::parse_hex(text)
456 .ok_or_else(|| ParseError::msg(format!("{key} is #rrggbb or three numbers")));
457 }
458 let Some(items) = value.as_array() else {
459 return Err(ParseError::msg(format!(
460 "{key} is #rrggbb or three numbers"
461 )));
462 };
463 if items.len() != 3 {
464 return Err(ParseError::msg(format!(
465 "{key} is #rrggbb or three numbers"
466 )));
467 }
468 Ok([
469 number(&items[0], key)?,
470 number(&items[1], key)?,
471 number(&items[2], key)?,
472 ])
473}
474
475fn floats(value: &Value, key: &str, len: usize) -> Result<Vec<f32>, ParseError> {
476 let Some(items) = value.as_array() else {
477 return Err(ParseError::msg(format!("{key} is {len} numbers")));
478 };
479 if items.len() != len {
480 return Err(ParseError::msg(format!("{key} is {len} numbers")));
481 }
482 items.iter().map(|item| number(item, key)).collect()
483}
484
485fn unknown(prefix: &str, key: &str, allowed: &[&str]) -> ParseError {
486 ParseError::msg(format!(
487 "unknown key {prefix}.{key}; allowed: {}",
488 allowed.join(", ")
489 ))
490}
491
492fn push_missing(chain: &mut Chain, found: impl Fn(&Pass) -> bool, pass: Pass) {
493 if !chain.passes.iter().any(found) {
494 chain.passes.push(pass);
495 }
496}
497
498fn set_exposure(chain: &mut Chain, value: f32) {
499 for pass in &mut chain.passes {
500 if let Pass::Exposure(slot) = pass {
501 *slot = value;
502 return;
503 }
504 }
505 chain.passes.insert(0, Pass::Exposure(value));
506}
507
508fn set_black(chain: &mut Chain, value: f32) {
509 for pass in &mut chain.passes {
510 if let Pass::Black(slot) = pass {
511 *slot = value;
512 return;
513 }
514 }
515 chain.passes.push(Pass::Black(value));
516}
517
518fn set_saturation(chain: &mut Chain, value: f32) {
519 for pass in &mut chain.passes {
520 if let Pass::Saturation(slot) = pass {
521 slot.amount = value;
522 return;
523 }
524 }
525 chain
526 .passes
527 .push(Pass::Saturation(Saturation::rec709(value)));
528}
529
530fn set_contrast(chain: &mut Chain, value: f32) {
531 for pass in &mut chain.passes {
532 match pass {
533 Pass::Contrast(Contrast::Power { power, .. }) => {
534 *power = value;
535 return;
536 }
537 Pass::Contrast(Contrast::Pivot { amount, .. }) => {
538 *amount = value;
539 return;
540 }
541 Pass::Noir(noir) => {
542 noir.contrast = value;
543 return;
544 }
545 _ => {}
546 }
547 }
548 chain.passes.push(Pass::Contrast(Contrast::Pivot {
549 amount: value,
550 pivot: 0.5,
551 }));
552}
553
554fn set_warmth(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
555 if let Some(table) = value.as_table() {
556 if let Some(bad) = table
557 .keys()
558 .find(|key| !WARMTH_KEYS.contains(&key.as_str()))
559 {
560 return Err(unknown("warmth", bad, WARMTH_KEYS));
561 }
562 if let Some(kind) = table.get("kind") {
563 let name = kind
564 .as_str()
565 .ok_or_else(|| ParseError::msg("warmth.kind is a string"))?;
566 let amount = warmth_amount(chain);
567 let next = match name {
568 "linear" => Warmth::linear(amount),
569 "luma_curve" => Warmth::luma_curve(amount, [1.0; 2], [1.0; 2], 0.0, 1.0),
570 other => {
571 return Err(ParseError::msg(format!(
572 "unknown warmth.kind {other}; allowed: linear, luma_curve"
573 )));
574 }
575 };
576 replace_warmth(chain, next);
577 }
578 if let Some(v) = table.get("amount") {
579 warmth_mut(chain).amount = number(v, "warmth.amount")?;
580 }
581 if let Some(v) = table.get("low") {
582 let items = floats(v, "warmth.low", 2)?;
583 warmth_mut(chain).low = [items[0], items[1]];
584 }
585 if let Some(v) = table.get("high") {
586 let items = floats(v, "warmth.high", 2)?;
587 warmth_mut(chain).high = [items[0], items[1]];
588 }
589 if let Some(v) = table.get("shadow") {
590 warmth_mut(chain).shadow = number(v, "warmth.shadow")?;
591 }
592 if let Some(v) = table.get("highlight") {
593 warmth_mut(chain).highlight = number(v, "warmth.highlight")?;
594 }
595 if !chain
596 .passes
597 .iter()
598 .any(|pass| matches!(pass, Pass::Warmth(_)))
599 {
600 chain.passes.push(Pass::Warmth(Warmth::linear(0.0)));
601 }
602 return Ok(());
603 }
604 let n = number(value, "warmth")?;
605 if chain
606 .passes
607 .iter()
608 .any(|pass| matches!(pass, Pass::Warmth(_)))
609 {
610 warmth_mut(chain).amount = n;
611 } else {
612 chain.passes.push(Pass::Warmth(Warmth::linear(n)));
613 }
614 Ok(())
615}
616
617fn warmth_amount(chain: &Chain) -> f32 {
618 chain
619 .passes
620 .iter()
621 .find_map(|pass| match pass {
622 Pass::Warmth(warmth) => Some(warmth.amount),
623 _ => None,
624 })
625 .unwrap_or(0.0)
626}
627
628fn replace_warmth(chain: &mut Chain, warmth: Warmth) {
629 for pass in &mut chain.passes {
630 if let Pass::Warmth(slot) = pass {
631 *slot = warmth;
632 return;
633 }
634 }
635 chain.passes.push(Pass::Warmth(warmth));
636}
637
638fn warmth_mut(chain: &mut Chain) -> &mut Warmth {
639 if !chain
640 .passes
641 .iter()
642 .any(|pass| matches!(pass, Pass::Warmth(_)))
643 {
644 chain.passes.push(Pass::Warmth(Warmth::linear(0.0)));
645 }
646 let Some(Pass::Warmth(slot)) = chain
647 .passes
648 .iter_mut()
649 .find(|pass| matches!(pass, Pass::Warmth(_)))
650 else {
651 unreachable!("warmth was inserted");
652 };
653 slot
654}
655
656fn set_gain(chain: &mut Chain, value: f32) {
657 for pass in &mut chain.passes {
658 if let Pass::Tone(Tone::Aces { gain }) = pass {
659 *gain = value;
660 return;
661 }
662 }
663 chain.passes.push(Pass::Tone(Tone::Aces { gain: value }));
664}
665
666fn set_tone(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
667 let tone = if let Some(name) = value.as_str() {
668 match name {
669 "aces" => Tone::aces(),
670 "agx" => Tone::Agx,
671 "neutral" => existing_neutral(chain).unwrap_or_else(|| Tone::neutral(1.0, false)),
672 other => {
673 return Err(ParseError::msg(format!(
674 "unknown tone {other}; allowed: {}",
675 TONES.join(", ")
676 )));
677 }
678 }
679 } else if let Some(table) = value.as_table() {
680 tone_from_table(table)?
681 } else {
682 return Err(ParseError::msg("tone is aces, agx, neutral, or a table"));
683 };
684 for pass in &mut chain.passes {
685 if let Pass::Tone(slot) = pass {
686 if matches!(
687 (&*slot, &tone),
688 (Tone::Neutral { .. }, Tone::Neutral { .. })
689 ) {
690 return Ok(());
691 }
692 *slot = tone;
693 return Ok(());
694 }
695 }
696 chain.passes.push(Pass::Tone(tone));
697 Ok(())
698}
699
700fn tone_from_table(table: &Table) -> Result<Tone, ParseError> {
701 if let Some(bad) = table.keys().find(|key| !TONE_KEYS.contains(&key.as_str())) {
702 return Err(unknown("tone", bad, TONE_KEYS));
703 }
704 let kind = table
705 .get("kind")
706 .and_then(Value::as_str)
707 .ok_or_else(|| ParseError::msg("tone.kind is a string"))?;
708 match kind {
709 "aces" | "agx" => {
710 if table.contains_key("start") || table.contains_key("clamp") {
711 return Err(ParseError::msg("tone.start and tone.clamp are for neutral"));
712 }
713 Ok(if kind == "aces" {
714 Tone::aces()
715 } else {
716 Tone::Agx
717 })
718 }
719 "neutral" => {
720 let start = match table.get("start") {
721 Some(value) => number(value, "tone.start")?,
722 None => 1.0,
723 };
724 let clamp = match table.get("clamp") {
725 Some(value) => boolean(value, "tone.clamp")?,
726 None => false,
727 };
728 Ok(Tone::neutral(start, clamp))
729 }
730 other => Err(ParseError::msg(format!(
731 "unknown tone.kind {other}; allowed: {}",
732 TONES.join(", ")
733 ))),
734 }
735}
736
737fn existing_neutral(chain: &Chain) -> Option<Tone> {
738 chain.passes.iter().find_map(|pass| match pass {
739 Pass::Tone(tone @ Tone::Neutral { .. }) => Some(*tone),
740 _ => None,
741 })
742}
743
744fn ensure_bloom(chain: &mut Chain) -> &mut Bloom {
745 if !chain
746 .passes
747 .iter()
748 .any(|pass| matches!(pass, Pass::Bloom(_)))
749 {
750 let index = chain
751 .passes
752 .iter()
753 .position(|pass| matches!(pass, Pass::Exposure(_)))
754 .map(|i| i + 1)
755 .unwrap_or(0);
756 chain.passes.insert(index, Pass::Bloom(Bloom::neutral(0.0)));
757 }
758 let Some(Pass::Bloom(bloom)) = chain
759 .passes
760 .iter_mut()
761 .find(|pass| matches!(pass, Pass::Bloom(_)))
762 else {
763 unreachable!("bloom was inserted");
764 };
765 bloom
766}
767
768fn set_threshold(chain: &mut Chain, value: f32) {
769 ensure_bloom(chain).threshold = value;
770}
771
772fn set_bloom(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
773 if let Some(table) = value.as_table() {
774 if let Some(bad) = table.keys().find(|key| !BLOOM_KEYS.contains(&key.as_str())) {
775 return Err(unknown("bloom", bad, BLOOM_KEYS));
776 }
777 let bloom = ensure_bloom(chain);
778 if let Some(kind) = table.get("kind") {
779 let name = kind
780 .as_str()
781 .ok_or_else(|| ParseError::msg("bloom.kind is a string"))?;
782 *bloom = match name {
783 "box" => Bloom::neutral(bloom.strength),
784 "tent" => Bloom::tent(bloom.strength, 1.0, 4, 16.0, 2),
785 "rings" => Bloom::rings(bloom.strength, 1.0, 4, NEUTRAL_RADII, NEUTRAL_GAINS),
786 other => {
787 return Err(ParseError::msg(format!(
788 "unknown bloom.kind {other}; allowed: {}",
789 KINDS.join(", ")
790 )));
791 }
792 };
793 }
794 let bloom = ensure_bloom(chain);
795 if let Some(v) = table.get("strength") {
796 bloom.strength = number(v, "bloom.strength")?;
797 }
798 if let Some(v) = table.get("threshold") {
799 bloom.threshold = number(v, "bloom.threshold")?;
800 }
801 if let Some(v) = table.get("knee") {
802 bloom.knee = number(v, "bloom.knee")?;
803 }
804 if let Some(v) = table.get("knee_width") {
805 bloom.knee_width = number(v, "bloom.knee_width")?;
806 }
807 if let Some(v) = table.get("soft") {
808 bloom.soft = number(v, "bloom.soft")?;
809 }
810 if let Some(v) = table.get("radius") {
811 bloom.radius = number_u(v, "bloom.radius")?;
812 }
813 if let Some(v) = table.get("sigma") {
814 bloom.spread = number(v, "bloom.sigma")?;
815 }
816 if let Some(v) = table.get("down") {
817 bloom.down = number_u(v, "bloom.down")?.max(1);
818 }
819 if let Some(v) = table.get("clamp") {
820 bloom.clamp = number(v, "bloom.clamp")?;
821 }
822 if let Some(v) = table.get("unit") {
823 bloom.unit = number(v, "bloom.unit")?;
824 }
825 if let Some(v) = table.get("linear_taps") {
826 bloom.linear_taps = boolean(v, "bloom.linear_taps")?;
827 }
828 if let Some(v) = table.get("radii") {
829 let items = floats(v, "bloom.radii", 4)?;
830 bloom.radii = [items[0], items[1], items[2], items[3]];
831 }
832 if let Some(v) = table.get("gains") {
833 let items = floats(v, "bloom.gains", 4)?;
834 bloom.gains = [items[0], items[1], items[2], items[3]];
835 }
836 return Ok(());
837 }
838 ensure_bloom(chain).strength = number(value, "bloom")?;
839 Ok(())
840}
841
842fn set_vignette(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
843 if let Some(table) = value.as_table() {
844 if let Some(bad) = table
845 .keys()
846 .find(|key| !VIGNETTE_KEYS.contains(&key.as_str()))
847 {
848 return Err(unknown("vignette", bad, VIGNETTE_KEYS));
849 }
850 ensure_vignette(chain);
851 if let Some(kind) = table.get("kind") {
852 let name = kind
853 .as_str()
854 .ok_or_else(|| ParseError::msg("vignette.kind is a string"))?;
855 let strength = vignette_strength(chain);
856 let next = match name {
857 "power" => Vignette::power(strength, 2.0, 1.0, 1.0),
858 "smoothstep" => Vignette::smoothstep(strength, 1.0, 0.0, 1.0),
859 other => {
860 return Err(ParseError::msg(format!(
861 "unknown vignette.kind {other}; allowed: {}",
862 VIGNETTE_KINDS.join(", ")
863 )));
864 }
865 };
866 replace_vignette(chain, next);
867 }
868 if let Some(v) = table.get("strength") {
869 let n = number(v, "vignette.strength")?;
870 vignette_mut(chain).strength = n;
871 }
872 if let Some(v) = table.get("power") {
873 let n = number(v, "vignette.power")?;
874 vignette_mut(chain).power = n;
875 }
876 if let Some(v) = table.get("aspect") {
877 let n = number(v, "vignette.aspect")?;
878 vignette_mut(chain).aspect = n;
879 }
880 if let Some(v) = table.get("scale") {
881 let n = number(v, "vignette.scale")?;
882 vignette_mut(chain).scale = n;
883 }
884 if let Some(v) = table.get("inner") {
885 let n = number(v, "vignette.inner")?;
886 vignette_mut(chain).inner = n;
887 }
888 if let Some(v) = table.get("outer") {
889 let n = number(v, "vignette.outer")?;
890 vignette_mut(chain).outer = n;
891 }
892 return Ok(());
893 }
894 let n = number(value, "vignette")?;
895 if chain
896 .passes
897 .iter()
898 .any(|pass| matches!(pass, Pass::Vignette(_)))
899 {
900 vignette_mut(chain).strength = n;
901 } else {
902 chain
903 .passes
904 .push(Pass::Vignette(Vignette::power(n, 2.0, 1.0, 1.0)));
905 }
906 Ok(())
907}
908
909fn ensure_vignette(chain: &mut Chain) {
910 if !chain
911 .passes
912 .iter()
913 .any(|pass| matches!(pass, Pass::Vignette(_)))
914 {
915 chain
916 .passes
917 .push(Pass::Vignette(Vignette::power(0.0, 2.0, 1.0, 1.0)));
918 }
919}
920
921fn vignette_strength(chain: &Chain) -> f32 {
922 chain
923 .passes
924 .iter()
925 .find_map(|pass| match pass {
926 Pass::Vignette(v) => Some(v.strength),
927 _ => None,
928 })
929 .unwrap_or(0.0)
930}
931
932fn replace_vignette(chain: &mut Chain, vignette: Vignette) {
933 for pass in &mut chain.passes {
934 if let Pass::Vignette(slot) = pass {
935 *slot = vignette;
936 return;
937 }
938 }
939}
940
941fn vignette_mut(chain: &mut Chain) -> &mut Vignette {
942 ensure_vignette(chain);
943 let Some(Pass::Vignette(slot)) = chain
944 .passes
945 .iter_mut()
946 .find(|pass| matches!(pass, Pass::Vignette(_)))
947 else {
948 unreachable!("vignette was inserted");
949 };
950 slot
951}
952
953fn set_grain(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
954 if let Some(table) = value.as_table() {
955 if let Some(bad) = table.keys().find(|key| !GRAIN_KEYS.contains(&key.as_str())) {
956 return Err(unknown("grain", bad, GRAIN_KEYS));
957 }
958 ensure_grain(chain);
959 if let Some(v) = table.get("strength") {
960 let n = number(v, "grain.strength")?;
961 grain_mut(chain).strength = n;
962 }
963 if let Some(v) = table.get("response") {
964 let n = number(v, "grain.response")?;
965 grain_mut(chain).response = n;
966 }
967 if let Some(v) = table.get("clamp") {
968 let n = boolean(v, "grain.clamp")?;
969 grain_mut(chain).clamp = n;
970 }
971 return Ok(());
972 }
973 let n = number(value, "grain")?;
974 if chain
975 .passes
976 .iter()
977 .any(|pass| matches!(pass, Pass::Grain(_)))
978 {
979 grain_mut(chain).strength = n;
980 } else {
981 chain.passes.push(Pass::Grain(Grain {
982 strength: n,
983 response: 0.0,
984 clamp: false,
985 }));
986 }
987 Ok(())
988}
989
990fn ensure_grain(chain: &mut Chain) {
991 if !chain
992 .passes
993 .iter()
994 .any(|pass| matches!(pass, Pass::Grain(_)))
995 {
996 chain.passes.push(Pass::Grain(Grain {
997 strength: 0.0,
998 response: 0.0,
999 clamp: false,
1000 }));
1001 }
1002}
1003
1004fn grain_mut(chain: &mut Chain) -> &mut Grain {
1005 ensure_grain(chain);
1006 let Some(Pass::Grain(slot)) = chain
1007 .passes
1008 .iter_mut()
1009 .find(|pass| matches!(pass, Pass::Grain(_)))
1010 else {
1011 unreachable!("grain was inserted");
1012 };
1013 slot
1014}
1015
1016fn set_dither(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1017 let amplitude = if let Some(on) = value.as_bool() {
1018 if on { 1.0 } else { 0.0 }
1019 } else {
1020 number(value, "dither")?
1021 };
1022 for pass in &mut chain.passes {
1023 if let Pass::Dither(slot) = pass {
1024 slot.amplitude = amplitude;
1025 return Ok(());
1026 }
1027 }
1028 chain.passes.push(Pass::Dither(Dither { amplitude }));
1029 Ok(())
1030}
1031
1032fn set_dither_kind(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1033 let name = value
1034 .as_str()
1035 .ok_or_else(|| ParseError::msg("dither_kind is a string"))?;
1036 let kind = match name {
1037 "ordered" => NoiseKind::Ordered,
1038 "blue" => NoiseKind::Blue,
1039 other => {
1040 return Err(ParseError::msg(format!(
1041 "unknown dither_kind {other}; allowed: {}",
1042 DITHER_KINDS.join(", ")
1043 )));
1044 }
1045 };
1046 for pass in &mut chain.passes {
1047 if let Pass::OneBit(slot) = pass {
1048 slot.kind = kind;
1049 return Ok(());
1050 }
1051 }
1052 chain.passes.push(Pass::OneBit(fx::OneBit { kind }));
1053 Ok(())
1054}
1055
1056fn set_lut(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1057 let Some(table) = value.as_table() else {
1058 return Err(ParseError::msg("lut is a table"));
1059 };
1060 if let Some(bad) = table.keys().find(|key| !LUT_KEYS.contains(&key.as_str())) {
1061 return Err(unknown("lut", bad, LUT_KEYS));
1062 }
1063 let size = table
1064 .get("size")
1065 .ok_or_else(|| ParseError::msg("lut.size is an integer"))
1066 .and_then(|v| integer(v, "lut.size"))?;
1067 if !(2..=32).contains(&size) {
1068 return Err(ParseError::msg("lut.size is from 2 to 32"));
1069 }
1070 let values = table
1071 .get("values")
1072 .ok_or_else(|| ParseError::msg("lut.values is a list"))?;
1073 let Some(items) = values.as_array() else {
1074 return Err(ParseError::msg("lut.values is a list"));
1075 };
1076 let need = (size as usize) * (size as usize) * (size as usize) * 3;
1077 if items.len() != need {
1078 return Err(ParseError::msg(format!("lut.values has {need} numbers")));
1079 }
1080 let mut rgb = Vec::with_capacity(need / 3);
1081 for chunk in items.chunks(3) {
1082 rgb.push([
1083 number(&chunk[0], "lut.values")?,
1084 number(&chunk[1], "lut.values")?,
1085 number(&chunk[2], "lut.values")?,
1086 ]);
1087 }
1088 let lut = crate::grade::Lut { size, values: rgb };
1089 for pass in &mut chain.passes {
1090 if let Pass::Lut(slot) = pass {
1091 *slot = lut;
1092 return Ok(());
1093 }
1094 }
1095 let index = chain
1096 .passes
1097 .iter()
1098 .position(|pass| matches!(pass, Pass::Tone(_)))
1099 .unwrap_or(chain.passes.len());
1100 chain.passes.insert(index, Pass::Lut(lut));
1101 Ok(())
1102}
1103
1104fn set_weights(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1105 let items = floats(value, "weights", 3)?;
1106 let weights = [items[0], items[1], items[2]];
1107 for pass in &mut chain.passes {
1108 match pass {
1109 Pass::Bw(slot) => {
1110 slot.weights = weights;
1111 return Ok(());
1112 }
1113 Pass::Saturation(slot) => {
1114 slot.weights = weights;
1115 return Ok(());
1116 }
1117 _ => {}
1118 }
1119 }
1120 chain.passes.push(Pass::Bw(fx::Bw { weights }));
1121 Ok(())
1122}
1123
1124fn default_cel() -> fx::Cel {
1125 fx::Cel {
1126 bands: 3,
1127 shadow: 0.45,
1128 threshold: 0.45,
1129 softness: 0.08,
1130 spec: 0.0,
1131 spec_roughness: 0.25,
1132 spec_threshold: 0.72,
1133 light: [0.3, 0.75, 0.6],
1134 }
1135}
1136
1137fn set_cel(chain: &mut Chain, edit: impl FnOnce(&mut fx::Cel)) {
1138 if !chain.passes.iter().any(|pass| matches!(pass, Pass::Cel(_))) {
1139 chain.passes.insert(0, Pass::Cel(default_cel()));
1140 }
1141 if let Some(Pass::Cel(slot)) = chain
1142 .passes
1143 .iter_mut()
1144 .find(|pass| matches!(pass, Pass::Cel(_)))
1145 {
1146 edit(slot);
1147 }
1148}
1149
1150fn set_cavity(chain: &mut Chain, edit: impl FnOnce(&mut fx::Cavity)) {
1151 if !chain
1152 .passes
1153 .iter()
1154 .any(|pass| matches!(pass, Pass::Cavity(_)))
1155 {
1156 chain.passes.push(Pass::Cavity(fx::Cavity {
1157 strength: 0.35,
1158 distance: 2.0,
1159 }));
1160 }
1161 if let Some(Pass::Cavity(slot)) = chain
1162 .passes
1163 .iter_mut()
1164 .find(|pass| matches!(pass, Pass::Cavity(_)))
1165 {
1166 edit(slot);
1167 }
1168}
1169
1170fn set_rim(chain: &mut Chain, edit: impl FnOnce(&mut fx::Rim)) {
1171 if !chain.passes.iter().any(|pass| matches!(pass, Pass::Rim(_))) {
1172 chain.passes.push(Pass::Rim(fx::Rim {
1173 strength: 0.22,
1174 width: 0.35,
1175 color: [1.0, 0.95, 0.85],
1176 }));
1177 }
1178 if let Some(Pass::Rim(slot)) = chain
1179 .passes
1180 .iter_mut()
1181 .find(|pass| matches!(pass, Pass::Rim(_)))
1182 {
1183 edit(slot);
1184 }
1185}
1186
1187fn default_outline() -> fx::Outline {
1188 fx::Outline {
1189 enabled: true,
1190 thickness: 1.25,
1191 color: [0.02, 0.015, 0.02],
1192 local_color: false,
1193 alpha: 0.92,
1194 crease_angle: 35.0,
1195 depth_gap: 0.04,
1196 }
1197}
1198
1199fn set_outline(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1200 if let Some(on) = value.as_bool() {
1201 set_outline_field(chain, |outline| outline.enabled = on);
1202 return Ok(());
1203 }
1204 let Some(table) = value.as_table() else {
1205 return Err(ParseError::msg("outline is a bool or a table"));
1206 };
1207 if let Some(bad) = table
1208 .keys()
1209 .find(|key| !OUTLINE_KEYS.contains(&key.as_str()))
1210 {
1211 return Err(unknown("outline", bad, OUTLINE_KEYS));
1212 }
1213 if let Some(v) = table.get("enabled") {
1214 let on = boolean(v, "outline.enabled")?;
1215 set_outline_field(chain, |o| o.enabled = on);
1216 } else {
1217 set_outline_field(chain, |_| {});
1218 }
1219 if let Some(v) = table.get("thickness") {
1220 let n = number(v, "outline.thickness")?;
1221 set_outline_field(chain, |o| o.thickness = n);
1222 }
1223 if let Some(v) = table.get("alpha") {
1224 let n = number(v, "outline.alpha")?;
1225 set_outline_field(chain, |o| o.alpha = n);
1226 }
1227 if let Some(v) = table.get("crease_angle") {
1228 let n = number(v, "outline.crease_angle")?;
1229 set_outline_field(chain, |o| o.crease_angle = n);
1230 }
1231 if let Some(v) = table.get("color") {
1232 let c = color(v, "outline.color")?;
1233 set_outline_field(chain, |o| o.color = c);
1234 }
1235 if let Some(v) = table.get("local_color") {
1236 let on = boolean(v, "outline.local_color")?;
1237 set_outline_field(chain, |o| o.local_color = on);
1238 }
1239 Ok(())
1240}
1241
1242fn set_outline_field(chain: &mut Chain, edit: impl FnOnce(&mut fx::Outline)) {
1243 if !chain
1244 .passes
1245 .iter()
1246 .any(|pass| matches!(pass, Pass::Outline(_)))
1247 {
1248 chain.passes.push(Pass::Outline(default_outline()));
1249 }
1250 if let Some(Pass::Outline(slot)) = chain
1251 .passes
1252 .iter_mut()
1253 .find(|pass| matches!(pass, Pass::Outline(_)))
1254 {
1255 edit(slot);
1256 }
1257}
1258
1259fn set_noir(chain: &mut Chain, edit: impl FnOnce(&mut fx::Noir)) {
1260 if !chain
1261 .passes
1262 .iter()
1263 .any(|pass| matches!(pass, Pass::Noir(_)))
1264 {
1265 chain.passes.insert(
1266 0,
1267 Pass::Noir(fx::Noir {
1268 contrast: 1.9,
1269 crush: 0.18,
1270 keep: None,
1271 keep_range: 0.45,
1272 }),
1273 );
1274 }
1275 if let Some(Pass::Noir(slot)) = chain
1276 .passes
1277 .iter_mut()
1278 .find(|pass| matches!(pass, Pass::Noir(_)))
1279 {
1280 edit(slot);
1281 }
1282}
1283
1284fn set_sepia(chain: &mut Chain, value: f32) {
1285 for pass in &mut chain.passes {
1286 if let Pass::Sepia(slot) = pass {
1287 *slot = value;
1288 return;
1289 }
1290 }
1291 chain.passes.push(Pass::Sepia(value));
1292}
1293
1294fn set_levels(chain: &mut Chain, value: f32) {
1295 let mut hit = false;
1296 for pass in &mut chain.passes {
1297 match pass {
1298 Pass::Posterize(slot) => {
1299 *slot = value;
1300 hit = true;
1301 }
1302 Pass::Comic(slot) => {
1303 slot.levels = value.max(2.0).round() as u32;
1304 hit = true;
1305 }
1306 _ => {}
1307 }
1308 }
1309 if !hit {
1310 chain.passes.push(Pass::Posterize(value));
1311 }
1312}
1313
1314fn set_modern_comic(chain: &mut Chain, edit: impl FnOnce(&mut ModernComic)) {
1315 if let Some(Pass::ModernComic(pass)) = chain
1316 .passes
1317 .iter_mut()
1318 .find(|pass| matches!(pass, Pass::ModernComic(_)))
1319 {
1320 edit(pass);
1321 } else {
1322 let mut pass = ModernComic::default();
1323 edit(&mut pass);
1324 chain.passes.push(Pass::ModernComic(pass));
1325 }
1326}
1327
1328fn set_f32(
1329 chain: &mut Chain,
1330 value: f32,
1331 found: impl Fn(&Pass) -> bool,
1332 make: impl Fn(f32) -> Pass,
1333) {
1334 for pass in &mut chain.passes {
1335 if found(pass) {
1336 match pass {
1337 Pass::Flicker(slot)
1338 | Pass::Weave(slot)
1339 | Pass::Dust(slot)
1340 | Pass::Aberration(slot)
1341 | Pass::Distortion(slot)
1342 | Pass::Curvature(slot)
1343 | Pass::Aperture(slot) => *slot = value,
1344 _ => {}
1345 }
1346 return;
1347 }
1348 }
1349 chain.passes.push(make(value));
1350}
1351
1352fn set_scratches(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1353 if let Some(table) = value.as_table() {
1354 if let Some(bad) = table
1355 .keys()
1356 .find(|key| !SCRATCH_KEYS.contains(&key.as_str()))
1357 {
1358 return Err(unknown("scratches", bad, SCRATCH_KEYS));
1359 }
1360 ensure_scratches(chain);
1361 if let Some(v) = table.get("count") {
1362 let n = integer(v, "scratches.count")?;
1363 scratches_mut(chain).count = n;
1364 }
1365 if let Some(v) = table.get("strength") {
1366 let n = number(v, "scratches.strength")?;
1367 scratches_mut(chain).strength = n;
1368 }
1369 return Ok(());
1370 }
1371 let n = number(value, "scratches")?;
1372 if chain
1373 .passes
1374 .iter()
1375 .any(|pass| matches!(pass, Pass::Scratches(_)))
1376 {
1377 scratches_mut(chain).strength = n;
1378 } else {
1379 chain.passes.push(Pass::Scratches(fx::Scratches {
1380 count: 3,
1381 strength: n,
1382 }));
1383 }
1384 Ok(())
1385}
1386
1387fn ensure_scratches(chain: &mut Chain) {
1388 if !chain
1389 .passes
1390 .iter()
1391 .any(|pass| matches!(pass, Pass::Scratches(_)))
1392 {
1393 chain.passes.push(Pass::Scratches(fx::Scratches {
1394 count: 3,
1395 strength: 0.35,
1396 }));
1397 }
1398}
1399
1400fn scratches_mut(chain: &mut Chain) -> &mut fx::Scratches {
1401 ensure_scratches(chain);
1402 let Some(Pass::Scratches(slot)) = chain
1403 .passes
1404 .iter_mut()
1405 .find(|pass| matches!(pass, Pass::Scratches(_)))
1406 else {
1407 unreachable!("scratches were inserted");
1408 };
1409 slot
1410}
1411
1412fn set_halation(chain: &mut Chain, value: f32) {
1413 for pass in &mut chain.passes {
1414 if let Pass::Halation(slot) = pass {
1415 slot.strength = value;
1416 return;
1417 }
1418 }
1419 chain.passes.push(Pass::Halation(fx::Halation {
1420 strength: value,
1421 threshold: 0.65,
1422 radius: 6,
1423 }));
1424}
1425
1426fn set_scanlines(chain: &mut Chain, value: f32) {
1427 for pass in &mut chain.passes {
1428 if let Pass::Scanlines(slot) = pass {
1429 slot.strength = value;
1430 return;
1431 }
1432 }
1433 chain.passes.push(Pass::Scanlines(fx::Scanlines {
1434 strength: value,
1435 period: 2,
1436 }));
1437}
1438
1439fn set_halftone(chain: &mut Chain, edit: impl FnOnce(&mut fx::Halftone)) {
1440 if !chain
1441 .passes
1442 .iter()
1443 .any(|pass| matches!(pass, Pass::Halftone(_)))
1444 {
1445 chain.passes.push(Pass::Halftone(fx::Halftone {
1446 cell: 6.0,
1447 angle: 15.0,
1448 ink: [0.02, 0.02, 0.03],
1449 }));
1450 }
1451 if let Some(Pass::Halftone(slot)) = chain
1452 .passes
1453 .iter_mut()
1454 .find(|pass| matches!(pass, Pass::Halftone(_)))
1455 {
1456 edit(slot);
1457 }
1458}
1459
1460fn set_ink(chain: &mut Chain, value: &Value) -> Result<(), ParseError> {
1461 if value.as_array().is_some() || value.as_str().is_some() {
1462 let c = color(value, "ink")?;
1463 set_halftone(chain, |h| h.ink = c);
1464 return Ok(());
1465 }
1466 let n = number(value, "ink")?;
1467 for pass in &mut chain.passes {
1468 if let Pass::Comic(slot) = pass {
1469 slot.ink = n;
1470 return Ok(());
1471 }
1472 }
1473 chain
1474 .passes
1475 .push(Pass::Comic(fx::Comic { levels: 5, ink: n }));
1476 Ok(())
1477}
1478
1479fn set_water(chain: &mut Chain, edit: impl FnOnce(&mut fx::Watercolor)) {
1480 if !chain
1481 .passes
1482 .iter()
1483 .any(|pass| matches!(pass, Pass::Watercolor(_)))
1484 {
1485 chain.passes.push(Pass::Watercolor(fx::Watercolor {
1486 darkening: 0.65,
1487 grain: 0.22,
1488 scale: 6.0,
1489 }));
1490 }
1491 if let Some(Pass::Watercolor(slot)) = chain
1492 .passes
1493 .iter_mut()
1494 .find(|pass| matches!(pass, Pass::Watercolor(_)))
1495 {
1496 edit(slot);
1497 }
1498}
1499
1500fn set_paper_grain(chain: &mut Chain, value: f32) {
1501 let mut hit = false;
1502 for pass in &mut chain.passes {
1503 match pass {
1504 Pass::PaperGrain(slot) => {
1505 slot.strength = value;
1506 hit = true;
1507 }
1508 Pass::Watercolor(slot) => {
1509 slot.grain = value;
1510 hit = true;
1511 }
1512 _ => {}
1513 }
1514 }
1515 if !hit {
1516 chain.passes.push(Pass::PaperGrain(fx::PaperGrain {
1517 strength: value,
1518 scale: 5.0,
1519 }));
1520 }
1521}
1522
1523fn set_scale(chain: &mut Chain, value: f32) {
1524 let mut hit = false;
1525 for pass in &mut chain.passes {
1526 match pass {
1527 Pass::PaperGrain(slot) => {
1528 slot.scale = value;
1529 hit = true;
1530 }
1531 Pass::Watercolor(slot) => {
1532 slot.scale = value;
1533 hit = true;
1534 }
1535 _ => {}
1536 }
1537 }
1538 if !hit {
1539 chain.passes.push(Pass::PaperGrain(fx::PaperGrain {
1540 strength: 0.28,
1541 scale: value,
1542 }));
1543 }
1544}
1545
1546fn set_pixel_size(chain: &mut Chain, value: u32) {
1547 for pass in &mut chain.passes {
1548 if let Pass::Pixel(slot) = pass {
1549 slot.size = value.max(1);
1550 return;
1551 }
1552 }
1553 chain.passes.push(Pass::Pixel(fx::Pixelate {
1554 size: value.max(1),
1555 levels: 0,
1556 palette: true,
1557 }));
1558}
1559
1560fn set_palette(chain: &mut Chain, value: bool) {
1561 for pass in &mut chain.passes {
1562 if let Pass::Pixel(slot) = pass {
1563 slot.palette = value;
1564 return;
1565 }
1566 }
1567 chain.passes.push(Pass::Pixel(fx::Pixelate {
1568 size: 4,
1569 levels: 0,
1570 palette: value,
1571 }));
1572}
1573
1574fn set_duotone(chain: &mut Chain, edit: impl FnOnce(&mut fx::Duotone)) {
1575 if !chain
1576 .passes
1577 .iter()
1578 .any(|pass| matches!(pass, Pass::Duotone(_)))
1579 {
1580 chain.passes.push(Pass::Duotone(fx::Duotone {
1581 shadow: [0.05, 0.08, 0.18],
1582 highlight: [0.96, 0.82, 0.55],
1583 }));
1584 }
1585 if let Some(Pass::Duotone(slot)) = chain
1586 .passes
1587 .iter_mut()
1588 .find(|pass| matches!(pass, Pass::Duotone(_)))
1589 {
1590 edit(slot);
1591 }
1592}
1593
1594fn set_radius(chain: &mut Chain, value: f32) {
1595 let mut hit = false;
1596 for pass in &mut chain.passes {
1597 match pass {
1598 Pass::Bloom(slot) => {
1599 slot.radius = value.max(0.0).round() as u32;
1600 hit = true;
1601 }
1602 Pass::Kuwahara(slot) => {
1603 *slot = value.max(0.0).round() as u32;
1604 hit = true;
1605 }
1606 Pass::Halation(slot) => {
1607 slot.radius = value.max(0.0).round() as u32;
1608 hit = true;
1609 }
1610 Pass::Neon(slot) => {
1611 slot.radius = value.max(0.0).round() as u32;
1612 hit = true;
1613 }
1614 Pass::TiltShift(slot) => {
1615 slot.radius = value.max(0.0);
1616 hit = true;
1617 }
1618 _ => {}
1619 }
1620 }
1621 if !hit {
1622 chain
1623 .passes
1624 .push(Pass::Kuwahara(value.max(0.0).round() as u32));
1625 }
1626}
1627
1628fn set_tilt(chain: &mut Chain, edit: impl FnOnce(&mut fx::TiltShift)) {
1629 if !chain
1630 .passes
1631 .iter()
1632 .any(|pass| matches!(pass, Pass::TiltShift(_)))
1633 {
1634 chain.passes.push(Pass::TiltShift(fx::TiltShift {
1635 focus: 0.5,
1636 range: 0.22,
1637 radius: 5.0,
1638 }));
1639 }
1640 if let Some(Pass::TiltShift(slot)) = chain
1641 .passes
1642 .iter_mut()
1643 .find(|pass| matches!(pass, Pass::TiltShift(_)))
1644 {
1645 edit(slot);
1646 }
1647}