1use crate::{AnimationCurve, Curve, DesktopError, IconId, Point};
4use std::sync::Arc;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum ShaderPipeline {
8 Sprite,
9 Particles,
10 Procedural,
11}
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct ShaderProgram {
16 pub execution: Option<Arc<crate::EffectExecution>>,
17 pub pixel_bytecode: Arc<[u8]>,
18 pub vertex_bytecode: Arc<[u8]>,
19 pub pipeline: ShaderPipeline,
20}
21
22impl ShaderProgram {
23 pub fn default_params(&self) -> [f32; 4] {
24 match self.pipeline {
25 ShaderPipeline::Sprite => [8.0, 3.0, 6.0, 30.0],
26 ShaderPipeline::Particles => [3.0, 1.5, 2.0, 0.65],
27 ShaderPipeline::Procedural => self.execution.as_ref().map_or([0.0; 4], |execution| {
28 let values = execution.defaults();
29 [values[0], values[1], values[2], values[3]]
30 }),
31 }
32 }
33
34 pub fn with_parameters(&self, values: &std::collections::BTreeMap<String, f32>) -> Result<Self, DesktopError> {
35 let mut result = self.clone();
36 let execution = result.execution.as_mut().ok_or_else(|| DesktopError::InvalidEffect("named parameters require a procedural definition".into()))?;
37 let execution = Arc::make_mut(execution);
38 for (name, value) in values {
39 let parameter = execution.parameters.iter_mut().find(|parameter| parameter.name == *name)
40 .ok_or_else(|| DesktopError::InvalidEffect(format!("unknown parameter: {name}")))?;
41 if !parameter.accepts(*value) { return Err(DesktopError::InvalidEffect(format!("invalid parameter: {name}"))); }
42 parameter.default = *value;
43 }
44 execution.validate(&execution.defaults())?;
45 Ok(result)
46 }
47}
48
49#[derive(Clone, Debug, PartialEq)]
51pub struct Effect {
52 pub shader: ShaderProgram,
53 pub params: [f32; 4],
54 pub padding_px: u32,
55 pub envelope: Curve,
56 pub seed: f32,
57}
58
59impl Effect {
60 pub fn validate(&self) -> Result<(), DesktopError> {
62 if self.shader.pixel_bytecode.is_empty()
63 || self.shader.vertex_bytecode.is_empty()
64 || self.padding_px > 256
65 || !self.seed.is_finite()
66 || self.params.iter().any(|value| !value.is_finite())
67 {
68 return Err(DesktopError::InvalidEffect(
69 "empty shader, non-finite constants or padding > 256".into(),
70 ));
71 }
72 if self.shader.pipeline == ShaderPipeline::Particles
73 && (!(1.0..=32.0).contains(&self.params[0])
74 || !(0.0..=3.0).contains(&self.params[1])
75 || !(-8.0..=8.0).contains(&self.params[2])
76 || !(0.1..=2.0).contains(&self.params[3])
77 || self.seed.abs() > 65535.0)
78 {
79 return Err(DesktopError::InvalidEffect(
80 "particle pipeline requires cell_px in [1,32], radius in [0,3], turns in [-8,8], dust_size in [0.1,2], and abs(seed) <= 65535".into(),
81 ));
82 }
83 if self.shader.pipeline == ShaderPipeline::Procedural {
84 let execution = self.shader.execution.as_ref().ok_or_else(|| DesktopError::InvalidEffect("missing procedural execution".into()))?;
85 execution.validate(&self.parameter_values())?;
86 if self.seed.abs() > 65535.0 { return Err(DesktopError::InvalidEffect("abs(seed) exceeds 65535".into())); }
87 } else if self.shader.execution.is_some() {
88 return Err(DesktopError::InvalidEffect("execution requires procedural pipeline".into()));
89 }
90 Ok(())
91 }
92
93 pub fn parameter_values(&self) -> [f32; 16] {
94 let mut values = self.shader.execution.as_ref().map_or([0.0; 16], |execution| execution.defaults());
95 values[..4].copy_from_slice(&self.params);
96 values
97 }
98
99 pub fn strength(&self, progress: f32) -> f32 {
101 if progress <= 0.0 || progress >= 1.0 {
102 0.0
103 } else {
104 self.envelope.eval(progress).clamp(0.0, 1.0)
105 }
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct IconFrame {
112 pub id: IconId,
113 pub position: Point,
114 pub progress: f32,
115 pub elapsed_seconds: f32,
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn procedural_requires_execution() {
124 let shader = ShaderProgram {
125 execution: None,
126 pixel_bytecode: Arc::from([1u8]),
127 vertex_bytecode: Arc::from([1u8]),
128 pipeline: ShaderPipeline::Procedural,
129 };
130 let effect = Effect {
131 params: shader.default_params(), shader, padding_px: 16,
132 envelope: Curve::linear(), seed: 3.0,
133 };
134 assert!(effect.validate().is_err());
135 assert_eq!(effect.strength(0.0), 0.0);
136 assert_eq!(effect.strength(1.0), 0.0);
137 }
138
139 #[test]
140 fn particle_parameters_are_bounded() {
141 let shader = ShaderProgram {
142 execution: None,
143 pixel_bytecode: Arc::from([1u8]),
144 vertex_bytecode: Arc::from([1u8]),
145 pipeline: ShaderPipeline::Particles,
146 };
147 let mut effect = Effect {
148 params: shader.default_params(),
149 shader,
150 padding_px: 0,
151 envelope: Curve::linear(),
152 seed: 0.0,
153 };
154 effect.validate().unwrap();
155 for (index, value) in [
156 (0, 0.0),
157 (0, 33.0),
158 (1, -1.0),
159 (1, 4.0),
160 (2, -9.0),
161 (2, 9.0),
162 (3, 0.0),
163 (3, 3.0),
164 ] {
165 let mut invalid = effect.clone();
166 invalid.params[index] = value;
167 assert!(invalid.validate().is_err());
168 }
169 effect.seed = f32::MAX;
170 assert!(effect.validate().is_err());
171 }
172
173 #[test]
174 fn envelope_endpoints_are_identity() {
175 let effect = Effect {
176 shader: ShaderProgram {
177 execution: None,
178 pixel_bytecode: Arc::from([1u8]),
179 vertex_bytecode: Arc::from([1u8]),
180 pipeline: ShaderPipeline::Sprite,
181 },
182 params: [0.0; 4],
183 padding_px: 16,
184 envelope: Curve::linear(),
185 seed: 0.0,
186 };
187 assert!(effect.validate().is_ok());
188 assert_eq!(effect.strength(0.0), 0.0);
189 assert_eq!(effect.strength(0.5), 0.5);
190 assert_eq!(effect.strength(1.0), 0.0);
191 let invalid = Effect {
192 padding_px: 257,
193 ..effect
194 };
195 assert!(invalid.validate().is_err());
196 }
197}