///|
/// Direction of a transport clock.
pub(all) enum PlaybackDirection {
Forward
Reverse
} derive(Eq, Debug)
///|
pub(all) enum TransportStatus {
Stopped
Playing
Paused
Finished
} derive(Eq, Debug)
///|
/// A transport window with explicit in/out points for media scrubbing.
pub struct TransportRange {
start : Double
end : Double
} derive(Debug)
///|
pub fn transport_range(
start : Double,
end : Double,
) -> TransportRange raise MotionError {
ensure_finite(start)
ensure_finite(end)
if end <= start {
raise MotionError::InvalidDuration(end - start)
}
{ start, end }
}
///|
pub fn TransportRange::start(self : TransportRange) -> Double {
self.start
}
///|
pub fn TransportRange::end(self : TransportRange) -> Double {
self.end
}
///|
pub fn TransportRange::duration(self : TransportRange) -> Double {
self.end - self.start
}
///|
pub fn TransportRange::contains(self : TransportRange, time : Double) -> Bool {
time >= self.start && time <= self.end
}
///|
pub fn TransportRange::clamp(self : TransportRange, time : Double) -> Double {
time.max(self.start).min(self.end)
}
///|
pub fn TransportRange::normalized(
self : TransportRange,
time : Double,
) -> Double {
clamp01((time - self.start) / self.duration())
}
///|
/// A stateful transport clock for preview, timeline scrubbing, and export.
pub struct MotionTransport {
mut position : Double
mut speed : Double
mut status : TransportStatus
mut direction : PlaybackDirection
range : TransportRange
markers : MarkerTimeline
} derive(Debug)
///|
pub fn MotionTransport::new(
range : TransportRange,
markers? : MarkerTimeline = MarkerTimeline::empty(),
) -> MotionTransport {
{
position: range.start,
speed: 1.0,
status: Stopped,
direction: Forward,
range,
markers,
}
}
///|
pub fn MotionTransport::position(self : MotionTransport) -> Double {
self.position
}
///|
pub fn MotionTransport::speed(self : MotionTransport) -> Double {
self.speed
}
///|
pub fn MotionTransport::status(self : MotionTransport) -> TransportStatus {
self.status
}
///|
pub fn MotionTransport::direction(self : MotionTransport) -> PlaybackDirection {
self.direction
}
///|
pub fn MotionTransport::range(self : MotionTransport) -> TransportRange {
self.range
}
///|
pub fn MotionTransport::markers(self : MotionTransport) -> MarkerTimeline {
self.markers
}
///|
pub fn MotionTransport::set_speed(
self : MotionTransport,
speed : Double,
) -> Unit raise MotionError {
ensure_finite(speed)
if speed < 0.0 {
raise MotionError::InvalidTime(speed)
}
self.speed = speed
}
///|
pub fn MotionTransport::set_direction(
self : MotionTransport,
direction : PlaybackDirection,
) -> Unit {
self.direction = direction
}
///|
pub fn MotionTransport::play(self : MotionTransport) -> Unit {
self.status = Playing
}
///|
pub fn MotionTransport::pause(self : MotionTransport) -> Unit {
if self.status is Playing {
self.status = Paused
}
}
///|
pub fn MotionTransport::stop(self : MotionTransport) -> Unit {
self.position = match self.direction {
Forward => self.range.start
Reverse => self.range.end
}
self.status = Stopped
}
///|
pub fn MotionTransport::seek(
self : MotionTransport,
position : Double,
) -> Unit raise MotionError {
ensure_finite(position)
self.position = self.range.clamp(position)
self.status = Paused
}
///|
pub fn MotionTransport::rewind(self : MotionTransport) -> Unit {
self.position = self.range.start
self.direction = Forward
self.status = Paused
}
///|
pub fn MotionTransport::fast_forward(self : MotionTransport) -> Unit {
self.position = self.range.end
self.direction = Reverse
self.status = Paused
}
///|
/// Advance the clock and return markers crossed by this step.
pub fn MotionTransport::advance(
self : MotionTransport,
delta_seconds : Double,
) -> Array[MarkerHit] raise MotionError {
ensure_finite(delta_seconds)
if delta_seconds < 0.0 {
raise MotionError::InvalidTime(delta_seconds)
}
if !(self.status is Playing) || delta_seconds == 0.0 {
return []
}
let signed_delta = match self.direction {
Forward => delta_seconds * self.speed
Reverse => -delta_seconds * self.speed
}
let next = self.position + signed_delta
let clamped = self.range.clamp(next)
let hits = self.markers.between(self.position, clamped)
self.position = clamped
if next < self.range.start || next > self.range.end {
self.status = Finished
}
hits
}
///|
pub fn MotionTransport::seek_and_collect(
self : MotionTransport,
position : Double,
) -> Array[MarkerHit] raise MotionError {
ensure_finite(position)
let target = self.range.clamp(position)
let result = self.markers.between(self.position, target)
self.position = target
result
}
///|
pub fn MotionTransport::normalized_position(self : MotionTransport) -> Double {
self.range.normalized(self.position)
}
///|
pub fn MotionTransport::remaining(self : MotionTransport) -> Double {
match self.direction {
Forward => self.range.end - self.position
Reverse => self.position - self.range.start
}
}
///|
pub fn MotionTransport::elapsed(self : MotionTransport) -> Double {
match self.direction {
Forward => self.position - self.range.start
Reverse => self.range.end - self.position
}
}
///|
pub fn MotionTransport::is_at_boundary(self : MotionTransport) -> Bool {
self.position == self.range.start || self.position == self.range.end
}
///|
pub fn MotionTransport::clone_at(
self : MotionTransport,
position : Double,
) -> MotionTransport {
let copy = MotionTransport::new(self.range, markers=self.markers)
copy.position = self.range.clamp(position)
copy.speed = self.speed
copy.status = self.status
copy.direction = self.direction
copy
}
///|
/// A fixed-rate export iterator that emits frame times and marker events.
pub struct TransportFrame {
index : Int
time : Double
normalized : Double
marker_hits : Array[MarkerHit]
} derive(Debug)
///|
pub fn TransportFrame::index(self : TransportFrame) -> Int {
self.index
}
///|
pub fn TransportFrame::time(self : TransportFrame) -> Double {
self.time
}
///|
pub fn TransportFrame::normalized(self : TransportFrame) -> Double {
self.normalized
}
///|
pub fn TransportFrame::marker_hits(self : TransportFrame) -> Array[MarkerHit] {
self.marker_hits.copy()
}
///|
pub fn transport_frames(
range : TransportRange,
frame_rate : Double,
markers? : MarkerTimeline = MarkerTimeline::empty(),
) -> Array[TransportFrame] raise MotionError {
if frame_rate <= 0.0 || frame_rate.is_nan() || frame_rate.is_inf() {
raise MotionError::InvalidFrameRate(frame_rate)
}
let count = @math.floor(range.duration() * frame_rate).to_int() + 1
let result : Array[TransportFrame] = []
let mut previous = range.start
for index in 0.. Int {
if frame_rate <= 0.0 {
0
} else {
@math.floor(range.duration() * frame_rate).to_int() + 1
}
}
///|
pub fn transport_time_at_frame(
range : TransportRange,
frame_rate : Double,
frame : Int,
) -> Double {
if frame <= 0 || frame_rate <= 0.0 {
range.start
} else {
(range.start + frame.to_double() / frame_rate).min(range.end)
}
}
///|
pub fn transport_progress(range : TransportRange, time : Double) -> Double {
range.normalized(time)
}