use crate::ecs::primitives::EasingFunction;
use nalgebra_glm::{Quat, Vec3};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CutsceneShot {
pub eye: Vec3,
pub target: Vec3,
pub field_of_view_degrees: f32,
pub roll_degrees: f32,
}
impl CutsceneShot {
pub fn new(eye: Vec3, target: Vec3) -> Self {
Self {
eye,
target,
field_of_view_degrees: 45.0,
roll_degrees: 0.0,
}
}
pub fn with_field_of_view(mut self, field_of_view_degrees: f32) -> Self {
self.field_of_view_degrees = field_of_view_degrees;
self
}
pub fn with_roll(mut self, roll_degrees: f32) -> Self {
self.roll_degrees = roll_degrees;
self
}
pub fn interpolate(self, other: CutsceneShot, factor: f32) -> CutsceneShot {
CutsceneShot {
eye: self.eye + (other.eye - self.eye) * factor,
target: self.target + (other.target - self.target) * factor,
field_of_view_degrees: self.field_of_view_degrees
+ (other.field_of_view_degrees - self.field_of_view_degrees) * factor,
roll_degrees: self.roll_degrees + (other.roll_degrees - self.roll_degrees) * factor,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum CutsceneAction {
CameraMove {
from: CutsceneShot,
to: CutsceneShot,
},
CameraFollow {
actor: String,
eye_offset: Vec3,
look_height: f32,
field_of_view_degrees: f32,
},
CameraShake {
amplitude: f32,
frequency: f32,
},
ActorMove {
actor: String,
from: Vec3,
to: Vec3,
},
ActorTurn {
actor: String,
from_yaw_radians: f32,
to_yaw_radians: f32,
},
ActorVisible {
actor: String,
visible: bool,
},
Dialogue {
speaker: Option<String>,
line: String,
},
Title {
line: String,
},
Fade {
from: f32,
to: f32,
color: Vec3,
},
Letterbox {
from: f32,
to: f32,
},
FocusPull {
from_distance: f32,
to_distance: f32,
range: f32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct CutsceneEvent {
pub start: f32,
pub duration: f32,
pub easing: EasingFunction,
pub action: CutsceneAction,
}
impl CutsceneEvent {
pub fn end(&self) -> f32 {
self.start + self.duration
}
pub fn eased_progress(&self, time: f32) -> f32 {
if self.duration <= 0.0 {
return if time >= self.start { 1.0 } else { 0.0 };
}
let raw = ((time - self.start) / self.duration).clamp(0.0, 1.0);
self.easing.evaluate(raw)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Cutscene {
pub name: String,
pub events: Vec<CutsceneEvent>,
}
impl Cutscene {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
events: Vec::new(),
}
}
pub fn duration(&self) -> f32 {
self.events
.iter()
.map(CutsceneEvent::end)
.fold(0.0, f32::max)
}
fn push(
mut self,
start: f32,
duration: f32,
easing: EasingFunction,
action: CutsceneAction,
) -> Self {
self.events.push(CutsceneEvent {
start,
duration,
easing,
action,
});
self
}
pub fn camera(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: CutsceneShot,
to: CutsceneShot,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::CameraMove { from, to },
)
}
pub fn camera_follow(
self,
start: f32,
duration: f32,
actor: impl Into<String>,
eye_offset: Vec3,
look_height: f32,
field_of_view_degrees: f32,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::CameraFollow {
actor: actor.into(),
eye_offset,
look_height,
field_of_view_degrees,
},
)
}
pub fn camera_shake(self, start: f32, duration: f32, amplitude: f32, frequency: f32) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::CameraShake {
amplitude,
frequency,
},
)
}
pub fn camera_cut(self, start: f32, shot: CutsceneShot) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::CameraMove {
from: shot,
to: shot,
},
)
}
pub fn actor_move(
self,
actor: impl Into<String>,
start: f32,
duration: f32,
easing: EasingFunction,
from: Vec3,
to: Vec3,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::ActorMove {
actor: actor.into(),
from,
to,
},
)
}
pub fn actor_turn(
self,
actor: impl Into<String>,
start: f32,
duration: f32,
easing: EasingFunction,
from_yaw_radians: f32,
to_yaw_radians: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::ActorTurn {
actor: actor.into(),
from_yaw_radians,
to_yaw_radians,
},
)
}
pub fn actor_visible(self, actor: impl Into<String>, time: f32, visible: bool) -> Self {
self.push(
time,
0.0,
EasingFunction::Linear,
CutsceneAction::ActorVisible {
actor: actor.into(),
visible,
},
)
}
pub fn dialogue(
self,
start: f32,
duration: f32,
speaker: Option<&str>,
line: impl Into<String>,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::Dialogue {
speaker: speaker.map(str::to_string),
line: line.into(),
},
)
}
pub fn title(self, start: f32, duration: f32, line: impl Into<String>) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::Title { line: line.into() },
)
}
pub fn fade(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: f32,
to: f32,
color: Vec3,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::Fade { from, to, color },
)
}
pub fn fade_in(self, start: f32, duration: f32) -> Self {
self.fade(
start,
duration,
EasingFunction::QuadOut,
1.0,
0.0,
Vec3::zeros(),
)
}
pub fn fade_out(self, start: f32, duration: f32) -> Self {
self.fade(
start,
duration,
EasingFunction::QuadIn,
0.0,
1.0,
Vec3::zeros(),
)
}
pub fn letterbox(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: f32,
to: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::Letterbox { from, to },
)
}
pub fn letterbox_in(self, start: f32, duration: f32) -> Self {
self.letterbox(start, duration, EasingFunction::CubicOut, 0.0, 1.0)
}
pub fn letterbox_out(self, start: f32, duration: f32) -> Self {
self.letterbox(start, duration, EasingFunction::CubicIn, 1.0, 0.0)
}
pub fn focus_pull(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from_distance: f32,
to_distance: f32,
range: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::FocusPull {
from_distance,
to_distance,
range,
},
)
}
}
pub fn camera_look_rotation(eye: Vec3, target: Vec3, roll_degrees: f32) -> Quat {
let direction = target - eye;
if direction.magnitude_squared() < f32::EPSILON {
return Quat::identity();
}
let direction = direction.normalize();
let yaw = (-direction.x).atan2(-direction.z);
let pitch = (-direction.y).clamp(-1.0, 1.0).asin();
let yaw_rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y());
let pitch_rotation = nalgebra_glm::quat_angle_axis(-pitch, &Vec3::x());
let look = yaw_rotation * pitch_rotation;
if roll_degrees.abs() < f32::EPSILON {
return look;
}
let roll = nalgebra_glm::quat_angle_axis(roll_degrees.to_radians(), &direction);
roll * look
}