1use std::time::Duration;
6
7use serde::{Deserialize, Serialize};
8
9use super::parameters;
10
11#[cfg(not(target_os = "macos"))]
12use crate::input::Bounds;
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum Style {
17 Sparks,
18 Waves,
19 Clouds,
20 Sunrise,
21 Aurora,
22 Arcs,
23 Ferrofluid,
24 #[default]
25 #[serde(other)]
26 Membrane,
27}
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum Landing {
32 #[default]
33 Ring,
34 Pulse,
35 Dot,
36}
37
38impl Landing {
39 pub(super) const fn radius(self) -> f64 {
40 match self {
41 Self::Ring => 80.0,
42 Self::Pulse => 110.0,
43 Self::Dot => 70.0,
44 }
45 }
46
47 pub(super) const fn band(self) -> f64 {
48 match self {
49 Self::Ring => 28.0,
50 Self::Pulse => 24.0,
51 Self::Dot => 0.0,
52 }
53 }
54
55 pub(super) fn frame(self, elapsed: Duration, duration: Duration, animate: bool) -> (f64, f64) {
57 const PULSE_START: f64 = 30.0 / Landing::Pulse.radius();
58 let progress = (elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0);
59 let motion = if animate { progress } else { 0.0 };
60 let radius = match self {
61 Self::Ring => 1.0,
62 Self::Pulse if !animate => 1.0,
63 Self::Pulse => PULSE_START + (1.0 - PULSE_START) * motion,
64 Self::Dot => 1.0 - motion,
65 };
66 let alpha = if progress >= 1.0 {
67 0.0
68 } else if animate {
69 1.0 - progress
70 } else {
71 1.0
72 };
73 (radius, alpha)
74 }
75}
76
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79#[repr(u8)]
80pub enum Color {
81 #[default]
82 Accent,
83 Green,
84 Orange,
85 Pink,
86 Ibtesam,
87 Lunada,
88 Stripe,
89 Ali,
90 Alihossein,
91 Transfile,
92 ElectricViolet,
93 Martini,
94 Superman,
95 EdsSunsetGradient,
96 SublimeLight,
97 Argon,
98 #[serde(alias = "jshine")]
99 JShine,
100 Flare,
101 UltraVoilet,
102 KyooPal,
103 KyeMeh,
104 Wiretap,
105 Rastafari,
106 SinCityRed,
107 AzurLane,
108 WitchingHour,
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum Size {
114 Small,
115 #[default]
116 Medium,
117 Large,
118}
119
120impl Size {
121 #[cfg(all(unix, not(target_os = "macos")))]
122 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
124 pub(super) fn pixels(self, base: u32) -> u32 {
125 (f64::from(base) * self.multiplier()).round().max(1.0) as u32
126 }
127
128 #[must_use]
129 pub(super) const fn multiplier(self) -> f64 {
130 match self {
131 Self::Small => 0.5,
132 Self::Medium => 1.0,
133 Self::Large => 2.0,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
139#[serde(default, deny_unknown_fields)]
140pub struct Look {
141 pub style: Style,
142 #[serde(rename = "landing_style")]
143 pub landing: Landing,
144 pub color: Color,
145 pub size: Size,
146 pub depth: f32,
147 pub radius: f32,
148 pub speed: f32,
149 pub frequency: f32,
150 pub intensity: f32,
151 #[serde(skip)]
152 pub show_edge: bool,
153 #[serde(skip)]
154 pub show_marker: bool,
155}
156
157pub(super) const LANDING_SOFT_EDGE: f64 = 4.0;
158
159impl Look {
160 pub(super) fn arrival_diameter(self) -> f64 {
161 (self.landing.radius() + LANDING_SOFT_EDGE) * 2.0 * self.size.multiplier()
162 }
163}
164
165impl Default for Look {
166 fn default() -> Self {
167 Self {
168 style: Style::default(),
169 landing: Landing::default(),
170 color: Color::default(),
171 size: Size::default(),
172 depth: parameters::DEPTH.default,
173 radius: parameters::RADIUS.default,
174 speed: parameters::SPEED.default,
175 frequency: parameters::FREQUENCY.default,
176 intensity: parameters::INTENSITY.default,
177 show_edge: true,
178 show_marker: true,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub(super) struct Rect {
185 pub x: f64,
186 pub y: f64,
187 pub width: f64,
188 pub height: f64,
189}
190
191#[cfg(not(target_os = "macos"))]
192impl Rect {
193 #[cfg(all(unix, not(target_os = "macos")))]
194 pub(super) fn from_bounds(bounds: Bounds) -> Self {
195 Self {
196 x: f64::from(bounds.x),
197 y: f64::from(bounds.y),
198 width: f64::from(bounds.width),
199 height: f64::from(bounds.height),
200 }
201 }
202
203 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
205 pub(super) fn bounds(self) -> Bounds {
206 Bounds {
207 x: self.x.round() as i32,
208 y: self.y.round() as i32,
209 width: self.width.round().max(1.0) as u32,
210 height: self.height.round().max(1.0) as u32,
211 }
212 }
213}
214
215pub(super) fn landing_rect(area: Rect, point: (f64, f64), diameter: f64) -> Rect {
217 let width = diameter.min(area.width);
218 let height = diameter.min(area.height);
219 Rect {
220 x: (point.0 - width / 2.0).clamp(area.x, area.x + area.width - width),
221 y: (point.1 - height / 2.0).clamp(area.y, area.y + area.height - height),
222 width,
223 height,
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn frame_elapsed_pulse_grows_and_dot_shrinks() {
233 let duration = Duration::from_millis(400);
235 for (style, grows) in [(Landing::Pulse, true), (Landing::Dot, false)] {
236 let start = style.frame(Duration::ZERO, duration, true);
238 let later = style.frame(duration / 2, duration, true);
239 let reduced = style.frame(duration / 2, duration, false);
240
241 assert!(if grows {
243 later.0 > start.0
244 } else {
245 later.0 < start.0
246 });
247 assert!(later.1 < start.1);
248 assert_eq!(reduced, style.frame(Duration::ZERO, duration, false));
249 assert_eq!(style.frame(duration, duration, true).1, 0.0);
250 }
251 }
252}