// =============================================================================
// Helper functions
// =============================================================================
///|
pub fn min(a : Double, b : Double) -> Double {
if a < b {
a
} else {
b
}
}
///|
pub fn max(a : Double, b : Double) -> Double {
if a > b {
a
} else {
b
}
}
// =============================================================================
// Core Types
// =============================================================================
///|
/// 2D size with width and height
pub struct Size[T] {
width : T
height : T
}
///|
pub fn[T] Size::new(width : T, height : T) -> Size[T] {
{ width, height }
}
///|
pub impl[T : Show] Show for Size[T] with fn output(self, logger) {
logger.write_string("Size { width: ")
self.width.output(logger)
logger.write_string(", height: ")
self.height.output(logger)
logger.write_string(" }")
}
///|
pub fn Size::zero() -> Size[Double] {
{ width: 0.0, height: 0.0 }
}
///|
pub fn[T, U] Size::map(self : Size[T], f : (T) -> U) -> Size[U] {
{ width: f(self.width), height: f(self.height) }
}
///|
/// 2D point with x and y coordinates
pub struct Point[T] {
x : T
y : T
}
///|
pub fn[T] Point::new(x : T, y : T) -> Point[T] {
{ x, y }
}
///|
pub impl[T : Show] Show for Point[T] with fn output(self, logger) {
logger.write_string("Point { x: ")
self.x.output(logger)
logger.write_string(", y: ")
self.y.output(logger)
logger.write_string(" }")
}
///|
pub fn Point::zero() -> Point[Double] {
{ x: 0.0, y: 0.0 }
}
///|
pub fn[T, U] Point::map(self : Point[T], f : (T) -> U) -> Point[U] {
{ x: f(self.x), y: f(self.y) }
}
///|
/// Rectangle with left, right, top, bottom edges
/// Used for margin, padding, border, inset
pub(all) struct Rect[T] {
left : T
right : T
top : T
bottom : T
} derive(Debug, Eq)
///|
pub fn[T] Rect::new(left : T, right : T, top : T, bottom : T) -> Rect[T] {
{ left, right, top, bottom }
}
///|
pub impl[T : Show] Show for Rect[T] with fn output(self, logger) {
logger.write_string("Rect { left: ")
self.left.output(logger)
logger.write_string(", right: ")
self.right.output(logger)
logger.write_string(", top: ")
self.top.output(logger)
logger.write_string(", bottom: ")
self.bottom.output(logger)
logger.write_string(" }")
}
///|
pub fn Rect::zero() -> Rect[Double] {
{ left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 }
}
///|
/// Create a rect with all sides set to the same value
pub fn[T] Rect::all(value : T) -> Rect[T] {
{ left: value, right: value, top: value, bottom: value }
}
///|
pub fn[T, U] Rect::map(self : Rect[T], f : (T) -> U) -> Rect[U] {
{
left: f(self.left),
right: f(self.right),
top: f(self.top),
bottom: f(self.bottom),
}
}
///|
/// Sum of left and right (horizontal axis)
pub fn Rect::horizontal_sum(self : Rect[Double]) -> Double {
self.left + self.right
}
///|
/// Sum of top and bottom (vertical axis)
pub fn Rect::vertical_sum(self : Rect[Double]) -> Double {
self.top + self.bottom
}
///|
/// A line segment with start and end points
pub struct Line[T] {
start : T
end : T
}
///|
pub fn[T] Line::new(start : T, end : T) -> Line[T] {
{ start, end }
}
///|
pub impl[T : Show] Show for Line[T] with fn output(self, logger) {
logger.write_string("Line { start: ")
self.start.output(logger)
logger.write_string(", end: ")
self.end.output(logger)
logger.write_string(" }")
}
// =============================================================================
// Easing
// =============================================================================
///|
/// Step timing function jump position.
pub(all) enum StepPosition {
Start
End
JumpNone
JumpBoth
} derive(Debug, Eq)
///|
pub impl Show for StepPosition with fn output(self, logger) {
match self {
StepPosition::Start => logger.write_string("Start")
StepPosition::End => logger.write_string("End")
StepPosition::JumpNone => logger.write_string("JumpNone")
StepPosition::JumpBoth => logger.write_string("JumpBoth")
}
}
///|
/// One point in a CSS `linear()` easing function.
pub(all) struct LinearEasingPoint {
input : Double
output : Double
} derive(Debug, Eq)
///|
pub impl Show for LinearEasingPoint with fn output(self, logger) {
logger.write_string("LinearEasingPoint { input: ")
self.input.output(logger)
logger.write_string(", output: ")
self.output.output(logger)
logger.write_string(" }")
}
///|
pub fn LinearEasingPoint::new(
input : Double,
output : Double,
) -> LinearEasingPoint {
{ input, output }
}
///|
/// CSS easing/timing-function value without binding it to transitions or
/// animations. Consumers can sample it against normalized progress `[0, 1]`.
pub(all) enum Easing {
Linear
Ease
EaseIn
EaseOut
EaseInOut
EaseInSine
EaseOutSine
EaseInOutSine
EaseInQuad
EaseOutQuad
EaseInOutQuad
EaseInCubic
EaseOutCubic
EaseInOutCubic
EaseInQuart
EaseOutQuart
EaseInOutQuart
EaseInQuint
EaseOutQuint
EaseInOutQuint
EaseInExpo
EaseOutExpo
EaseInOutExpo
EaseInCirc
EaseOutCirc
EaseInOutCirc
EaseInBack
EaseOutBack
EaseInOutBack
EaseInElastic
EaseOutElastic
EaseInOutElastic
EaseInBounce
EaseOutBounce
EaseInOutBounce
LinearFunction(Array[LinearEasingPoint])
CubicBezier(Double, Double, Double, Double)
Steps(Int, StepPosition)
} derive(Debug, Eq)
///|
/// Eased value at one point in an interpolation sequence.
pub(all) struct EasingFrame {
progress : Double
eased_progress : Double
value : Double
}
///|
pub impl Show for EasingFrame with fn output(self, logger) {
logger.write_string("EasingFrame { progress: ")
self.progress.output(logger)
logger.write_string(", eased_progress: ")
self.eased_progress.output(logger)
logger.write_string(", value: ")
self.value.output(logger)
logger.write_string(" }")
}
///|
fn easing_variant_name(easing : Easing) -> String? {
match easing {
Linear => Some("Linear")
Ease => Some("Ease")
EaseIn => Some("EaseIn")
EaseOut => Some("EaseOut")
EaseInOut => Some("EaseInOut")
EaseInSine => Some("EaseInSine")
EaseOutSine => Some("EaseOutSine")
EaseInOutSine => Some("EaseInOutSine")
EaseInQuad => Some("EaseInQuad")
EaseOutQuad => Some("EaseOutQuad")
EaseInOutQuad => Some("EaseInOutQuad")
EaseInCubic => Some("EaseInCubic")
EaseOutCubic => Some("EaseOutCubic")
EaseInOutCubic => Some("EaseInOutCubic")
EaseInQuart => Some("EaseInQuart")
EaseOutQuart => Some("EaseOutQuart")
EaseInOutQuart => Some("EaseInOutQuart")
EaseInQuint => Some("EaseInQuint")
EaseOutQuint => Some("EaseOutQuint")
EaseInOutQuint => Some("EaseInOutQuint")
EaseInExpo => Some("EaseInExpo")
EaseOutExpo => Some("EaseOutExpo")
EaseInOutExpo => Some("EaseInOutExpo")
EaseInCirc => Some("EaseInCirc")
EaseOutCirc => Some("EaseOutCirc")
EaseInOutCirc => Some("EaseInOutCirc")
EaseInBack => Some("EaseInBack")
EaseOutBack => Some("EaseOutBack")
EaseInOutBack => Some("EaseInOutBack")
EaseInElastic => Some("EaseInElastic")
EaseOutElastic => Some("EaseOutElastic")
EaseInOutElastic => Some("EaseInOutElastic")
EaseInBounce => Some("EaseInBounce")
EaseOutBounce => Some("EaseOutBounce")
EaseInOutBounce => Some("EaseInOutBounce")
LinearFunction(_) | CubicBezier(_, _, _, _) | Steps(_, _) => None
}
}
///|
pub impl Show for Easing with fn output(self, logger) {
match easing_variant_name(self) {
Some(name) => logger.write_string(name)
None =>
match self {
LinearFunction(points) => {
logger.write_string("LinearFunction(")
logger.write_string("[")
for i = 0; i < points.length(); i = i + 1 {
if i > 0 {
logger.write_string(", ")
}
points[i].output(logger)
}
logger.write_string("]")
logger.write_string(")")
}
CubicBezier(x1, y1, x2, y2) => {
logger.write_string("CubicBezier(")
x1.output(logger)
logger.write_string(", ")
y1.output(logger)
logger.write_string(", ")
x2.output(logger)
logger.write_string(", ")
y2.output(logger)
logger.write_string(")")
}
Steps(count, position) => {
logger.write_string("Steps(")
count.output(logger)
logger.write_string(", ")
position.output(logger)
logger.write_string(")")
}
_ => ()
}
}
}
///|
pub fn Easing::cubic_bezier(
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
) -> Easing {
CubicBezier(x1, y1, x2, y2)
}
///|
pub fn Easing::steps(count : Int, position : StepPosition) -> Easing {
Steps(count, position)
}
///|
pub fn Easing::linear(points : Array[LinearEasingPoint]) -> Easing? {
if points.length() < 2 {
return None
}
for i = 1; i < points.length(); i = i + 1 {
if points[i].input < points[i - 1].input {
return None
}
}
Some(LinearFunction(points.copy()))
}
///|
fn apply_eased_progress(
start_value : Double,
end_value : Double,
eased_progress : Double,
) -> Double {
start_value + (end_value - start_value) * eased_progress
}
///|
fn clamp_unit(value : Double) -> Double {
if value < 0.0 {
0.0
} else if value > 1.0 {
1.0
} else {
value
}
}
///|
fn cubic_bezier_axis(t : Double, p1 : Double, p2 : Double) -> Double {
let inv = 1.0 - t
3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t
}
///|
fn cubic_bezier_axis_derivative(t : Double, p1 : Double, p2 : Double) -> Double {
let inv = 1.0 - t
3.0 * inv * inv * p1 + 6.0 * inv * t * (p2 - p1) + 3.0 * t * t * (1.0 - p2)
}
///|
fn solve_cubic_bezier_t(x : Double, x1 : Double, x2 : Double) -> Double {
let mut t = x
for _i = 0; _i < 8; _i = _i + 1 {
let x_at_t = cubic_bezier_axis(t, x1, x2)
let dx = x_at_t - x
if dx.abs() < 0.000001 {
return t
}
let d = cubic_bezier_axis_derivative(t, x1, x2)
if d.abs() < 0.000001 {
break
}
t = clamp_unit(t - dx / d)
}
let mut low = 0.0
let mut high = 1.0
t = x
for _i = 0; _i < 16; _i = _i + 1 {
t = (low + high) / 2.0
let x_at_t = cubic_bezier_axis(t, x1, x2)
if (x_at_t - x).abs() < 0.000001 {
return t
}
if x_at_t < x {
low = t
} else {
high = t
}
}
t
}
///|
fn pow(value : Double, exponent : Double) -> Double {
@math.pow(value, exponent)
}
///|
fn ease_in_out_power(t : Double, exponent : Double) -> Double {
if t < 0.5 {
pow(2.0 * t, exponent) / 2.0
} else {
(2.0 - pow(-2.0 * t + 2.0, exponent)) / 2.0
}
}
///|
fn ease_out_bounce(t : Double) -> Double {
let n1 = 7.5625
let d1 = 2.75
if t < 1.0 / d1 {
n1 * t * t
} else if t < 2.0 / d1 {
let x = t - 1.5 / d1
n1 * x * x + 0.75
} else if t < 2.5 / d1 {
let x = t - 2.25 / d1
n1 * x * x + 0.9375
} else {
let x = t - 2.625 / d1
n1 * x * x + 0.984375
}
}
///|
fn sample_linear_function(
points : Array[LinearEasingPoint],
t : Double,
) -> Double {
if points.length() < 2 {
return t
}
let mut point_a_index = 0
for i = 0; i < points.length(); i = i + 1 {
if points[i].input <= t {
point_a_index = i
}
}
if point_a_index == points.length() - 1 {
point_a_index = point_a_index - 1
}
let point_a = points[point_a_index]
let point_b = points[point_a_index + 1]
if point_a.input == point_b.input {
point_b.output
} else {
let progress_between_points = (t - point_a.input) /
(point_b.input - point_a.input)
point_a.output + progress_between_points * (point_b.output - point_a.output)
}
}
///|
fn sample_steps(count : Int, position : StepPosition, t : Double) -> Double {
if count <= 0 || (position == StepPosition::JumpNone && count <= 1) {
return t
}
let steps = count.to_double()
let mut current_step = (t * steps).floor()
match position {
StepPosition::Start | StepPosition::JumpBoth =>
current_step = current_step + 1.0
StepPosition::End | StepPosition::JumpNone => ()
}
if current_step < 0.0 {
current_step = 0.0
}
let jumps = match position {
StepPosition::JumpNone => count - 1
StepPosition::JumpBoth => count + 1
StepPosition::Start | StepPosition::End => count
}
let jumps = jumps.to_double()
if current_step > jumps {
current_step = jumps
}
current_step / jumps
}
///|
/// Sample the easing at normalized progress. Input progress is clamped to
/// `[0, 1]`; cubic-bezier output may overshoot if its Y control points do.
pub fn Easing::sample(self : Easing, progress : Double) -> Double {
let t = clamp_unit(progress)
let one_minus_t = 1.0 - t
match self {
Linear => t
Ease => CubicBezier(0.25, 0.1, 0.25, 1.0).sample(t)
EaseIn => CubicBezier(0.42, 0.0, 1.0, 1.0).sample(t)
EaseOut => CubicBezier(0.0, 0.0, 0.58, 1.0).sample(t)
EaseInOut => CubicBezier(0.42, 0.0, 0.58, 1.0).sample(t)
EaseInSine => 1.0 - @math.cos(t * @math.PI / 2.0)
EaseOutSine => @math.sin(t * @math.PI / 2.0)
EaseInOutSine => -(@math.cos(@math.PI * t) - 1.0) / 2.0
EaseInQuad => t * t
EaseOutQuad => 1.0 - one_minus_t * one_minus_t
EaseInOutQuad => ease_in_out_power(t, 2.0)
EaseInCubic => t * t * t
EaseOutCubic => 1.0 - one_minus_t * one_minus_t * one_minus_t
EaseInOutCubic => ease_in_out_power(t, 3.0)
EaseInQuart => t * t * t * t
EaseOutQuart => 1.0 - pow(one_minus_t, 4.0)
EaseInOutQuart => ease_in_out_power(t, 4.0)
EaseInQuint => t * t * t * t * t
EaseOutQuint => 1.0 - pow(one_minus_t, 5.0)
EaseInOutQuint => ease_in_out_power(t, 5.0)
EaseInExpo => if t == 0.0 { 0.0 } else { pow(2.0, 10.0 * t - 10.0) }
EaseOutExpo => if t == 1.0 { 1.0 } else { 1.0 - pow(2.0, -10.0 * t) }
EaseInOutExpo =>
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else if t < 0.5 {
pow(2.0, 20.0 * t - 10.0) / 2.0
} else {
(2.0 - pow(2.0, -20.0 * t + 10.0)) / 2.0
}
EaseInCirc => 1.0 - (1.0 - t * t).sqrt()
EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt()
EaseInOutCirc =>
if t < 0.5 {
(1.0 - (1.0 - pow(2.0 * t, 2.0)).sqrt()) / 2.0
} else {
((1.0 - pow(-2.0 * t + 2.0, 2.0)).sqrt() + 1.0) / 2.0
}
EaseInBack => {
let c1 = 1.70158
let c3 = c1 + 1.0
c3 * t * t * t - c1 * t * t
}
EaseOutBack => {
let c1 = 1.70158
let c3 = c1 + 1.0
1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0)
}
EaseInOutBack => {
let c1 = 1.70158
let c2 = c1 * 1.525
if t < 0.5 {
pow(2.0 * t, 2.0) * ((c2 + 1.0) * 2.0 * t - c2) / 2.0
} else {
(pow(2.0 * t - 2.0, 2.0) * ((c2 + 1.0) * (t * 2.0 - 2.0) + c2) + 2.0) /
2.0
}
}
EaseInElastic =>
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
let c4 = 2.0 * @math.PI / 3.0
-pow(2.0, 10.0 * t - 10.0) * @math.sin((t * 10.0 - 10.75) * c4)
}
EaseOutElastic =>
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
let c4 = 2.0 * @math.PI / 3.0
pow(2.0, -10.0 * t) * @math.sin((t * 10.0 - 0.75) * c4) + 1.0
}
EaseInOutElastic =>
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
let c5 = 2.0 * @math.PI / 4.5
if t < 0.5 {
-(pow(2.0, 20.0 * t - 10.0) * @math.sin((20.0 * t - 11.125) * c5)) /
2.0
} else {
pow(2.0, -20.0 * t + 10.0) * @math.sin((20.0 * t - 11.125) * c5) / 2.0 +
1.0
}
}
EaseInBounce => 1.0 - ease_out_bounce(1.0 - t)
EaseOutBounce => ease_out_bounce(t)
EaseInOutBounce =>
if t < 0.5 {
(1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
}
LinearFunction(points) => sample_linear_function(points, t)
CubicBezier(x1, y1, x2, y2) =>
if t == 0.0 || t == 1.0 {
t
} else {
let bezier_t = solve_cubic_bezier_t(t, x1, x2)
cubic_bezier_axis(bezier_t, y1, y2)
}
Steps(count, position) => sample_steps(count, position, t)
}
}
///|
/// Apply the easing to a numeric range at normalized progress.
pub fn Easing::apply(
self : Easing,
start_value : Double,
end_value : Double,
progress : Double,
) -> Double {
apply_eased_progress(start_value, end_value, self.sample(progress))
}
///|
/// Build one frame with the clamped input progress, eased progress and value.
pub fn Easing::frame(
self : Easing,
start_value : Double,
end_value : Double,
progress : Double,
) -> EasingFrame {
let progress = clamp_unit(progress)
let eased_progress = self.sample(progress)
{
progress,
eased_progress,
value: apply_eased_progress(start_value, end_value, eased_progress),
}
}
///|
/// Build evenly spaced frames across the easing range. The sequence includes
/// both endpoints when `count >= 2`.
pub fn Easing::frames(
self : Easing,
start_value : Double,
end_value : Double,
count : Int,
) -> Array[EasingFrame] {
let frames : Array[EasingFrame] = []
if count <= 0 {
return frames
}
if count == 1 {
frames.push(self.frame(start_value, end_value, 0.0))
return frames
}
let last_index = count - 1
let denominator = last_index.to_double()
for i = 0; i < count; i = i + 1 {
let progress = i.to_double() / denominator
frames.push(self.frame(start_value, end_value, progress))
}
frames
}
///|
fn parse_easing_number(value : String) -> Double? {
Some(@string.parse_double(value.trim().to_owned())) catch {
_ => None
}
}
///|
fn parse_easing_int(value : String) -> Int? {
Some(@string.parse_int(value.trim().to_owned())) catch {
_ => None
}
}
///|
fn parse_function_args(value : String, prefix : String) -> Array[String]? {
if !value.has_prefix(prefix) || !value.has_suffix(")") {
return None
}
let inner = value.unsafe_substring(
start=prefix.length(),
end=value.length() - 1,
)
Some(inner.split(",").map(fn(part) { part.trim().to_owned() }).collect())
}
///|
priv struct LinearStop {
output : Double
inputs : Array[Double]
}
///|
priv struct PendingLinearPoint {
input : Double?
output : Double
}
///|
fn parse_easing_percentage(value : String) -> Double? {
let value = value.trim().to_owned()
if !value.has_suffix("%") {
return None
}
let number = value.unsafe_substring(start=0, end=value.length() - 1)
match parse_easing_number(number) {
Some(v) => Some(v / 100.0)
None => None
}
}
///|
fn parse_linear_stop(value : String) -> LinearStop? {
let tokens = value
.split(" ")
.map(fn(token) { token.trim().to_owned() })
.filter(fn(token) { !token.is_empty() })
let inputs : Array[Double] = []
let mut output : Double? = None
for token in tokens {
match parse_easing_percentage(token) {
Some(input) => inputs.push(input)
None =>
match parse_easing_number(token) {
Some(value) =>
match output {
Some(_) => return None
None => output = Some(value)
}
None => return None
}
}
}
if inputs.length() > 2 {
return None
}
match output {
Some(output) => Some({ output, inputs })
None => None
}
}
///|
fn resolve_pending_linear_points(
pending : Array[PendingLinearPoint],
) -> Array[LinearEasingPoint]? {
let points : Array[LinearEasingPoint] = []
let mut index = 0
while index < pending.length() {
match pending[index].input {
Some(input) => {
points.push(LinearEasingPoint::new(input, pending[index].output))
index = index + 1
}
None => {
if points.length() == 0 {
return None
}
let previous_input = points[points.length() - 1].input
let run_start = index
let mut run_end = index
while run_end < pending.length() && pending[run_end].input is None {
run_end = run_end + 1
}
if run_end >= pending.length() {
return None
}
let next_input = match pending[run_end].input {
Some(v) => v
None => return None
}
let denominator = (run_end - run_start + 1).to_double()
for i = run_start; i < run_end; i = i + 1 {
let ratio = (i - run_start + 1).to_double() / denominator
let input = previous_input + (next_input - previous_input) * ratio
points.push(LinearEasingPoint::new(input, pending[i].output))
}
index = run_end
}
}
}
Some(points)
}
///|
fn create_linear_easing_function(stops : Array[LinearStop]) -> Easing? {
if stops.length() < 2 {
return None
}
let pending : Array[PendingLinearPoint] = []
let mut largest_input = 0.0
let mut has_largest_input = false
let last_stop_index = stops.length() - 1
for i = 0; i < stops.length(); i = i + 1 {
let stop = stops[i]
if stop.inputs.length() > 0 {
let input = if has_largest_input && stop.inputs[0] < largest_input {
largest_input
} else {
stop.inputs[0]
}
pending.push({ input: Some(input), output: stop.output })
largest_input = input
has_largest_input = true
if stop.inputs.length() == 2 {
let input = if stop.inputs[1] < largest_input {
largest_input
} else {
stop.inputs[1]
}
pending.push({ input: Some(input), output: stop.output })
largest_input = input
}
} else if i == 0 {
pending.push({ input: Some(0.0), output: stop.output })
largest_input = 0.0
has_largest_input = true
} else if i == last_stop_index {
let input = if has_largest_input && largest_input > 1.0 {
largest_input
} else {
1.0
}
pending.push({ input: Some(input), output: stop.output })
} else {
pending.push({ input: None, output: stop.output })
}
}
match resolve_pending_linear_points(pending) {
Some(points) => Easing::linear(points)
None => None
}
}
///|
fn parse_linear_function(value : String) -> Easing? {
match parse_function_args(value, "linear(") {
Some(parts) if parts.length() >= 2 => {
let stops : Array[LinearStop] = []
for part in parts {
match parse_linear_stop(part) {
Some(stop) => stops.push(stop)
None => return None
}
}
create_linear_easing_function(stops)
}
_ => None
}
}
///|
fn parse_cubic_bezier(value : String) -> Easing? {
match parse_function_args(value, "cubic-bezier(") {
Some(parts) if parts.length() == 4 =>
match
(
parse_easing_number(parts[0]),
parse_easing_number(parts[1]),
parse_easing_number(parts[2]),
parse_easing_number(parts[3]),
) {
(Some(x1), Some(y1), Some(x2), Some(y2)) =>
if x1 >= 0.0 && x1 <= 1.0 && x2 >= 0.0 && x2 <= 1.0 {
Some(CubicBezier(x1, y1, x2, y2))
} else {
None
}
_ => None
}
_ => None
}
}
///|
fn parse_steps(value : String) -> Easing? {
match parse_function_args(value, "steps(") {
Some(parts) if parts.length() == 1 =>
match parse_easing_int(parts[0]) {
Some(count) if count > 0 => Some(Steps(count, StepPosition::End))
_ => None
}
Some(parts) if parts.length() == 2 =>
match parse_easing_int(parts[0]) {
Some(count) if count > 0 => {
let position = match parts[1].to_lower() {
"start" | "jump-start" => Some(StepPosition::Start)
"end" | "jump-end" => Some(StepPosition::End)
"jump-none" => Some(StepPosition::JumpNone)
"jump-both" => Some(StepPosition::JumpBoth)
_ => None
}
match position {
Some(StepPosition::JumpNone) if count <= 1 => None
Some(pos) => Some(Steps(count, pos))
None => None
}
}
_ => None
}
_ => None
}
}
///|
fn remove_all(value : String, separator : String) -> String {
value.split(separator).map(fn(part) { part.to_owned() }).collect().join("")
}
///|
fn normalize_easing_name(value : String) -> String {
let lower = value.trim().to_lower().to_owned()
remove_all(remove_all(remove_all(lower, "-"), "_"), " ")
}
///|
fn parse_named_easing(value : String) -> Easing? {
match normalize_easing_name(value) {
"linear" => Some(Linear)
"ease" => Some(Ease)
"easein" => Some(EaseIn)
"easeout" => Some(EaseOut)
"easeinout" => Some(EaseInOut)
"stepstart" => Some(Steps(1, StepPosition::Start))
"stepend" => Some(Steps(1, StepPosition::End))
"easeinsine" => Some(EaseInSine)
"easeoutsine" => Some(EaseOutSine)
"easeinoutsine" => Some(EaseInOutSine)
"easeinquad" => Some(EaseInQuad)
"easeoutquad" => Some(EaseOutQuad)
"easeinoutquad" => Some(EaseInOutQuad)
"easeincubic" => Some(EaseInCubic)
"easeoutcubic" => Some(EaseOutCubic)
"easeinoutcubic" => Some(EaseInOutCubic)
"easeinquart" => Some(EaseInQuart)
"easeoutquart" => Some(EaseOutQuart)
"easeinoutquart" => Some(EaseInOutQuart)
"easeinquint" => Some(EaseInQuint)
"easeoutquint" => Some(EaseOutQuint)
"easeinoutquint" => Some(EaseInOutQuint)
"easeinexpo" => Some(EaseInExpo)
"easeoutexpo" => Some(EaseOutExpo)
"easeinoutexpo" => Some(EaseInOutExpo)
"easeincirc" => Some(EaseInCirc)
"easeoutcirc" => Some(EaseOutCirc)
"easeinoutcirc" => Some(EaseInOutCirc)
"easeinback" => Some(EaseInBack)
"easeoutback" => Some(EaseOutBack)
"easeinoutback" => Some(EaseInOutBack)
"easeinelastic" => Some(EaseInElastic)
"easeoutelastic" => Some(EaseOutElastic)
"easeinoutelastic" => Some(EaseInOutElastic)
"easeinbounce" => Some(EaseInBounce)
"easeoutbounce" => Some(EaseOutBounce)
"easeinoutbounce" => Some(EaseInOutBounce)
_ => None
}
}
///|
/// Parse a CSS easing keyword or basic timing function.
pub fn parse_easing(value : String) -> Easing? {
let normalized = value.trim().to_lower().to_owned()
match parse_named_easing(normalized) {
Some(easing) => Some(easing)
None =>
match parse_linear_function(normalized) {
Some(easing) => Some(easing)
None =>
match parse_cubic_bezier(normalized) {
Some(easing) => Some(easing)
None => parse_steps(normalized)
}
}
}
}
///|
/// CSS comparison/math function selector for min() / max() / clamp().
pub(all) enum MathOp {
Min
Max
Clamp
} derive(Eq, Debug)
///|
/// Apply a min()/max()/clamp() reduction over already-resolved scalar values.
/// clamp expects exactly 3 values (lo, val, hi); other lengths fall back to the
/// first value (or 0.0 when empty) so callers never panic on malformed input.
pub fn apply_math_op(op : MathOp, vals : Array[Double]) -> Double {
match op {
Clamp =>
if vals.length() == 3 {
// CSS: clamp(MIN, VAL, MAX) == max(MIN, min(VAL, MAX)). This stays
// correct even when MIN > MAX (it returns MIN), unlike ordered ifs.
let lo = vals[0]
let mid = vals[1]
let hi = vals[2]
let inner = if mid < hi { mid } else { hi }
if lo > inner {
lo
} else {
inner
}
} else if vals.length() > 0 {
vals[0]
} else {
0.0
}
_ => {
if vals.is_empty() {
return 0.0
}
let mut acc = vals[0]
for k = 1; k < vals.length(); k = k + 1 {
match op {
Min => if vals[k] < acc { acc = vals[k] }
_ => if vals[k] > acc { acc = vals[k] }
}
}
acc
}
}
}
///|
/// CSS dimension value: length, percentage, auto, or intrinsic sizing keywords
pub(all) enum Dimension {
Length(Double)
Percent(Double) // 0.0 to 1.0
Auto
MinContent // Intrinsic minimum content size
MaxContent // Intrinsic maximum content size
FitContent(Double) // fit-content(length) - clamp between min-content and max-content
// Mixed calc() carrying both a fixed length (px) and a percentage ratio,
// e.g. calc(100% - 20px) => Calc(px=-20.0, percent=1.0). Resolves to
// `px + percent * basis` once a containing-block basis is known at layout
// time. Pure-length / pure-percent calc() stay Length / Percent.
Calc(Double, Double) // Calc(px, percent_ratio)
// min()/max()/clamp() whose arguments mix length and percentage and so cannot
// be ordered until a containing-block basis is known. Each argument is a
// linear (px, percent_ratio) form. Resolved at layout time like Calc.
MathFn(MathOp, Array[(Double, Double)])
} derive(Debug, Eq)
///|
/// Check if dimension equals a specific length value
pub fn Dimension::eq_length(self : Dimension, value : Double) -> Bool {
match self {
Length(v) => (v - value).abs() < 0.0001
_ => false
}
}
///|
/// Check if dimension equals a specific percent value
pub fn Dimension::eq_percent(self : Dimension, value : Double) -> Bool {
match self {
Percent(v) => (v - value).abs() < 0.0001
_ => false
}
}
///|
pub impl Show for Dimension with fn output(self, logger) {
match self {
Length(v) => {
logger.write_string("Length(")
v.output(logger)
logger.write_string(")")
}
Percent(v) => {
logger.write_string("Percent(")
v.output(logger)
logger.write_string(")")
}
Auto => logger.write_string("Auto")
MinContent => logger.write_string("MinContent")
MaxContent => logger.write_string("MaxContent")
FitContent(v) => {
logger.write_string("FitContent(")
v.output(logger)
logger.write_string(")")
}
Calc(px, pct) => {
logger.write_string("Calc(")
px.output(logger)
logger.write_string(", ")
pct.output(logger)
logger.write_string(")")
}
MathFn(op, args) => {
logger.write_string("MathFn(")
logger.write_string(
match op {
Min => "Min"
Max => "Max"
Clamp => "Clamp"
},
)
logger.write_string(", [")
for i = 0; i < args.length(); i = i + 1 {
if i > 0 {
logger.write_string(", ")
}
let (px, pct) = args[i]
logger.write_string("(")
px.output(logger)
logger.write_string(", ")
pct.output(logger)
logger.write_string(")")
}
logger.write_string("])")
}
}
}
///|
/// Resolve dimension to concrete value
/// context: parent size for percentage calculation
/// Note: MinContent, MaxContent, FitContent return None - they need intrinsic size calculation
pub fn Dimension::resolve(self : Dimension, context : Double) -> Double? {
match self {
Length(v) => Some(v)
Percent(v) => Some(context * v)
Calc(px, pct) => Some(px + pct * context)
MathFn(op, args) =>
if args.is_empty() {
None
} else {
Some(apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context })))
}
Auto => None
MinContent => None // Requires intrinsic size calculation
MaxContent => None // Requires intrinsic size calculation
FitContent(_) => None // Requires intrinsic size calculation
}
}
///|
/// Resolve with fallback value for Auto
///
/// Matches `self` directly rather than going through `resolve`, which returns a
/// `Double?` and so boxes a `Some(..)` on every call. This is on the hot layout
/// path (every width/height/padding/margin per element, plus all four sides via
/// `resolve_rect`), where the intermediate `Option` allocation dominated.
pub fn Dimension::resolve_or(
self : Dimension,
context : Double,
fallback : Double,
) -> Double {
match self {
Length(v) => v
Percent(v) => context * v
Calc(px, pct) => px + pct * context
MathFn(op, args) =>
if args.is_empty() {
fallback
} else {
apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context }))
}
Auto => fallback
MinContent => fallback // Requires intrinsic size calculation
MaxContent => fallback
FitContent(_) => fallback
}
}
///|
/// Check if dimension is a definite value (Length or Percent)
pub fn Dimension::is_definite(self : Dimension) -> Bool {
match self {
Length(_) | Percent(_) | Calc(_, _) | MathFn(_, _) => true
_ => false
}
}
///|
/// Check if dimension requires intrinsic sizing
pub fn Dimension::is_intrinsic(self : Dimension) -> Bool {
match self {
MinContent | MaxContent | FitContent(_) => true
_ => false
}
}
///|
/// Resolve dimension to length, treating intrinsic keywords as fallback
pub fn Dimension::resolve_or_intrinsic(
self : Dimension,
context : Double,
fallback : Double,
) -> Double {
match self {
Length(v) => v
Percent(p) => context * p
Calc(px, p) => px + context * p
MathFn(op, args) =>
if args.is_empty() {
fallback
} else {
apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context }))
}
Auto => fallback
MinContent => fallback // Intrinsic sizing handled by layout engine
MaxContent => fallback
FitContent(_) => fallback
}
}
///|
/// Resolve a Rect[Dimension] (margin/padding/border) to Rect[Double]
/// Uses parent_width for percentage resolution (CSS spec: percentages resolve against width)
pub fn resolve_rect(
rect : Rect[Dimension],
parent_width : Double,
) -> Rect[Double] {
{
left: rect.left.resolve_or(parent_width, 0.0),
right: rect.right.resolve_or(parent_width, 0.0),
top: rect.top.resolve_or(parent_width, 0.0),
bottom: rect.bottom.resolve_or(parent_width, 0.0),
}
}
///|
/// Resolve a dimension rect for intrinsic sizing
/// Percentages resolve to 0 in intrinsic sizing contexts
pub fn resolve_rect_intrinsic(rect : Rect[Dimension]) -> Rect[Double] {
fn resolve_intrinsic(dim : Dimension) -> Double {
match dim {
Length(v) => v
Percent(_) => 0.0
Calc(px, _) => px // percent resolves to 0 in intrinsic context; keep px
MathFn(op, args) =>
// intrinsic context: percentages resolve to 0, so keep px parts only
if args.is_empty() {
0.0
} else {
apply_math_op(op, args.map(fn(a) { a.0 }))
}
Auto => 0.0
MinContent => 0.0 // Not applicable for margin/padding/border
MaxContent => 0.0
FitContent(_) => 0.0
}
}
{
left: resolve_intrinsic(rect.left),
right: resolve_intrinsic(rect.right),
top: resolve_intrinsic(rect.top),
bottom: resolve_intrinsic(rect.bottom),
}
}
///|
/// BoundingRect - the output of layout computation
/// Matches browser's getBoundingClientRect()
pub(all) struct BoundingRect {
x : Double
y : Double
width : Double
height : Double
}
///|
pub fn BoundingRect::new(
x : Double,
y : Double,
width : Double,
height : Double,
) -> BoundingRect {
{ x, y, width, height }
}
///|
pub impl Show for BoundingRect with fn output(self, logger) {
logger.write_string("BoundingRect { x: ")
self.x.output(logger)
logger.write_string(", y: ")
self.y.output(logger)
logger.write_string(", width: ")
self.width.output(logger)
logger.write_string(", height: ")
self.height.output(logger)
logger.write_string(" }")
}
///|
pub fn BoundingRect::zero() -> BoundingRect {
{ x: 0.0, y: 0.0, width: 0.0, height: 0.0 }
}
///|
/// Get the right edge (x + width)
pub fn BoundingRect::right(self : BoundingRect) -> Double {
self.x + self.width
}
///|
/// Get the bottom edge (y + height)
pub fn BoundingRect::bottom(self : BoundingRect) -> Double {
self.y + self.height
}
///|
/// Calculate the area of the bounding rect
pub fn BoundingRect::area(self : BoundingRect) -> Double {
self.width * self.height
}
///|
/// Calculate union of two bounding rects
pub fn BoundingRect::union(
self : BoundingRect,
other : BoundingRect,
) -> BoundingRect {
let x = min(self.x, other.x)
let y = min(self.y, other.y)
let right = max(self.right(), other.right())
let bottom = max(self.bottom(), other.bottom())
{ x, y, width: right - x, height: bottom - y }
}
// ============================================================================
// Aspect Ratio utilities
// ============================================================================
///|
/// Apply aspect ratio to calculate width from height.
/// Returns width if aspect_ratio is defined, otherwise returns None.
pub fn apply_aspect_ratio_width(
height : Double,
aspect_ratio : Double?,
) -> Double? {
match aspect_ratio {
Some(ar) if ar > 0.0 => Some(height * ar)
_ => None
}
}
///|
/// Apply aspect ratio to calculate height from width.
/// Returns height if aspect_ratio is defined, otherwise returns None.
pub fn apply_aspect_ratio_height(
width : Double,
aspect_ratio : Double?,
) -> Double? {
match aspect_ratio {
Some(ar) if ar > 0.0 => Some(width / ar)
_ => None
}
}
///|
/// Resolve dimensions with aspect ratio.
/// Given known dimension (width or height), aspect_ratio, and min/max constraints,
/// returns the final (width, height) pair.
///
/// - If width is known, height = width / aspect_ratio
/// - If height is known, width = height * aspect_ratio
/// - If both are known, aspect_ratio overrides the height
/// - Min/max constraints are applied after aspect ratio calculation
pub fn resolve_dimensions_with_aspect_ratio(
width : Double?, // Known width (or None)
height : Double?, // Known height (or None)
aspect_ratio : Double?, // aspect_ratio = width / height
min_width : Double, // 0.0 if no constraint
max_width : Double, // Infinity if no constraint
min_height : Double, // 0.0 if no constraint
max_height : Double, // Infinity if no constraint
) -> (Double?, Double?) {
match aspect_ratio {
None => (width, height)
Some(ar) if ar <= 0.0 => (width, height)
Some(ar) =>
match (width, height) {
(Some(w), Some(_)) => {
// Both known: aspect ratio overrides height
let h = w / ar
let h = clamp(h, min_height, max_height)
(Some(w), Some(h))
}
(Some(w), None) => {
// Width known: calculate height
let h = w / ar
let h = clamp(h, min_height, max_height)
(Some(w), Some(h))
}
(None, Some(h)) => {
// Height known: calculate width
let w = h * ar
let w = clamp(w, min_width, max_width)
(Some(w), Some(h))
}
(None, None) => (None, None)
}
}
}
///|
/// Clamp a value between min and max
/// CSS spec: min takes precedence over max when they conflict
/// So apply max first (value cannot exceed max), then min (value cannot go below min)
pub fn clamp(value : Double, min_val : Double, max_val : Double) -> Double {
// First apply max constraint
let clamped = if value > max_val { max_val } else { value }
// Then apply min constraint (min wins if min > max)
if clamped < min_val {
min_val
} else {
clamped
}
}
// =============================================================================
// Clip Rect (for legacy clip property)
// =============================================================================
///|
/// CSS clip property value
/// clip: rect(top, right, bottom, left) or auto
/// Note: This is a deprecated property, but still widely used for accessibility
pub(all) enum ClipRect {
/// clip: auto (no clipping)
Auto
/// clip: rect(top, right, bottom, left)
/// Values are offsets from the element's edges
Rect(top~ : Double, right~ : Double, bottom~ : Double, left~ : Double)
} derive(Debug, Eq)
///|
pub impl Show for ClipRect with fn output(self, logger) {
match self {
Auto => logger.write_string("auto")
Rect(top~, right~, bottom~, left~) => {
logger.write_string("rect(")
top.output(logger)
logger.write_string(", ")
right.output(logger)
logger.write_string(", ")
bottom.output(logger)
logger.write_string(", ")
left.output(logger)
logger.write_string(")")
}
}
}
///|
pub fn ClipRect::default() -> ClipRect {
Auto
}