1use super::preset_slot::PresetSlot;
21
22const MOTION_VARS: &[&str] = &["zoom", "rot", "cx", "cy", "sx", "sy", "dx", "dy", "warp"];
26
27const MOTION_DEFAULTS: &[f64] = &[
31 1.0, 0.0, 0.5, 0.5,
32 1.0, 1.0, 0.0, 0.0,
33 0.0,
34];
35
36const BOOL_FLAGS: &[&str] = &[
38 "wrap",
39 "darken_center",
40 "invert",
41 "brighten",
42 "darken",
43 "solarize",
44];
45
46fn snapshot_motion(slot: &PresetSlot) -> [f64; 9] {
47 let ctx = slot.evaluator.context();
48 let mut out = [0.0; 9];
49 for (i, name) in MOTION_VARS.iter().enumerate() {
50 out[i] = ctx.get_var(name).unwrap_or(MOTION_DEFAULTS[i]);
51 }
52 out
53}
54
55fn snapshot_flags(slot: &PresetSlot) -> [f64; 6] {
56 let ctx = slot.evaluator.context();
57 let mut out = [0.0; 6];
58 for (i, name) in BOOL_FLAGS.iter().enumerate() {
59 out[i] = ctx.get_var(name).unwrap_or(0.0);
60 }
61 out
62}
63
64fn write_motion_and_flags(slot: &mut PresetSlot, motion: &[f64; 9], flags: &[f64; 6]) {
65 let ctx = slot.evaluator.context_mut();
66 for (i, name) in MOTION_VARS.iter().enumerate() {
67 ctx.set_var(name, motion[i]);
68 }
69 for (i, name) in BOOL_FLAGS.iter().enumerate() {
70 ctx.set_var(name, flags[i]);
71 }
72}
73
74pub(super) fn apply_smart_boolean_interpolation(
86 outgoing: &mut PresetSlot,
87 incoming: &mut PresetSlot,
88 progress: f32,
89) {
90 let t = progress.clamp(0.0, 1.0) as f64;
91
92 let a = snapshot_motion(outgoing);
93 let b = snapshot_motion(incoming);
94 let lerped: [f64; 9] = std::array::from_fn(|i| a[i] * (1.0 - t) + b[i] * t);
95
96 let flags = if progress < 0.5 {
97 snapshot_flags(outgoing)
98 } else {
99 snapshot_flags(incoming)
100 };
101
102 write_motion_and_flags(outgoing, &lerped, &flags);
103 write_motion_and_flags(incoming, &lerped, &flags);
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use onedrop_eval::MilkEvaluator;
110
111 fn slot_with(motion: &[(&str, f64)], flags: &[(&str, f64)]) -> PresetSlot {
112 let mut slot = PresetSlot::default();
113 let ctx = slot.evaluator.context_mut();
114 for (name, value) in motion {
115 ctx.set_var(name, *value);
116 }
117 for (name, value) in flags {
118 ctx.set_var(name, *value);
119 }
120 slot
121 }
122
123 fn read(slot: &PresetSlot, name: &str) -> f64 {
124 slot.evaluator.context().get_var(name).unwrap_or(0.0)
125 }
126
127 #[test]
128 fn progress_zero_keeps_outgoing_motion() {
129 let mut a = slot_with(&[("zoom", 2.0), ("rot", 0.5), ("cx", 0.2)], &[]);
130 let mut b = slot_with(&[("zoom", 1.0), ("rot", 0.0), ("cx", 0.5)], &[]);
131 apply_smart_boolean_interpolation(&mut a, &mut b, 0.0);
132 assert!((read(&a, "zoom") - 2.0).abs() < 1e-9);
133 assert!(
134 (read(&b, "zoom") - 2.0).abs() < 1e-9,
135 "both lerped to outgoing"
136 );
137 assert!((read(&b, "cx") - 0.2).abs() < 1e-9);
138 }
139
140 #[test]
141 fn progress_one_keeps_incoming_motion() {
142 let mut a = slot_with(&[("zoom", 2.0)], &[]);
143 let mut b = slot_with(&[("zoom", 1.0)], &[]);
144 apply_smart_boolean_interpolation(&mut a, &mut b, 1.0);
145 assert!((read(&a, "zoom") - 1.0).abs() < 1e-9);
146 assert!((read(&b, "zoom") - 1.0).abs() < 1e-9);
147 }
148
149 #[test]
150 fn progress_half_lerps_midpoint() {
151 let mut a = slot_with(&[("zoom", 0.0), ("dx", 1.0)], &[]);
152 let mut b = slot_with(&[("zoom", 4.0), ("dx", 0.0)], &[]);
153 apply_smart_boolean_interpolation(&mut a, &mut b, 0.5);
154 assert!((read(&a, "zoom") - 2.0).abs() < 1e-9);
155 assert!((read(&b, "zoom") - 2.0).abs() < 1e-9);
156 assert!((read(&a, "dx") - 0.5).abs() < 1e-9);
157 assert!((read(&b, "dx") - 0.5).abs() < 1e-9);
158 }
159
160 #[test]
161 fn bool_flags_snap_at_midpoint() {
162 let mut a = slot_with(&[], &[("wrap", 1.0), ("darken_center", 0.0)]);
163 let mut b = slot_with(&[], &[("wrap", 0.0), ("darken_center", 1.0)]);
164
165 apply_smart_boolean_interpolation(&mut a, &mut b, 0.49);
167 assert!((read(&a, "wrap") - 1.0).abs() < 1e-9);
168 assert!((read(&a, "darken_center") - 0.0).abs() < 1e-9);
169 assert!((read(&b, "wrap") - 1.0).abs() < 1e-9);
170
171 let mut a = slot_with(&[], &[("wrap", 1.0), ("darken_center", 0.0)]);
173 let mut b = slot_with(&[], &[("wrap", 0.0), ("darken_center", 1.0)]);
174 apply_smart_boolean_interpolation(&mut a, &mut b, 0.5);
175 assert!((read(&a, "wrap") - 0.0).abs() < 1e-9);
176 assert!((read(&a, "darken_center") - 1.0).abs() < 1e-9);
177 assert!((read(&b, "wrap") - 0.0).abs() < 1e-9);
178 }
179
180 #[test]
181 fn missing_vars_fall_back_to_md2_defaults() {
182 let mut a = PresetSlot::default();
184 let mut b = slot_with(&[("zoom", 5.0)], &[]);
185 apply_smart_boolean_interpolation(&mut a, &mut b, 1.0);
186 assert!((read(&a, "zoom") - 5.0).abs() < 1e-9);
187
188 let mut a = PresetSlot::default();
191 let mut b = slot_with(&[("zoom", 5.0)], &[]);
192 apply_smart_boolean_interpolation(&mut a, &mut b, 0.0);
193 assert!((read(&a, "zoom") - 1.0).abs() < 1e-9);
194 }
195
196 #[allow(dead_code)]
199 fn _force_use_evaluator() -> MilkEvaluator {
200 MilkEvaluator::new()
201 }
202}