1use std::f64::consts::{PI, TAU};
4
5use crate::behavior::Behavior;
6
7pub const FRAMES_PER_SECOND: u32 = 15;
9
10const TAIL_BEND_PER_WAVE_STRENGTH: f64 = 0.45;
11const TAIL_BEND_LAG_RADIANS: f64 = 1.7;
12const TAIL_SPLIT_RADIANS: f64 = 0.1;
13const TAIL_SPLIT_LAG_RADIANS: f64 = 2.6;
14const SIDE_FIN_TRAIL_FRAMES: u32 = 3;
16const PELVIC_SHARE_OF_SIDE_FIN_OPENING: f64 = 0.5;
17const WHISKER_SWAY_RADIANS: f64 = 0.15;
18const WHISKER_SWAY_LAG_RADIANS: f64 = 0.6;
19
20const REST_FRAME_COUNT: u32 = 90;
22const REST_DEPTH: f64 = 0.22;
23const FEEDING_CYCLES_PER_LOOP: f64 = 3.0;
25const BREATHS_PER_LOOP: f64 = 5.0;
27const BREATH_OPENING: f64 = 0.7;
28const GULPS_PER_LOOP: f64 = 3.0;
30const DEEP_REST_FRAME_COUNT: u32 = 120;
31const DEEP_REST_DEPTH: f64 = 0.5;
32
33struct LapStyle {
36 loop_frame_count: u32,
37 lap_count: f64,
38 stroke_frame_count: f64,
39 stroke_share: f64,
41 speed_lag_frames: f64,
43 glide_speed: f64,
44 stroke_speed_gain: f64,
45 wave_frame_count: f64,
46 glide_wave_strength: f64,
47 stroke_wave_gain: f64,
48 glide_tail_spread_radians: f64,
49 stroke_tail_spread_radians: f64,
50 side_fin_strokes: SideFinStrokes,
51 inside_flare_radians: f64,
54 scull_swing_radians: f64,
55 scull_frame_count: f64,
56 scull_phase_radians: f64,
57}
58
59enum SideFinStrokes {
61 Brake {
63 stroke_opening_radians: f64,
64 brake_opening_radians: f64,
65 },
66 TuckAndSpread {
69 tuck_radians: f64,
70 spread_radians: f64,
71 },
72}
73
74const CRUISE_STYLE: LapStyle = LapStyle {
76 loop_frame_count: 120,
77 lap_count: 1.0,
78 stroke_frame_count: 60.0,
79 stroke_share: 0.55,
80 speed_lag_frames: 6.0,
81 glide_speed: 0.75,
82 stroke_speed_gain: 0.5,
83 wave_frame_count: 8.0,
84 glide_wave_strength: 0.3,
85 stroke_wave_gain: 0.9,
86 glide_tail_spread_radians: 0.05,
87 stroke_tail_spread_radians: 0.12,
88 side_fin_strokes: SideFinStrokes::Brake {
89 stroke_opening_radians: 0.08,
90 brake_opening_radians: 0.3,
91 },
92 inside_flare_radians: 0.12,
93 scull_swing_radians: 0.1,
94 scull_frame_count: 20.0,
95 scull_phase_radians: 1.1,
96};
97
98const DART_STYLE: LapStyle = LapStyle {
100 loop_frame_count: 120,
101 lap_count: 2.0,
102 stroke_frame_count: 30.0,
103 stroke_share: 0.35,
104 speed_lag_frames: 3.0,
105 glide_speed: 0.35,
106 stroke_speed_gain: 2.2,
107 wave_frame_count: 6.0,
108 glide_wave_strength: 0.25,
109 stroke_wave_gain: 1.4,
110 glide_tail_spread_radians: 0.04,
111 stroke_tail_spread_radians: 0.16,
112 side_fin_strokes: SideFinStrokes::TuckAndSpread {
113 tuck_radians: 0.12,
114 spread_radians: 0.4,
115 },
116 inside_flare_radians: 0.1,
117 scull_swing_radians: 0.08,
118 scull_frame_count: 15.0,
119 scull_phase_radians: 0.0,
120};
121
122#[derive(Clone, Copy, Debug, PartialEq)]
124pub enum Course {
125 Lap {
128 angle_radians: f64,
130 },
131 Hover {
133 heading_offset_radians: f64,
135 drift_pixels: [f64; 2],
137 },
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
143pub struct SwimPose {
144 pub course: Course,
146 pub depth: f64,
148 pub wave_phase_radians: f64,
150 pub wave_strength: f64,
152 pub tail_lobe_bends_radians: [f64; 2],
154 pub tail_spread_radians: f64,
156 pub side_fin_openings_radians: [f64; 2],
159 pub side_fin_bends_radians: [f64; 2],
161 pub pelvic_fin_opening_radians: f64,
163 pub whisker_sway_radians: f64,
165 pub feeding_phase: Option<f64>,
168 pub mouth_opening: f64,
170}
171
172pub(crate) const STILL_POSE: SwimPose = SwimPose {
175 course: Course::Hover {
176 heading_offset_radians: 0.0,
177 drift_pixels: [0.0; 2],
178 },
179 depth: 0.0,
180 wave_phase_radians: 0.8,
181 wave_strength: 1.0,
182 tail_lobe_bends_radians: [0.0; 2],
183 tail_spread_radians: 0.0,
184 side_fin_openings_radians: [0.0; 2],
185 side_fin_bends_radians: [0.0; 2],
186 pelvic_fin_opening_radians: 0.0,
187 whisker_sway_radians: 0.0,
188 feeding_phase: None,
189 mouth_opening: 0.0,
190};
191
192pub fn frame_count(behavior: Behavior) -> u32 {
194 match behavior {
195 Behavior::Cruise => CRUISE_STYLE.loop_frame_count,
196 Behavior::Dart => DART_STYLE.loop_frame_count,
197 Behavior::Rest | Behavior::SurfaceFeeding => REST_FRAME_COUNT,
198 Behavior::DeepRest => DEEP_REST_FRAME_COUNT,
199 }
200}
201
202pub fn calculate_swim_pose(behavior: Behavior, frame_index: u32) -> SwimPose {
217 let loop_frame_count = frame_count(behavior);
218 let loop_frame = frame_index % loop_frame_count;
219 let drive = calculate_drive(behavior, loop_frame);
220 let trailing_drive = calculate_drive(
221 behavior,
222 (loop_frame + loop_frame_count - SIDE_FIN_TRAIL_FRAMES) % loop_frame_count,
223 );
224 let tail_bend = TAIL_BEND_PER_WAVE_STRENGTH
225 * drive.wave_strength
226 * (drive.wave_phase_radians - TAIL_BEND_LAG_RADIANS).sin();
227 let tail_split = TAIL_SPLIT_RADIANS * (drive.wave_phase_radians - TAIL_SPLIT_LAG_RADIANS).sin();
228 let side_fin_openings = drive.calculate_side_fin_openings();
229 let trailing_openings = trailing_drive.calculate_side_fin_openings();
230 SwimPose {
231 course: drive.course,
232 depth: drive.depth,
233 wave_phase_radians: drive.wave_phase_radians,
234 wave_strength: drive.wave_strength,
235 tail_lobe_bends_radians: [tail_bend - tail_split, tail_bend + tail_split],
236 tail_spread_radians: drive.tail_spread_radians,
237 side_fin_openings_radians: side_fin_openings,
238 side_fin_bends_radians: [
239 trailing_openings[0] - side_fin_openings[0],
240 trailing_openings[1] - side_fin_openings[1],
241 ],
242 pelvic_fin_opening_radians: PELVIC_SHARE_OF_SIDE_FIN_OPENING * drive.stroke_opening_radians,
243 whisker_sway_radians: WHISKER_SWAY_RADIANS
244 * (drive.wave_phase_radians - WHISKER_SWAY_LAG_RADIANS).sin(),
245 feeding_phase: drive.feeding_phase,
246 mouth_opening: drive.mouth_opening,
247 }
248}
249
250struct Drive {
253 course: Course,
254 depth: f64,
255 wave_phase_radians: f64,
256 wave_strength: f64,
257 tail_spread_radians: f64,
258 stroke_opening_radians: f64,
260 right_fin_offset_radians: f64,
263 feeding_phase: Option<f64>,
264 mouth_opening: f64,
265}
266
267impl Drive {
268 fn calculate_side_fin_openings(&self) -> [f64; 2] {
269 [
270 self.stroke_opening_radians - self.right_fin_offset_radians,
271 self.stroke_opening_radians + self.right_fin_offset_radians,
272 ]
273 }
274}
275
276fn calculate_drive(behavior: Behavior, loop_frame: u32) -> Drive {
277 match behavior {
278 Behavior::Cruise => CRUISE_STYLE.calculate_drive(loop_frame),
279 Behavior::Dart => DART_STYLE.calculate_drive(loop_frame),
280 Behavior::Rest => calculate_rest_drive(loop_frame, REST_DEPTH),
281 Behavior::SurfaceFeeding => Drive {
282 feeding_phase: Some(
283 (FEEDING_CYCLES_PER_LOOP * f64::from(loop_frame) / f64::from(REST_FRAME_COUNT))
284 .fract(),
285 ),
286 ..calculate_rest_drive(loop_frame, 0.0)
287 },
288 Behavior::DeepRest => calculate_deep_rest_drive(loop_frame),
289 }
290}
291
292fn calculate_rest_drive(loop_frame: u32, depth: f64) -> Drive {
295 const STROKE_FRAME_COUNT: f64 = 45.0;
296 const STROKE_SHARE: f64 = 0.4;
297 const SWING_RADIANS: f64 = 0.1;
298 const DRIFT_PIXELS: [f64; 2] = [3.0, 2.0];
299 const WAVE_FRAME_COUNT: f64 = 15.0;
300 const GLIDE_WAVE_STRENGTH: f64 = 0.15;
301 const STROKE_WAVE_GAIN: f64 = 0.5;
302 const TAIL_SPREAD_RADIANS: f64 = 0.04;
303 const SIDE_FIN_OPENING_RADIANS: f64 = 0.25;
304 const SIDE_FIN_BREATH_RADIANS: f64 = 0.08;
305 const SCULL_SWING_RADIANS: f64 = 0.12;
306 const SCULL_FRAME_COUNT: f64 = 18.0;
307 let frame = f64::from(loop_frame);
308 let loop_turn = TAU * frame / f64::from(REST_FRAME_COUNT);
309 let stroke = calculate_beat(frame, STROKE_FRAME_COUNT, STROKE_SHARE);
310 Drive {
311 course: Course::Hover {
312 heading_offset_radians: SWING_RADIANS * loop_turn.sin(),
313 drift_pixels: [
314 -DRIFT_PIXELS[0] * loop_turn.sin(),
315 -DRIFT_PIXELS[1] * (2.0 * loop_turn + 1.0).sin(),
316 ],
317 },
318 depth,
319 wave_phase_radians: STILL_POSE.wave_phase_radians + frame * TAU / WAVE_FRAME_COUNT,
320 wave_strength: GLIDE_WAVE_STRENGTH + STROKE_WAVE_GAIN * stroke,
321 tail_spread_radians: TAIL_SPREAD_RADIANS,
322 stroke_opening_radians: SIDE_FIN_OPENING_RADIANS
323 + SIDE_FIN_BREATH_RADIANS * (3.0 * loop_turn).sin(),
324 right_fin_offset_radians: SCULL_SWING_RADIANS * (TAU * frame / SCULL_FRAME_COUNT).sin(),
325 feeding_phase: None,
326 mouth_opening: (GULPS_PER_LOOP * loop_turn).sin().max(0.0),
327 }
328}
329
330fn calculate_deep_rest_drive(loop_frame: u32) -> Drive {
333 const SWING_RADIANS: f64 = 0.05;
334 const DRIFT_PIXELS: [f64; 2] = [1.5, 1.0];
335 const SINK_PIXELS: f64 = 4.0;
336 const WAVE_FRAME_COUNT: f64 = 30.0;
337 const WAVE_STRENGTH: f64 = 0.12;
338 const WAVE_BREATH: f64 = 0.1;
339 const TAIL_SPREAD_RADIANS: f64 = 0.02;
340 const SIDE_FIN_OPENING_RADIANS: f64 = 0.2;
341 const FLICK_RADIANS: f64 = 0.35;
342 const FLICK_START_FRAME: f64 = 70.0;
343 const FLICK_FRAME_COUNT: f64 = 8.0;
344 const GULP_START_FRAME: f64 = 66.0;
346 const GULP_FRAME_COUNT: f64 = 16.0;
347 let frame = f64::from(loop_frame);
348 let loop_turn = TAU * frame / f64::from(DEEP_REST_FRAME_COUNT);
349 let flick_fraction = (frame - FLICK_START_FRAME) / FLICK_FRAME_COUNT;
350 let flick = if (0.0..=1.0).contains(&flick_fraction) {
351 (PI * flick_fraction).sin()
352 } else {
353 0.0
354 };
355 let gulp_fraction = (frame - GULP_START_FRAME) / GULP_FRAME_COUNT;
356 let mouth_opening = if (0.0..=1.0).contains(&gulp_fraction) {
357 (PI * gulp_fraction).sin()
358 } else {
359 0.0
360 };
361 Drive {
362 course: Course::Hover {
363 heading_offset_radians: SWING_RADIANS * loop_turn.sin(),
364 drift_pixels: [
365 -DRIFT_PIXELS[0] * loop_turn.sin(),
366 SINK_PIXELS - DRIFT_PIXELS[1] * (2.0 * loop_turn).sin(),
367 ],
368 },
369 depth: DEEP_REST_DEPTH,
370 wave_phase_radians: STILL_POSE.wave_phase_radians + frame * TAU / WAVE_FRAME_COUNT,
371 wave_strength: WAVE_STRENGTH + WAVE_BREATH * (2.0 * loop_turn).sin(),
372 tail_spread_radians: TAIL_SPREAD_RADIANS,
373 stroke_opening_radians: SIDE_FIN_OPENING_RADIANS,
374 right_fin_offset_radians: FLICK_RADIANS * flick,
375 feeding_phase: None,
376 mouth_opening,
377 }
378}
379
380fn calculate_beat(frame: f64, cycle_frame_count: f64, beat_share: f64) -> f64 {
383 let cycle_fraction = frame.rem_euclid(cycle_frame_count) / cycle_frame_count;
384 if cycle_fraction < beat_share {
385 (PI * cycle_fraction / beat_share).sin()
386 } else {
387 0.0
388 }
389}
390
391impl LapStyle {
392 fn calculate_cycle_fraction(&self, frame: f64) -> f64 {
394 frame.rem_euclid(self.stroke_frame_count) / self.stroke_frame_count
395 }
396
397 fn calculate_stroke(&self, frame: f64) -> f64 {
398 calculate_beat(frame, self.stroke_frame_count, self.stroke_share)
399 }
400
401 fn calculate_drive(&self, loop_frame: u32) -> Drive {
402 let lap_speeds: Vec<f64> = (0..self.loop_frame_count)
403 .map(|speed_frame| {
404 self.glide_speed
405 + self.stroke_speed_gain
406 * self.calculate_stroke(f64::from(speed_frame) - self.speed_lag_frames)
407 })
408 .collect();
409 let total_speed: f64 = lap_speeds.iter().sum();
410 let covered_speed: f64 = lap_speeds[..loop_frame as usize].iter().sum();
411 let frame = f64::from(loop_frame);
412 let stroke = self.calculate_stroke(frame);
413 let wave_phase_radians =
414 STILL_POSE.wave_phase_radians + frame * TAU / self.wave_frame_count;
415 let scull = self.scull_swing_radians
416 * (TAU * frame / self.scull_frame_count + self.scull_phase_radians).sin();
417 let breath = (TAU * BREATHS_PER_LOOP * frame / f64::from(self.loop_frame_count))
418 .sin()
419 .max(0.0);
420 Drive {
421 course: Course::Lap {
422 angle_radians: -PI / 2.0 + TAU * self.lap_count * covered_speed / total_speed,
423 },
424 depth: 0.0,
425 wave_phase_radians,
426 wave_strength: self.glide_wave_strength + self.stroke_wave_gain * stroke,
427 tail_spread_radians: self.glide_tail_spread_radians
428 + self.stroke_tail_spread_radians * stroke * wave_phase_radians.sin().abs(),
429 stroke_opening_radians: self.calculate_stroke_opening(frame),
430 right_fin_offset_radians: self.inside_flare_radians + scull,
431 feeding_phase: None,
432 mouth_opening: BREATH_OPENING * breath * breath,
433 }
434 }
435
436 fn calculate_stroke_opening(&self, frame: f64) -> f64 {
438 match self.side_fin_strokes {
439 SideFinStrokes::Brake {
440 stroke_opening_radians,
441 brake_opening_radians,
442 } => {
443 stroke_opening_radians
444 + brake_opening_radians * (1.0 - self.calculate_stroke(frame))
445 }
446 SideFinStrokes::TuckAndSpread {
447 tuck_radians,
448 spread_radians,
449 } => {
450 let cycle_fraction = self.calculate_cycle_fraction(frame);
451 if cycle_fraction < self.stroke_share {
452 -tuck_radians * self.calculate_stroke(frame)
453 } else {
454 let glide_fraction =
455 (cycle_fraction - self.stroke_share) / (1.0 - self.stroke_share);
456 spread_radians * (PI * glide_fraction).sin()
457 }
458 }
459 }
460 }
461}