onedrop_parser/
double_preset.rs

1//! Double-preset format (.od2) - Blend two presets simultaneously
2//!
3//! Inspired by MilkDrop3's .milk2 format, this allows blending two presets
4//! with 27 different blending patterns for creative combinations.
5
6use crate::error::Result;
7use crate::preset::MilkPreset;
8use serde::{Deserialize, Serialize};
9
10/// A double-preset that blends two presets together.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct DoublePreset {
13    /// First preset (A)
14    pub preset_a: MilkPreset,
15
16    /// Second preset (B)
17    pub preset_b: MilkPreset,
18
19    /// Blending pattern
20    pub blend_pattern: BlendPattern,
21
22    /// Blend amount (0.0 = all A, 1.0 = all B)
23    pub blend_amount: f32,
24
25    /// Whether to animate the blend
26    pub animate_blend: bool,
27
28    /// Animation speed (if animate_blend is true)
29    pub animation_speed: f32,
30}
31
32/// Blending patterns for double-presets.
33///
34/// These 27 patterns are inspired by MilkDrop3 and provide various
35/// ways to combine two presets visually.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum BlendPattern {
38    /// Simple alpha blend
39    Alpha = 0,
40
41    /// Additive blending
42    Additive = 1,
43
44    /// Multiply blending
45    Multiply = 2,
46
47    /// Screen blending
48    Screen = 3,
49
50    /// Overlay blending
51    Overlay = 4,
52
53    /// Darken (min)
54    Darken = 5,
55
56    /// Lighten (max)
57    Lighten = 6,
58
59    /// Color dodge
60    ColorDodge = 7,
61
62    /// Color burn
63    ColorBurn = 8,
64
65    /// Hard light
66    HardLight = 9,
67
68    /// Soft light
69    SoftLight = 10,
70
71    /// Difference
72    Difference = 11,
73
74    /// Exclusion
75    Exclusion = 12,
76
77    /// Plasma blend
78    Plasma = 13,
79
80    /// Snail blend (spiral pattern)
81    Snail = 14,
82
83    /// Triangle blend
84    Triangle = 15,
85
86    /// Donuts blend (circular pattern)
87    Donuts = 16,
88
89    /// Checkerboard blend
90    Checkerboard = 17,
91
92    /// Horizontal stripes
93    HorizontalStripes = 18,
94
95    /// Vertical stripes
96    VerticalStripes = 19,
97
98    /// Diagonal stripes
99    DiagonalStripes = 20,
100
101    /// Radial blend (from center)
102    Radial = 21,
103
104    /// Angular blend (rotating)
105    Angular = 22,
106
107    /// Perlin noise blend
108    PerlinNoise = 23,
109
110    /// Voronoi blend
111    Voronoi = 24,
112
113    /// Wave blend (sine wave pattern)
114    Wave = 25,
115
116    /// Random pixel blend
117    RandomPixel = 26,
118}
119
120impl BlendPattern {
121    /// Get all available blend patterns.
122    pub fn all() -> Vec<BlendPattern> {
123        vec![
124            BlendPattern::Alpha,
125            BlendPattern::Additive,
126            BlendPattern::Multiply,
127            BlendPattern::Screen,
128            BlendPattern::Overlay,
129            BlendPattern::Darken,
130            BlendPattern::Lighten,
131            BlendPattern::ColorDodge,
132            BlendPattern::ColorBurn,
133            BlendPattern::HardLight,
134            BlendPattern::SoftLight,
135            BlendPattern::Difference,
136            BlendPattern::Exclusion,
137            BlendPattern::Plasma,
138            BlendPattern::Snail,
139            BlendPattern::Triangle,
140            BlendPattern::Donuts,
141            BlendPattern::Checkerboard,
142            BlendPattern::HorizontalStripes,
143            BlendPattern::VerticalStripes,
144            BlendPattern::DiagonalStripes,
145            BlendPattern::Radial,
146            BlendPattern::Angular,
147            BlendPattern::PerlinNoise,
148            BlendPattern::Voronoi,
149            BlendPattern::Wave,
150            BlendPattern::RandomPixel,
151        ]
152    }
153
154    /// Get pattern name.
155    pub fn name(&self) -> &'static str {
156        match self {
157            BlendPattern::Alpha => "Alpha",
158            BlendPattern::Additive => "Additive",
159            BlendPattern::Multiply => "Multiply",
160            BlendPattern::Screen => "Screen",
161            BlendPattern::Overlay => "Overlay",
162            BlendPattern::Darken => "Darken",
163            BlendPattern::Lighten => "Lighten",
164            BlendPattern::ColorDodge => "Color Dodge",
165            BlendPattern::ColorBurn => "Color Burn",
166            BlendPattern::HardLight => "Hard Light",
167            BlendPattern::SoftLight => "Soft Light",
168            BlendPattern::Difference => "Difference",
169            BlendPattern::Exclusion => "Exclusion",
170            BlendPattern::Plasma => "Plasma",
171            BlendPattern::Snail => "Snail",
172            BlendPattern::Triangle => "Triangle",
173            BlendPattern::Donuts => "Donuts",
174            BlendPattern::Checkerboard => "Checkerboard",
175            BlendPattern::HorizontalStripes => "Horizontal Stripes",
176            BlendPattern::VerticalStripes => "Vertical Stripes",
177            BlendPattern::DiagonalStripes => "Diagonal Stripes",
178            BlendPattern::Radial => "Radial",
179            BlendPattern::Angular => "Angular",
180            BlendPattern::PerlinNoise => "Perlin Noise",
181            BlendPattern::Voronoi => "Voronoi",
182            BlendPattern::Wave => "Wave",
183            BlendPattern::RandomPixel => "Random Pixel",
184        }
185    }
186
187    /// Get pattern from index.
188    pub fn from_index(index: usize) -> Option<BlendPattern> {
189        match index {
190            0 => Some(BlendPattern::Alpha),
191            1 => Some(BlendPattern::Additive),
192            2 => Some(BlendPattern::Multiply),
193            3 => Some(BlendPattern::Screen),
194            4 => Some(BlendPattern::Overlay),
195            5 => Some(BlendPattern::Darken),
196            6 => Some(BlendPattern::Lighten),
197            7 => Some(BlendPattern::ColorDodge),
198            8 => Some(BlendPattern::ColorBurn),
199            9 => Some(BlendPattern::HardLight),
200            10 => Some(BlendPattern::SoftLight),
201            11 => Some(BlendPattern::Difference),
202            12 => Some(BlendPattern::Exclusion),
203            13 => Some(BlendPattern::Plasma),
204            14 => Some(BlendPattern::Snail),
205            15 => Some(BlendPattern::Triangle),
206            16 => Some(BlendPattern::Donuts),
207            17 => Some(BlendPattern::Checkerboard),
208            18 => Some(BlendPattern::HorizontalStripes),
209            19 => Some(BlendPattern::VerticalStripes),
210            20 => Some(BlendPattern::DiagonalStripes),
211            21 => Some(BlendPattern::Radial),
212            22 => Some(BlendPattern::Angular),
213            23 => Some(BlendPattern::PerlinNoise),
214            24 => Some(BlendPattern::Voronoi),
215            25 => Some(BlendPattern::Wave),
216            26 => Some(BlendPattern::RandomPixel),
217            _ => None,
218        }
219    }
220}
221
222impl Default for DoublePreset {
223    fn default() -> Self {
224        Self {
225            preset_a: MilkPreset::default(),
226            preset_b: MilkPreset::default(),
227            blend_pattern: BlendPattern::Alpha,
228            blend_amount: 0.5,
229            animate_blend: false,
230            animation_speed: 1.0,
231        }
232    }
233}
234
235impl DoublePreset {
236    /// Create a new double-preset from two presets.
237    pub fn new(preset_a: MilkPreset, preset_b: MilkPreset) -> Self {
238        Self {
239            preset_a,
240            preset_b,
241            blend_pattern: BlendPattern::Alpha,
242            blend_amount: 0.5,
243            animate_blend: false,
244            animation_speed: 1.0,
245        }
246    }
247
248    /// Create with specific blend pattern.
249    pub fn with_pattern(mut self, pattern: BlendPattern) -> Self {
250        self.blend_pattern = pattern;
251        self
252    }
253
254    /// Set blend amount.
255    pub fn with_blend_amount(mut self, amount: f32) -> Self {
256        self.blend_amount = amount.clamp(0.0, 1.0);
257        self
258    }
259
260    /// Enable animation.
261    pub fn with_animation(mut self, speed: f32) -> Self {
262        self.animate_blend = true;
263        self.animation_speed = speed;
264        self
265    }
266}
267
268/// Parse a .od2 double-preset file.
269pub fn parse_double_preset(content: &str) -> Result<DoublePreset> {
270    // .od2 format:
271    // [DoublePreset]
272    // BlendPattern=<index>
273    // BlendAmount=<0.0-1.0>
274    // AnimateBlend=<0|1>
275    // AnimationSpeed=<float>
276    //
277    // [PresetA]
278    // <preset A content>
279    //
280    // [PresetB]
281    // <preset B content>
282
283    let mut blend_pattern = BlendPattern::Alpha;
284    let mut blend_amount = 0.5;
285    let mut animate_blend = false;
286    let mut animation_speed = 1.0;
287
288    let mut preset_a_content = String::new();
289    let mut preset_b_content = String::new();
290
291    let mut current_section = "";
292
293    for line in content.lines() {
294        let trimmed = line.trim();
295
296        if trimmed.starts_with('[') && trimmed.ends_with(']') {
297            let section = &trimmed[1..trimmed.len() - 1];
298
299            // Handle top-level sections
300            if section == "DoublePreset" || section == "PresetA" || section == "PresetB" {
301                current_section = section;
302                continue;
303            }
304
305            // For preset internal sections like [preset00], add to current preset content
306            match current_section {
307                "PresetA" => {
308                    preset_a_content.push_str(line);
309                    preset_a_content.push('\n');
310                }
311                "PresetB" => {
312                    preset_b_content.push_str(line);
313                    preset_b_content.push('\n');
314                }
315                _ => {}
316            }
317            continue;
318        }
319
320        match current_section {
321            "DoublePreset" => {
322                if let Some((key, value)) = trimmed.split_once('=') {
323                    match key.trim() {
324                        "BlendPattern" => {
325                            if let Ok(index) = value.trim().parse::<usize>() {
326                                blend_pattern =
327                                    BlendPattern::from_index(index).unwrap_or(BlendPattern::Alpha);
328                            }
329                        }
330                        "BlendAmount" => {
331                            if let Ok(amount) = value.trim().parse::<f32>() {
332                                blend_amount = amount.clamp(0.0, 1.0);
333                            }
334                        }
335                        "AnimateBlend" => {
336                            animate_blend = value.trim() == "1";
337                        }
338                        "AnimationSpeed" => {
339                            if let Ok(speed) = value.trim().parse::<f32>() {
340                                animation_speed = speed;
341                            }
342                        }
343                        _ => {}
344                    }
345                }
346            }
347            "PresetA" => {
348                preset_a_content.push_str(line);
349                preset_a_content.push('\n');
350            }
351            "PresetB" => {
352                preset_b_content.push_str(line);
353                preset_b_content.push('\n');
354            }
355            _ => {}
356        }
357    }
358
359    // Parse both presets
360    let preset_a = crate::parse_preset(&preset_a_content)?;
361    let preset_b = crate::parse_preset(&preset_b_content)?;
362
363    Ok(DoublePreset {
364        preset_a,
365        preset_b,
366        blend_pattern,
367        blend_amount,
368        animate_blend,
369        animation_speed,
370    })
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_blend_pattern_all() {
379        let patterns = BlendPattern::all();
380        assert_eq!(patterns.len(), 27);
381    }
382
383    #[test]
384    fn test_blend_pattern_from_index() {
385        assert_eq!(BlendPattern::from_index(0), Some(BlendPattern::Alpha));
386        assert_eq!(BlendPattern::from_index(13), Some(BlendPattern::Plasma));
387        assert_eq!(
388            BlendPattern::from_index(26),
389            Some(BlendPattern::RandomPixel)
390        );
391        assert_eq!(BlendPattern::from_index(27), None);
392    }
393
394    #[test]
395    fn test_double_preset_creation() {
396        let preset_a = MilkPreset::default();
397        let preset_b = MilkPreset::default();
398
399        let double = DoublePreset::new(preset_a, preset_b)
400            .with_pattern(BlendPattern::Plasma)
401            .with_blend_amount(0.7)
402            .with_animation(2.0);
403
404        assert_eq!(double.blend_pattern, BlendPattern::Plasma);
405        assert_eq!(double.blend_amount, 0.7);
406        assert!(double.animate_blend);
407        assert_eq!(double.animation_speed, 2.0);
408    }
409}