Skip to main content

pfx_core/
fx.rs

1use crate::clock::HitStop;
2use crate::ease::Ease;
3use crate::sim::mix64;
4
5pub const FLASHES_PER_SECOND: usize = 3;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Curve {
9    pub low: f32,
10    pub high: f32,
11    pub ease: Ease,
12}
13
14impl Curve {
15    pub const ONE: Self = Self::constant(1.0);
16
17    pub const fn constant(value: f32) -> Self {
18        Self {
19            low: value,
20            high: value,
21            ease: Ease::Linear,
22        }
23    }
24
25    pub const fn new(low: f32, high: f32, ease: Ease) -> Self {
26        Self { low, high, ease }
27    }
28
29    pub fn at(&self, intensity: f32) -> f32 {
30        let t = if intensity.is_finite() {
31            intensity.clamp(0.0, 1.0)
32        } else {
33            0.0
34        };
35        self.low + (self.high - self.low) * self.ease.at(t)
36    }
37}
38
39impl Default for Curve {
40    fn default() -> Self {
41        Self::ONE
42    }
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq)]
46pub struct Response {
47    pub gain: Curve,
48    pub rate: Curve,
49}
50
51impl Response {
52    pub const STEADY: Self = Self {
53        gain: Curve::ONE,
54        rate: Curve::ONE,
55    };
56
57    pub fn gain(&self, intensity: f32) -> f32 {
58        self.gain.at(intensity).max(0.0)
59    }
60
61    pub fn rate(&self, intensity: f32) -> f32 {
62        self.rate.at(intensity).max(0.0)
63    }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct Comfort {
68    pub motion: f32,
69    pub glitch_floor: f32,
70}
71
72impl Default for Comfort {
73    fn default() -> Self {
74        Self::FULL
75    }
76}
77
78impl Comfort {
79    pub const FULL: Self = Self {
80        motion: 1.0,
81        glitch_floor: 0.5,
82    };
83
84    pub fn reduced(motion: f32) -> Self {
85        Self {
86            motion,
87            ..Self::FULL
88        }
89    }
90
91    pub fn amount(&self) -> f32 {
92        if self.motion.is_finite() {
93            self.motion.clamp(0.0, 1.0)
94        } else {
95            1.0
96        }
97    }
98
99    pub fn shake(&self, value: f32) -> f32 {
100        value * self.amount()
101    }
102
103    pub fn blur(&self, displacement: [f32; 2]) -> [f32; 2] {
104        let amount = self.amount();
105        displacement.map(|value| value * amount)
106    }
107
108    pub fn flash(&self, alpha: f32) -> f32 {
109        alpha * self.amount()
110    }
111
112    pub fn hit_stop(&self, stop: HitStop) -> HitStop {
113        let floor = stop.floor.clamp(0.0, 1.0);
114        HitStop {
115            floor: 1.0 - (1.0 - floor) * self.amount(),
116            ..stop
117        }
118    }
119
120    pub fn glitch(&self, strength: f32) -> f32 {
121        let floor = if self.glitch_floor.is_finite() {
122            self.glitch_floor.clamp(0.0, 1.0)
123        } else {
124            1.0
125        };
126        strength * (floor + (1.0 - floor) * self.amount())
127    }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub struct ShakeTuning {
132    pub translation: [f32; 2],
133    pub roll: f32,
134    pub frequency: f32,
135    pub decay: f32,
136    pub seed: u64,
137}
138
139impl Default for ShakeTuning {
140    fn default() -> Self {
141        Self {
142            translation: [0.0; 2],
143            roll: 0.0,
144            frequency: 0.0,
145            decay: 1.0,
146            seed: 0,
147        }
148    }
149}
150
151#[derive(Clone, Copy, Debug, Default, PartialEq)]
152pub struct ShakeOffset {
153    pub translation: [f32; 2],
154    pub roll: f32,
155}
156
157impl ShakeOffset {
158    pub fn is_zero(&self) -> bool {
159        self.translation == [0.0; 2] && self.roll == 0.0
160    }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq)]
164pub struct Shake {
165    pub tuning: ShakeTuning,
166    trauma: f32,
167    phase: f64,
168}
169
170impl Shake {
171    pub fn new(tuning: ShakeTuning) -> Self {
172        Self {
173            tuning,
174            trauma: 0.0,
175            phase: 0.0,
176        }
177    }
178
179    pub fn trauma(&self) -> f32 {
180        self.trauma
181    }
182
183    pub fn phase(&self) -> f64 {
184        self.phase
185    }
186
187    pub fn add(&mut self, trauma: f32) {
188        if trauma.is_finite() {
189            self.trauma = (self.trauma + trauma).clamp(0.0, 1.0);
190        }
191    }
192
193    pub fn clear(&mut self) {
194        self.trauma = 0.0;
195    }
196
197    pub fn step(&mut self, dt: f32, rate: f32) {
198        if !(dt.is_finite() && dt > 0.0) {
199            return;
200        }
201        let decay = self.tuning.decay.max(0.0);
202        self.trauma = (self.trauma - decay * dt).max(0.0);
203        let speed = f64::from(self.tuning.frequency.max(0.0)) * f64::from(rate.max(0.0));
204        self.phase += f64::from(dt) * speed;
205    }
206
207    pub fn offset(&self, gain: f32) -> ShakeOffset {
208        let amount = self.trauma * self.trauma * gain.max(0.0);
209        if amount.is_nan() || amount <= 0.0 {
210            return ShakeOffset::default();
211        }
212        let seed = self.tuning.seed;
213        let noise = |lane: u64| {
214            smooth_noise(
215                mix64(seed ^ lane.wrapping_mul(0x9e37_79b9_7f4a_7c15)),
216                self.phase,
217            )
218        };
219        ShakeOffset {
220            translation: [
221                amount * self.tuning.translation[0] * noise(1),
222                amount * self.tuning.translation[1] * noise(2),
223            ],
224            roll: amount * self.tuning.roll * noise(3),
225        }
226    }
227}
228
229fn lattice(seed: u64, cell: i64) -> f32 {
230    let bits = mix64(seed ^ (cell as u64).wrapping_mul(0xd1b5_4a32_d192_ed03));
231    ((bits >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
232}
233
234pub fn smooth_noise(seed: u64, x: f64) -> f32 {
235    let cell = x.floor();
236    let f = (x - cell) as f32;
237    let index = cell as i64;
238    let g0 = lattice(seed, index);
239    let g1 = lattice(seed, index.wrapping_add(1));
240    let a = g0 * f;
241    let b = g1 * (f - 1.0);
242    let fade = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
243    (2.0 * (a + (b - a) * fade)).clamp(-1.0, 1.0)
244}
245
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub struct FlashGate {
248    starts: [f64; FLASHES_PER_SECOND],
249    next: usize,
250}
251
252impl Default for FlashGate {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258impl FlashGate {
259    pub fn new() -> Self {
260        Self {
261            starts: [f64::NEG_INFINITY; FLASHES_PER_SECOND],
262            next: 0,
263        }
264    }
265
266    pub fn allows(&self, now: f64) -> bool {
267        now - self.starts[self.next] >= 1.0
268    }
269
270    pub fn admit(&mut self, now: f64) -> bool {
271        if !now.is_finite() || !self.allows(now) {
272            return false;
273        }
274        self.starts[self.next] = now;
275        self.next = (self.next + 1) % FLASHES_PER_SECOND;
276        true
277    }
278}
279
280#[cfg(test)]
281mod tests;