///|
/// Bridge from parsed `@keyframes` (cascade) to the animation package's typed
/// computed-value interpolation. This is the layout-independent part of "frame
/// value resolution": it turns a `KeyframesRule` plus the element's base
/// computed value into a per-property track of `AnimatableValue`s. The consumer
/// (layout engine) drives time and resolves the interpolated computed value to
/// a used value, exactly as for a static computed value.
///|
/// Classify a single keyframe declaration value into an AnimatableValue at the
/// computed-value level. Length units are resolved to px via `ctx`; percentages
/// stay as ratios; colors resolve to sRGB; bare numbers stay numbers; anything
/// else (`auto`, intrinsic sizing, keywords) is discrete.
pub fn keyframe_value_to_animatable(
value : String,
ctx : ComputeContext,
) -> @animation.AnimatableValue {
let v = value.trim().to_owned()
// Pure number (e.g. opacity). `0` stays a Number here and is reconciled to a
// Length/Percent zero per-track in `build_keyframe_track`.
if is_pure_number(v) {
return @animation.AnimatableValue::Number(parse_number(v))
}
// Length / percentage. Tried before color because `parse_color` is lenient
// (it resolves unknown input to transparent), so it must not see dimensions.
match resolve_dimension(v, ctx) {
@types.Length(px) => return @animation.AnimatableValue::Length(px)
@types.Percent(r) => return @animation.AnimatableValue::Percent(r)
_ => ()
}
// Color (#hex / rgb()/hsl()/... functions, or a named color).
match animatable_color(v) {
Some(col) => return @animation.AnimatableValue::Color(col)
None => ()
}
@animation.AnimatableValue::Discrete(v)
}
///|
/// Resolve a keyframe value for a specific property. The `transform` property is
/// parsed into a `Transform` (component-wise interpolable); everything else uses
/// the generic computed-value classifier.
fn keyframe_value_for_property(
property : String,
value : String,
ctx : ComputeContext,
) -> @animation.AnimatableValue {
if property == "transform" {
@animation.AnimatableValue::Transform(parse_transform(value))
} else if property == "translate" {
@animation.AnimatableValue::Transform(
@parser.parse_translate_property_from_string(value),
)
} else if property == "scale" {
@animation.AnimatableValue::Transform(
@parser.parse_scale_property_from_string(value),
)
} else if property == "rotate" {
@animation.AnimatableValue::Transform(
@parser.parse_rotate_property_from_string(value),
)
} else {
keyframe_value_to_animatable(value, ctx)
}
}
///|
/// Resolve a color, shape-gated so `parse_color`'s lenient transparent fallback
/// does not turn non-color keywords (`auto`, `flex-start`) into colors. Function
/// / hex forms are accepted outright; a bare ident is accepted as a named color
/// only when it does not resolve to the fully-transparent "unknown" sentinel.
fn animatable_color(v : String) -> @types.Color? {
if v == "transparent" {
return Some(@types.Color::transparent())
}
let is_func_or_hex = v.has_prefix("#") ||
v.has_prefix("rgb") ||
v.has_prefix("hsl") ||
v.has_prefix("hwb") ||
v.has_prefix("lab") ||
v.has_prefix("lch") ||
v.has_prefix("oklab") ||
v.has_prefix("oklch") ||
v.has_prefix("color(")
match parse_color(v).get_color() {
Some(c) =>
if is_func_or_hex {
Some(c)
} else if c.a == 0.0 && c.r == 0 && c.g == 0 && c.b == 0 {
None
} else {
Some(c)
}
None => None
}
}
///|
/// The dominant interpolable kind in a value list, used to reconcile a unitless
/// `0` (parsed as Number) with Length/Percent siblings (so `left: 0 -> 100px`
/// interpolates as lengths instead of falling back to discrete).
priv enum TrackKind {
KindNumber
KindLength
KindPercent
KindColor
KindNone
}
///|
fn animatable_kind(v : @animation.AnimatableValue) -> TrackKind {
match v {
@animation.AnimatableValue::Number(_) => KindNumber
@animation.AnimatableValue::Length(_) => KindLength
@animation.AnimatableValue::Percent(_) => KindPercent
@animation.AnimatableValue::Color(_) => KindColor
@animation.AnimatableValue::Transform(_) => KindNone
@animation.AnimatableValue::Discrete(_) => KindNone
}
}
///|
/// Coerce a unitless `Number(0)` to the track's dominant Length/Percent zero.
fn reconcile_zero(
v : @animation.AnimatableValue,
dominant : TrackKind,
) -> @animation.AnimatableValue {
match v {
@animation.AnimatableValue::Number(n) =>
if n == 0.0 {
match dominant {
KindLength => @animation.AnimatableValue::Length(0.0)
KindPercent => @animation.AnimatableValue::Percent(0.0)
_ => v
}
} else {
v
}
_ => v
}
}
///|
/// Extract the string value of a declaration (or "" if not a plain value).
fn declaration_value_string(decl : @cascade.Declaration) -> String {
match decl.value {
@cascade.Value(s) => s
_ => ""
}
}
///|
/// One raw keyframe collected from the rule before kind reconciliation.
priv struct RawKeyframe {
offset : Double
value : @animation.AnimatableValue
easing : @types.Easing
}
///|
/// Build a per-property animation track from a `@keyframes` rule and the
/// element's base computed value (`base_value`, the value the property has on
/// the element, used to fill missing 0%/100% endpoints). Returns keyframes
/// sorted by offset, with a 0.0 and 1.0 endpoint guaranteed, and unitless-zero
/// values reconciled to the track's dominant Length/Percent kind.
pub fn build_keyframe_track(
rule : @cascade.KeyframesRule,
property : String,
base_value : String,
ctx : ComputeContext,
) -> Array[@animation.AnimatableKeyframe] {
let raw : Array[RawKeyframe] = []
for block in rule.blocks {
// Find this block's value for the property and its per-keyframe easing.
let mut value_str : String? = None
let mut easing = @types.Easing::Linear
for decl in block.declarations {
if decl.property == property {
value_str = Some(declaration_value_string(decl))
} else if decl.property == "animation-timing-function" {
match @types.parse_easing(declaration_value_string(decl)) {
Some(e) => easing = e
None => ()
}
}
}
match value_str {
Some(s) => {
let v = keyframe_value_for_property(property, s, ctx)
for off in block.offsets {
raw.push({ offset: off, value: v, easing })
}
}
None => ()
}
}
let base = keyframe_value_for_property(property, base_value, ctx)
// Ensure 0.0 and 1.0 endpoints exist (CSS fills them with the base value).
let mut has_start = false
let mut has_end = false
for r in raw {
if r.offset <= 0.0 {
has_start = true
}
if r.offset >= 1.0 {
has_end = true
}
}
if !has_start {
raw.push({ offset: 0.0, value: base, easing: @types.Easing::Linear })
}
if !has_end {
raw.push({ offset: 1.0, value: base, easing: @types.Easing::Linear })
}
// Stable insertion sort by offset (keeps source order for equal offsets).
for i = 1; i < raw.length(); i = i + 1 {
let cur = raw[i]
let mut j = i - 1
while j >= 0 && raw[j].offset > cur.offset {
raw[j + 1] = raw[j]
j = j - 1
}
raw[j + 1] = cur
}
// Determine the dominant interpolable kind to reconcile unitless zeros.
let mut dominant = KindNone
for r in raw {
match animatable_kind(r.value) {
KindLength => dominant = KindLength
KindPercent => if dominant is KindNone { dominant = KindPercent }
KindColor => if dominant is KindNone { dominant = KindColor }
_ => ()
}
}
let result : Array[@animation.AnimatableKeyframe] = []
for r in raw {
result.push(
@animation.AnimatableKeyframe::with_easing(
r.offset,
reconcile_zero(r.value, dominant),
r.easing,
),
)
}
result
}
///|
/// Return `arr[index]` cycling by length (CSS animation longhand lists repeat
/// to match the animation count), or `default` when the list is empty.
fn[T] list_cycle(arr : Array[T], index : Int, default : T) -> T {
if arr.length() == 0 {
default
} else {
arr[index % arr.length()]
}
}
///|
fn fill_mode_to_anim(f : @types.AnimationFillMode) -> @animation.FillMode {
match f {
@types.AnimationFillMode::None => @animation.FillMode::None
@types.AnimationFillMode::Forwards => @animation.FillMode::Forwards
@types.AnimationFillMode::Backwards => @animation.FillMode::Backwards
@types.AnimationFillMode::Both => @animation.FillMode::Both
}
}
///|
fn direction_to_anim(
d : @types.AnimationDirection,
) -> @animation.PlaybackDirection {
match d {
@types.AnimationDirection::Normal => @animation.PlaybackDirection::Normal
@types.AnimationDirection::Reverse => @animation.PlaybackDirection::Reverse
@types.AnimationDirection::Alternate =>
@animation.PlaybackDirection::Alternate
@types.AnimationDirection::AlternateReverse =>
@animation.PlaybackDirection::AlternateReverse
}
}
///|
/// Build an `EffectTiming` for the `index`-th animation declared on `style`
/// (its `animation-*` longhands). Durations/delays are converted from seconds to
/// milliseconds (the timing model's unit); `infinite` maps to an infinite
/// iteration count. Longhand lists shorter than the animation list are cycled.
pub fn effect_timing_from_style(
style : @style.Style,
index : Int,
) -> @animation.EffectTiming {
let duration = list_cycle(style.animation_duration, index, 0.0) * 1000.0
let delay = list_cycle(style.animation_delay, index, 0.0) * 1000.0
let iterations = match list_cycle(
style.animation_iteration_count,
index,
@types.AnimationIterationCount::Count(1.0),
) {
@types.AnimationIterationCount::Count(n) => n
@types.AnimationIterationCount::Infinite => @double.infinity
}
let fill = fill_mode_to_anim(
list_cycle(style.animation_fill_mode, index, @types.AnimationFillMode::None),
)
let direction = direction_to_anim(
list_cycle(
style.animation_direction,
index,
@types.AnimationDirection::Normal,
),
)
let easing = list_cycle(
style.animation_timing_function,
index,
@types.Easing::Ease,
)
{
..@animation.EffectTiming::new(duration),
delay,
iterations,
fill,
direction,
easing,
}
}