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;