use std::f64::consts::{PI, TAU};
use crate::behavior::Behavior;
pub const FRAMES_PER_SECOND: u32 = 15;
const TAIL_BEND_PER_WAVE_STRENGTH: f64 = 0.45;
const TAIL_BEND_LAG_RADIANS: f64 = 1.7;
const TAIL_SPLIT_RADIANS: f64 = 0.1;
const TAIL_SPLIT_LAG_RADIANS: f64 = 2.6;
const SIDE_FIN_TRAIL_FRAMES: u32 = 3;
const PELVIC_SHARE_OF_SIDE_FIN_OPENING: f64 = 0.5;
const WHISKER_SWAY_RADIANS: f64 = 0.15;
const WHISKER_SWAY_LAG_RADIANS: f64 = 0.6;
const REST_FRAME_COUNT: u32 = 90;
const REST_DEPTH: f64 = 0.22;
const FEEDING_CYCLES_PER_LOOP: f64 = 3.0;
const BREATHS_PER_LOOP: f64 = 5.0;
const BREATH_OPENING: f64 = 0.7;
const GULPS_PER_LOOP: f64 = 3.0;
const DEEP_REST_FRAME_COUNT: u32 = 120;
const DEEP_REST_DEPTH: f64 = 0.5;
struct LapStyle {
loop_frame_count: u32,
lap_count: f64,
stroke_frame_count: f64,
stroke_share: f64,
speed_lag_frames: f64,
glide_speed: f64,
stroke_speed_gain: f64,
wave_frame_count: f64,
glide_wave_strength: f64,
stroke_wave_gain: f64,
glide_tail_spread_radians: f64,
stroke_tail_spread_radians: f64,
side_fin_strokes: SideFinStrokes,
inside_flare_radians: f64,
scull_swing_radians: f64,
scull_frame_count: f64,
scull_phase_radians: f64,
}
enum SideFinStrokes {
Brake {
stroke_opening_radians: f64,
brake_opening_radians: f64,
},
TuckAndSpread {
tuck_radians: f64,
spread_radians: f64,
},
}
const CRUISE_STYLE: LapStyle = LapStyle {
loop_frame_count: 120,
lap_count: 1.0,
stroke_frame_count: 60.0,
stroke_share: 0.55,
speed_lag_frames: 6.0,
glide_speed: 0.75,
stroke_speed_gain: 0.5,
wave_frame_count: 8.0,
glide_wave_strength: 0.3,
stroke_wave_gain: 0.9,
glide_tail_spread_radians: 0.05,
stroke_tail_spread_radians: 0.12,
side_fin_strokes: SideFinStrokes::Brake {
stroke_opening_radians: 0.08,
brake_opening_radians: 0.3,
},
inside_flare_radians: 0.12,
scull_swing_radians: 0.1,
scull_frame_count: 20.0,
scull_phase_radians: 1.1,
};
const DART_STYLE: LapStyle = LapStyle {
loop_frame_count: 120,
lap_count: 2.0,
stroke_frame_count: 30.0,
stroke_share: 0.35,
speed_lag_frames: 3.0,
glide_speed: 0.35,
stroke_speed_gain: 2.2,
wave_frame_count: 6.0,
glide_wave_strength: 0.25,
stroke_wave_gain: 1.4,
glide_tail_spread_radians: 0.04,
stroke_tail_spread_radians: 0.16,
side_fin_strokes: SideFinStrokes::TuckAndSpread {
tuck_radians: 0.12,
spread_radians: 0.4,
},
inside_flare_radians: 0.1,
scull_swing_radians: 0.08,
scull_frame_count: 15.0,
scull_phase_radians: 0.0,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Course {
Lap {
angle_radians: f64,
},
Hover {
heading_offset_radians: f64,
drift_pixels: [f64; 2],
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SwimPose {
pub course: Course,
pub depth: f64,
pub wave_phase_radians: f64,
pub wave_strength: f64,
pub tail_lobe_bends_radians: [f64; 2],
pub tail_spread_radians: f64,
pub side_fin_openings_radians: [f64; 2],
pub side_fin_bends_radians: [f64; 2],
pub pelvic_fin_opening_radians: f64,
pub whisker_sway_radians: f64,
pub feeding_phase: Option<f64>,
pub mouth_opening: f64,
}
pub(crate) const STILL_POSE: SwimPose = SwimPose {
course: Course::Hover {
heading_offset_radians: 0.0,
drift_pixels: [0.0; 2],
},
depth: 0.0,
wave_phase_radians: 0.8,
wave_strength: 1.0,
tail_lobe_bends_radians: [0.0; 2],
tail_spread_radians: 0.0,
side_fin_openings_radians: [0.0; 2],
side_fin_bends_radians: [0.0; 2],
pelvic_fin_opening_radians: 0.0,
whisker_sway_radians: 0.0,
feeding_phase: None,
mouth_opening: 0.0,
};
pub fn frame_count(behavior: Behavior) -> u32 {
match behavior {
Behavior::Cruise => CRUISE_STYLE.loop_frame_count,
Behavior::Dart => DART_STYLE.loop_frame_count,
Behavior::Rest | Behavior::SurfaceFeeding => REST_FRAME_COUNT,
Behavior::DeepRest => DEEP_REST_FRAME_COUNT,
}
}
pub fn calculate_swim_pose(behavior: Behavior, frame_index: u32) -> SwimPose {
let loop_frame_count = frame_count(behavior);
let loop_frame = frame_index % loop_frame_count;
let drive = calculate_drive(behavior, loop_frame);
let trailing_drive = calculate_drive(
behavior,
(loop_frame + loop_frame_count - SIDE_FIN_TRAIL_FRAMES) % loop_frame_count,
);
let tail_bend = TAIL_BEND_PER_WAVE_STRENGTH
* drive.wave_strength
* (drive.wave_phase_radians - TAIL_BEND_LAG_RADIANS).sin();
let tail_split = TAIL_SPLIT_RADIANS * (drive.wave_phase_radians - TAIL_SPLIT_LAG_RADIANS).sin();
let side_fin_openings = drive.calculate_side_fin_openings();
let trailing_openings = trailing_drive.calculate_side_fin_openings();
SwimPose {
course: drive.course,
depth: drive.depth,
wave_phase_radians: drive.wave_phase_radians,
wave_strength: drive.wave_strength,
tail_lobe_bends_radians: [tail_bend - tail_split, tail_bend + tail_split],
tail_spread_radians: drive.tail_spread_radians,
side_fin_openings_radians: side_fin_openings,
side_fin_bends_radians: [
trailing_openings[0] - side_fin_openings[0],
trailing_openings[1] - side_fin_openings[1],
],
pelvic_fin_opening_radians: PELVIC_SHARE_OF_SIDE_FIN_OPENING * drive.stroke_opening_radians,
whisker_sway_radians: WHISKER_SWAY_RADIANS
* (drive.wave_phase_radians - WHISKER_SWAY_LAG_RADIANS).sin(),
feeding_phase: drive.feeding_phase,
mouth_opening: drive.mouth_opening,
}
}
struct Drive {
course: Course,
depth: f64,
wave_phase_radians: f64,
wave_strength: f64,
tail_spread_radians: f64,
stroke_opening_radians: f64,
right_fin_offset_radians: f64,
feeding_phase: Option<f64>,
mouth_opening: f64,
}
impl Drive {
fn calculate_side_fin_openings(&self) -> [f64; 2] {
[
self.stroke_opening_radians - self.right_fin_offset_radians,
self.stroke_opening_radians + self.right_fin_offset_radians,
]
}
}
fn calculate_drive(behavior: Behavior, loop_frame: u32) -> Drive {
match behavior {
Behavior::Cruise => CRUISE_STYLE.calculate_drive(loop_frame),
Behavior::Dart => DART_STYLE.calculate_drive(loop_frame),
Behavior::Rest => calculate_rest_drive(loop_frame, REST_DEPTH),
Behavior::SurfaceFeeding => Drive {
feeding_phase: Some(
(FEEDING_CYCLES_PER_LOOP * f64::from(loop_frame) / f64::from(REST_FRAME_COUNT))
.fract(),
),
..calculate_rest_drive(loop_frame, 0.0)
},
Behavior::DeepRest => calculate_deep_rest_drive(loop_frame),
}
}
fn calculate_rest_drive(loop_frame: u32, depth: f64) -> Drive {
const STROKE_FRAME_COUNT: f64 = 45.0;
const STROKE_SHARE: f64 = 0.4;
const SWING_RADIANS: f64 = 0.1;
const DRIFT_PIXELS: [f64; 2] = [3.0, 2.0];
const WAVE_FRAME_COUNT: f64 = 15.0;
const GLIDE_WAVE_STRENGTH: f64 = 0.15;
const STROKE_WAVE_GAIN: f64 = 0.5;
const TAIL_SPREAD_RADIANS: f64 = 0.04;
const SIDE_FIN_OPENING_RADIANS: f64 = 0.25;
const SIDE_FIN_BREATH_RADIANS: f64 = 0.08;
const SCULL_SWING_RADIANS: f64 = 0.12;
const SCULL_FRAME_COUNT: f64 = 18.0;
let frame = f64::from(loop_frame);
let loop_turn = TAU * frame / f64::from(REST_FRAME_COUNT);
let stroke = calculate_beat(frame, STROKE_FRAME_COUNT, STROKE_SHARE);
Drive {
course: Course::Hover {
heading_offset_radians: SWING_RADIANS * loop_turn.sin(),
drift_pixels: [
-DRIFT_PIXELS[0] * loop_turn.sin(),
-DRIFT_PIXELS[1] * (2.0 * loop_turn + 1.0).sin(),
],
},
depth,
wave_phase_radians: STILL_POSE.wave_phase_radians + frame * TAU / WAVE_FRAME_COUNT,
wave_strength: GLIDE_WAVE_STRENGTH + STROKE_WAVE_GAIN * stroke,
tail_spread_radians: TAIL_SPREAD_RADIANS,
stroke_opening_radians: SIDE_FIN_OPENING_RADIANS
+ SIDE_FIN_BREATH_RADIANS * (3.0 * loop_turn).sin(),
right_fin_offset_radians: SCULL_SWING_RADIANS * (TAU * frame / SCULL_FRAME_COUNT).sin(),
feeding_phase: None,
mouth_opening: (GULPS_PER_LOOP * loop_turn).sin().max(0.0),
}
}
fn calculate_deep_rest_drive(loop_frame: u32) -> Drive {
const SWING_RADIANS: f64 = 0.05;
const DRIFT_PIXELS: [f64; 2] = [1.5, 1.0];
const SINK_PIXELS: f64 = 4.0;
const WAVE_FRAME_COUNT: f64 = 30.0;
const WAVE_STRENGTH: f64 = 0.12;
const WAVE_BREATH: f64 = 0.1;
const TAIL_SPREAD_RADIANS: f64 = 0.02;
const SIDE_FIN_OPENING_RADIANS: f64 = 0.2;
const FLICK_RADIANS: f64 = 0.35;
const FLICK_START_FRAME: f64 = 70.0;
const FLICK_FRAME_COUNT: f64 = 8.0;
const GULP_START_FRAME: f64 = 66.0;
const GULP_FRAME_COUNT: f64 = 16.0;
let frame = f64::from(loop_frame);
let loop_turn = TAU * frame / f64::from(DEEP_REST_FRAME_COUNT);
let flick_fraction = (frame - FLICK_START_FRAME) / FLICK_FRAME_COUNT;
let flick = if (0.0..=1.0).contains(&flick_fraction) {
(PI * flick_fraction).sin()
} else {
0.0
};
let gulp_fraction = (frame - GULP_START_FRAME) / GULP_FRAME_COUNT;
let mouth_opening = if (0.0..=1.0).contains(&gulp_fraction) {
(PI * gulp_fraction).sin()
} else {
0.0
};
Drive {
course: Course::Hover {
heading_offset_radians: SWING_RADIANS * loop_turn.sin(),
drift_pixels: [
-DRIFT_PIXELS[0] * loop_turn.sin(),
SINK_PIXELS - DRIFT_PIXELS[1] * (2.0 * loop_turn).sin(),
],
},
depth: DEEP_REST_DEPTH,
wave_phase_radians: STILL_POSE.wave_phase_radians + frame * TAU / WAVE_FRAME_COUNT,
wave_strength: WAVE_STRENGTH + WAVE_BREATH * (2.0 * loop_turn).sin(),
tail_spread_radians: TAIL_SPREAD_RADIANS,
stroke_opening_radians: SIDE_FIN_OPENING_RADIANS,
right_fin_offset_radians: FLICK_RADIANS * flick,
feeding_phase: None,
mouth_opening,
}
}
fn calculate_beat(frame: f64, cycle_frame_count: f64, beat_share: f64) -> f64 {
let cycle_fraction = frame.rem_euclid(cycle_frame_count) / cycle_frame_count;
if cycle_fraction < beat_share {
(PI * cycle_fraction / beat_share).sin()
} else {
0.0
}
}
impl LapStyle {
fn calculate_cycle_fraction(&self, frame: f64) -> f64 {
frame.rem_euclid(self.stroke_frame_count) / self.stroke_frame_count
}
fn calculate_stroke(&self, frame: f64) -> f64 {
calculate_beat(frame, self.stroke_frame_count, self.stroke_share)
}
fn calculate_drive(&self, loop_frame: u32) -> Drive {
let lap_speeds: Vec<f64> = (0..self.loop_frame_count)
.map(|speed_frame| {
self.glide_speed
+ self.stroke_speed_gain
* self.calculate_stroke(f64::from(speed_frame) - self.speed_lag_frames)
})
.collect();
let total_speed: f64 = lap_speeds.iter().sum();
let covered_speed: f64 = lap_speeds[..loop_frame as usize].iter().sum();
let frame = f64::from(loop_frame);
let stroke = self.calculate_stroke(frame);
let wave_phase_radians =
STILL_POSE.wave_phase_radians + frame * TAU / self.wave_frame_count;
let scull = self.scull_swing_radians
* (TAU * frame / self.scull_frame_count + self.scull_phase_radians).sin();
let breath = (TAU * BREATHS_PER_LOOP * frame / f64::from(self.loop_frame_count))
.sin()
.max(0.0);
Drive {
course: Course::Lap {
angle_radians: -PI / 2.0 + TAU * self.lap_count * covered_speed / total_speed,
},
depth: 0.0,
wave_phase_radians,
wave_strength: self.glide_wave_strength + self.stroke_wave_gain * stroke,
tail_spread_radians: self.glide_tail_spread_radians
+ self.stroke_tail_spread_radians * stroke * wave_phase_radians.sin().abs(),
stroke_opening_radians: self.calculate_stroke_opening(frame),
right_fin_offset_radians: self.inside_flare_radians + scull,
feeding_phase: None,
mouth_opening: BREATH_OPENING * breath * breath,
}
}
fn calculate_stroke_opening(&self, frame: f64) -> f64 {
match self.side_fin_strokes {
SideFinStrokes::Brake {
stroke_opening_radians,
brake_opening_radians,
} => {
stroke_opening_radians
+ brake_opening_radians * (1.0 - self.calculate_stroke(frame))
}
SideFinStrokes::TuckAndSpread {
tuck_radians,
spread_radians,
} => {
let cycle_fraction = self.calculate_cycle_fraction(frame);
if cycle_fraction < self.stroke_share {
-tuck_radians * self.calculate_stroke(frame)
} else {
let glide_fraction =
(cycle_fraction - self.stroke_share) / (1.0 - self.stroke_share);
spread_radians * (PI * glide_fraction).sin()
}
}
}
}
}