///|
/// Easing curves for timeline and motion interpolation.
pub(all) enum Easing {
Linear
Step
EaseIn
EaseOut
EaseInOut
SmoothStep
SineLike
Bounce
Pulse
} derive(Eq, Debug)
///|
pub fn Easing::sample(self : Easing, numerator : Int, denominator : Int) -> Int {
if denominator <= 0 {
1000
} else {
let t = if numerator < 0 {
0
} else if numerator > denominator {
denominator
} else {
numerator
}
let unit = t * 1000 / denominator
match self {
Linear => unit
Step => if unit < 1000 { 0 } else { 1000 }
EaseIn => unit * unit / 1000
EaseOut => 1000 - (1000 - unit) * (1000 - unit) / 1000
EaseInOut =>
if unit < 500 {
2 * unit * unit / 1000
} else {
1000 - 2 * (1000 - unit) * (1000 - unit) / 1000
}
SmoothStep => {
let squared = unit * unit / 1000
squared * (3000 - 2 * unit) / 1000
}
SineLike =>
if unit < 500 {
unit * unit * 2 / 1000
} else {
1000 - (1000 - unit) * (1000 - unit) * 2 / 1000
}
Bounce => if unit < 500 { unit * 2 } else { 1000 - (unit - 500) * 2 }
Pulse => if unit < 500 { unit * 2 } else { (1000 - unit) * 2 }
}
}
}
///|
/// Position, scale, rotation, and opacity values sampled along a track.
pub(all) struct MotionKeyframe {
time_cs : Int
x : Int
y : Int
scale_per_mille : Int
rotation : CanvasRotation
opacity : Byte
} derive(Debug)
///|
pub fn MotionKeyframe::new(
time_cs~ : Int,
x~ : Int,
y~ : Int,
scale_per_mille~ : Int,
rotation~ : CanvasRotation,
opacity~ : Byte,
) -> MotionKeyframe {
{
time_cs,
x,
y,
scale_per_mille: if scale_per_mille < 0 {
0
} else {
scale_per_mille
},
rotation,
opacity,
}
}
///|
pub fn MotionKeyframe::time(self : MotionKeyframe) -> Int {
self.time_cs
}
///|
pub fn MotionKeyframe::position(self : MotionKeyframe) -> (Int, Int) {
(self.x, self.y)
}
///|
pub fn MotionKeyframe::scale(self : MotionKeyframe) -> Int {
self.scale_per_mille
}
///|
pub fn MotionKeyframe::rotation(self : MotionKeyframe) -> CanvasRotation {
self.rotation
}
///|
pub fn MotionKeyframe::opacity(self : MotionKeyframe) -> Byte {
self.opacity
}
///|
pub(all) struct MotionSample {
x : Int
y : Int
scale_per_mille : Int
opacity : Byte
rotation : CanvasRotation
} derive(Debug)
///|
pub fn MotionSample::position(self : MotionSample) -> (Int, Int) {
(self.x, self.y)
}
///|
pub fn MotionSample::scale(self : MotionSample) -> Int {
self.scale_per_mille
}
///|
pub fn MotionSample::opacity(self : MotionSample) -> Byte {
self.opacity
}
///|
pub fn MotionSample::rotation(self : MotionSample) -> CanvasRotation {
self.rotation
}
///|
/// A sorted motion track. Keys added out of order are inserted into timeline
/// order, making it safe for editors to build tracks incrementally.
pub(all) struct MotionTrack {
keys : Array[MotionKeyframe]
easing : Easing
} derive(Debug)
///|
pub fn MotionTrack::new(easing~ : Easing) -> MotionTrack {
{ keys: [], easing }
}
///|
pub fn MotionTrack::keys(self : MotionTrack) -> ArrayView[MotionKeyframe] {
self.keys[:]
}
///|
pub fn MotionTrack::easing(self : MotionTrack) -> Easing {
self.easing
}
///|
pub fn MotionTrack::add(
self : MotionTrack,
key : MotionKeyframe,
) -> MotionTrack {
let mut index = self.keys.length()
for i, existing in self.keys {
if key.time() < existing.time() {
index = i
break
}
}
self.keys.push(key)
if index < self.keys.length() - 1 {
let mut i = self.keys.length() - 1
while i > index {
self.keys[i] = self.keys[i - 1]
i -= 1
}
self.keys[index] = key
}
self
}
///|
fn interpolate_int(left : Int, right : Int, amount : Int) -> Int {
left + (right - left) * amount / 1000
}
///|
fn interpolate_rotation(
left : CanvasRotation,
right : CanvasRotation,
amount : Int,
) -> CanvasRotation {
if amount < 500 {
left
} else {
right
}
}
///|
pub fn MotionTrack::sample(self : MotionTrack, time_cs : Int) -> MotionSample {
if self.keys.length() == 0 {
MotionSample::{
x: 0,
y: 0,
scale_per_mille: 1000,
opacity: 255,
rotation: Clockwise90,
}
} else if self.keys.length() == 1 || time_cs <= self.keys[0].time() {
let key = self.keys[0]
MotionSample::{
x: key.x,
y: key.y,
scale_per_mille: key.scale_per_mille,
opacity: key.opacity,
rotation: key.rotation,
}
} else {
let last = self.keys[self.keys.length() - 1]
if time_cs >= last.time() {
MotionSample::{
x: last.x,
y: last.y,
scale_per_mille: last.scale_per_mille,
opacity: last.opacity,
rotation: last.rotation,
}
} else {
let mut left = self.keys[0]
let mut right = last
for i in 1.. Canvas {
if scale_per_mille <= 0 {
Canvas::new(width=1, height=1, fill=rgba(0, 0, 0, 0))
} else {
let width = if self.width * scale_per_mille / 1000 < 1 {
1
} else {
self.width * scale_per_mille / 1000
}
let height = if self.height * scale_per_mille / 1000 < 1 {
1
} else {
self.height * scale_per_mille / 1000
}
self.resize_nearest(width~, height~)
}
}
///|
fn place_canvas(
background : Canvas,
source : Canvas,
left : Int,
top : Int,
opacity : Byte,
) -> Canvas {
let result = background.copy()
let layer = if opacity == 255 { source } else { source.scale_alpha(opacity) }
ignore(result.blit(layer, destination_x=left, destination_y=top))
result
}
///|
fn apply_motion_sample(
animation : Animation,
frame : Frame,
sample : MotionSample,
) -> Frame {
let rotated = frame.canvas.rotate(sample.rotation)
let scaled = rotated.scale_centered(sample.scale_per_mille)
let background = Canvas::new(
width=animation.width,
height=animation.height,
fill=animation.background,
)
let left = sample.x - scaled.width / 2
let top = sample.y - scaled.height / 2
let placed = place_canvas(background, scaled, left, top, sample.opacity)
Frame::new(canvas=placed, delay_cs=frame.delay_cs()).with_disposal(
disposal=frame.disposal(),
)
}
///|
/// Apply a motion track to each frame at its timeline start time.
pub fn Animation::apply_motion(
self : Animation,
track : MotionTrack,
) -> Animation {
let result : Array[Frame] = []
let starts = self.timeline_starts()
for i, frame in self.frames {
result.push(apply_motion_sample(self, frame, track.sample(starts[i])))
}
Animation::{ ..self, frames: result }
}
///|
/// Generate a moving copy of an animation between two positions.
pub fn Animation::move_between(
self : Animation,
from~ : (Int, Int),
to~ : (Int, Int),
easing~ : Easing,
) -> Animation {
let track = MotionTrack::new(easing~)
.add(
MotionKeyframe::new(
time_cs=0,
x=from.0,
y=from.1,
scale_per_mille=1000,
rotation=Clockwise90,
opacity=255,
),
)
.add(
MotionKeyframe::new(
time_cs=if self.duration_cs() <= 0 { 1 } else { self.duration_cs() },
x=to.0,
y=to.1,
scale_per_mille=1000,
rotation=Clockwise90,
opacity=255,
),
)
self.apply_motion(track)
}
///|
/// Add a pulsing opacity effect without changing frame dimensions.
pub fn Animation::pulse(
self : Animation,
minimum_alpha~ : Byte,
easing~ : Easing,
) -> Animation {
let result : Array[Frame] = []
let duration = if self.duration_cs() <= 0 { 1 } else { self.duration_cs() }
for start, frame in self.frames {
let time = self.timeline_starts()[start]
let phase = time % duration
let amount = easing.sample(phase, duration)
let alpha = minimum_alpha.to_int() +
(255 - minimum_alpha.to_int()) * amount / 1000
result.push(frame.with_canvas(frame.canvas().scale_alpha(alpha.to_byte())))
}
Animation::{ ..self, frames: result }
}
///|
/// Repeat each frame with alternating opacity to create a reliable blink.
pub fn Animation::blink(
self : Animation,
visible_copies : Int,
hidden_copies : Int,
) -> Animation {
let visible = if visible_copies < 1 { 1 } else { visible_copies }
let hidden = if hidden_copies < 0 { 0 } else { hidden_copies }
let result : Array[Frame] = []
for frame in self.frames {
for _ in 0.. Animation {
let safe_radius = if radius < 0 { 0 } else { radius }
let result : Array[Frame] = []
let mut state = seed
for frame in self.frames {
state = (state * 1103515245 + 12345) & 0x7fffffff
let x = if safe_radius == 0 {
0
} else {
state % (safe_radius * 2 + 1) - safe_radius
}
state = (state * 1103515245 + 12345) & 0x7fffffff
let y = if safe_radius == 0 {
0
} else {
state % (safe_radius * 2 + 1) - safe_radius
}
let background = Canvas::new(
width=self.width,
height=self.height,
fill=self.background,
)
let shaken = place_canvas(background, frame.canvas(), x, y, 255)
result.push(
Frame::new(canvas=shaken, delay_cs=frame.delay_cs()).with_disposal(
disposal=frame.disposal(),
),
)
}
Animation::{ ..self, frames: result }
}
///|
/// Add a zoom-in or zoom-out camera move around the logical screen center.
pub fn Animation::zoom(
self : Animation,
from_per_mille~ : Int,
to_per_mille~ : Int,
easing~ : Easing,
) -> Animation {
let result : Array[Frame] = []
let duration = if self.duration_cs() <= 0 { 1 } else { self.duration_cs() }
let starts = self.timeline_starts()
for i, frame in self.frames {
let amount = easing.sample(starts[i], duration)
let scale = interpolate_int(from_per_mille, to_per_mille, amount)
let centered = frame.canvas().scale_centered(scale)
let background = Canvas::new(
width=self.width,
height=self.height,
fill=self.background,
)
let placed = place_canvas(
background,
centered,
(self.width - centered.width) / 2,
(self.height - centered.height) / 2,
255,
)
result.push(
Frame::new(canvas=placed, delay_cs=frame.delay_cs()).with_disposal(
disposal=frame.disposal(),
),
)
}
Animation::{ ..self, frames: result }
}
///|
/// Create a frame-by-frame color wipe from one edge of the logical screen.
pub fn Animation::wipe(
self : Animation,
direction : Int,
color : Rgba,
) -> Animation {
let result : Array[Frame] = []
for i, frame in self.frames {
let progress = (i + 1) * 1000 / self.frames.length()
let canvas = frame.canvas().copy()
let width = self.width * progress / 1000
let height = self.height * progress / 1000
if direction % 4 == 0 {
ignore(
canvas.fill_rect(
x=0,
y=0,
width=self.width - width,
height=self.height,
color~,
),
)
} else if direction % 4 == 1 {
ignore(
canvas.fill_rect(
x=width,
y=0,
width=self.width - width,
height=self.height,
color~,
),
)
} else if direction % 4 == 2 {
ignore(
canvas.fill_rect(
x=0,
y=0,
width=self.width,
height=self.height - height,
color~,
),
)
} else {
ignore(
canvas.fill_rect(
x=0,
y=height,
width=self.width,
height=self.height - height,
color~,
),
)
}
result.push(Frame::new(canvas~, delay_cs=frame.delay_cs()))
}
Animation::{ ..self, frames: result }
}
///|
/// Create intermediate frames between two canvases using an easing curve.
pub fn interpolate_canvases(
first : Canvas,
second : Canvas,
count : Int,
easing~ : Easing,
) -> Array[Canvas] {
if first.width != second.width || first.height != second.height || count <= 0 {
[]
} else {
let result : Array[Canvas] = []
for index in 0.. Animation {
if between <= 0 || self.frames.length() <= 1 {
self
} else {
let result : Array[Frame] = []
for i in 0.. Animation {
let result : Array[Frame] = []
for frame in self.frames {
result.push(
Frame::new(
canvas=frame.canvas().adjust(effect),
delay_cs=frame.delay_cs(),
).with_disposal(disposal=frame.disposal()),
)
}
Animation::{ ..self, frames: result }
}