tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1// Copyright 2024 the Velato Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use kurbo::{
5 ParamCurve, Point,
6 common::{solve_cubic, solve_itp},
7};
8
9/// Fixed or animated value.
10#[derive(Clone, Debug)]
11pub enum Value<T: Tween> {
12 /// Fixed value.
13 Fixed(T),
14 /// Animated value.
15 Animated(Animated<T>),
16}
17
18impl<T: Tween + Default> Value<T> {
19 /// Returns the value at a specified frame.
20 pub fn evaluate(&self, frame: f64) -> T {
21 match self {
22 Self::Fixed(fixed) => fixed.clone(),
23 Self::Animated(animated) => animated.evaluate_or(frame, T::default()),
24 }
25 }
26}
27
28impl<T: Tween> Value<T> {
29 /// Returns true if the value is fixed.
30 pub fn is_fixed(&self) -> bool {
31 matches!(self, Self::Fixed(_))
32 }
33
34 /// Returns the value at a specified frame, with a fallback default.
35 pub fn evaluate_or(&self, frame: f64, default: T) -> T {
36 match self {
37 Self::Fixed(fixed) => fixed.clone(),
38 Self::Animated(animated) => animated.evaluate_or(frame, default),
39 }
40 }
41}
42
43impl<T: Tween + Default> Default for Value<T> {
44 fn default() -> Self {
45 Self::Fixed(T::default())
46 }
47}
48
49/// Borrowed or owned value.
50#[derive(Clone, Debug)]
51pub enum ValueRef<'a, T> {
52 Borrowed(&'a T),
53 Owned(T),
54}
55
56impl<T> AsRef<T> for ValueRef<'_, T> {
57 fn as_ref(&self) -> &T {
58 match self {
59 Self::Borrowed(value) => value,
60 Self::Owned(value) => value,
61 }
62 }
63}
64
65impl<T: Clone> ValueRef<'_, T> {
66 pub fn into_owned(self) -> T {
67 match self {
68 Self::Borrowed(value) => value.clone(),
69 Self::Owned(value) => value,
70 }
71 }
72}
73
74#[derive(Copy, Clone, Debug)]
75pub struct Easing {
76 pub o: EasingHandle,
77 pub i: EasingHandle,
78}
79
80impl Easing {
81 pub const LERP: Easing = Easing {
82 o: EasingHandle { x: 0.0, y: 0.0 },
83 i: EasingHandle { x: 1.0, y: 1.0 },
84 };
85}
86
87#[derive(Copy, Clone, Debug)]
88pub struct EasingHandle {
89 pub x: f64,
90 pub y: f64,
91}
92
93/// Time for a particular keyframe, represented as a frame number.
94#[derive(Copy, Clone, Debug)]
95pub struct Time {
96 /// Frame number.
97 pub frame: f64,
98 /// Easing tangent going into the next keyframe
99 pub in_tangent: Option<EasingHandle>,
100 /// Easing tangent leaving the current keyframe
101 pub out_tangent: Option<EasingHandle>,
102 /// Whether it's a hold frame.
103 pub hold: bool,
104}
105
106impl Time {
107 /// Returns the frame indices and interpolation weight for the given frame,
108 /// and whether to hold the frame
109 pub(crate) fn frames_and_weight(
110 times: &[Time],
111 frame: f64,
112 ) -> Option<([usize; 2], f64, Easing, bool)> {
113 if times.is_empty() {
114 return None;
115 }
116 use core::cmp::Ordering::*;
117 let ix = match times.binary_search_by(|x| {
118 if x.frame < frame {
119 Less
120 } else if x.frame > frame {
121 Greater
122 } else {
123 Equal
124 }
125 }) {
126 Ok(ix) => ix,
127 Err(ix) => ix.saturating_sub(1),
128 };
129 let ix0 = ix.min(times.len() - 1);
130 let ix1 = (ix0 + 1).min(times.len() - 1);
131
132 let t0 = times[ix0];
133 let t1 = times[ix1];
134 let (t0_ox, t0_oy) = t0.out_tangent.map(|o| (o.x, o.y)).unwrap_or((0.0, 0.0));
135 let (t0_ix, t0_iy) = t0.in_tangent.map(|i| (i.x, i.y)).unwrap_or((1.0, 1.0));
136 let easing = Easing {
137 o: EasingHandle { x: t0_ox, y: t0_oy },
138 i: EasingHandle { x: t0_ix, y: t0_iy },
139 };
140 let hold = t0.hold;
141 let t = (frame - t0.frame) / (t1.frame - t0.frame);
142 Some(([ix0, ix1], t.clamp(0.0, 1.0), easing, hold))
143 }
144}
145
146#[derive(Clone, Debug)]
147pub struct Animated<T: Tween> {
148 pub times: Vec<Time>,
149 pub values: Vec<T>,
150}
151
152impl<T: Tween> Animated<T> {
153 /// Returns the value at the specified frame.
154 pub fn evaluate_or(&self, frame: f64, default: T) -> T {
155 self.evaluate_inner(frame).unwrap_or(default)
156 }
157
158 fn evaluate_inner(&self, frame: f64) -> Option<T> {
159 let ([ix0, ix1], t, easing, hold) = Time::frames_and_weight(&self.times, frame)?;
160 let t = if hold { 0.0 } else { t };
161
162 let v1 = self.values.get(ix0)?;
163 let v2 = self.values.get(ix1)?;
164
165 Some(v1.tween(v2, t, &easing))
166 }
167}
168
169/// Something that can be interpolated with an easing function.
170pub trait Tween: Clone {
171 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self;
172}
173
174impl Tween for f64 {
175 /// Tween between two f64 values using a cubic bezier easing curve.
176 ///
177 /// Unfortunately, kurbo doesn't expose a way to get the eased value of t directly, so we have
178 /// to re-implement it here.
179 ///
180 /// t - normalized time 0.0 to 1.0
181 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self {
182 // avoid unnecessary calculations
183 if t <= 0.0 {
184 return *self;
185 }
186 if t >= 1.0 {
187 return *other;
188 }
189
190 // Created for [kurbo::ParamCurve::eval]
191 let curve = kurbo::CubicBez::new(
192 Point::new(0.0, 0.0),
193 Point::new(easing.o.x, easing.o.y),
194 Point::new(easing.i.x, easing.i.y),
195 Point::new(1.0, 1.0),
196 );
197
198 // Manual copy-paste of [kurbo::bezpath::cubic_bez_coefs]
199 // in preparation for [kurbo::common::solve_cubic]
200 let (x0, x1, x2, x3) = (curve.p0.x, curve.p1.x, curve.p2.x, curve.p3.x);
201 let c0 = x0;
202 let c1 = 3.0 * (x1 - x0);
203 let c2 = 3.0 * (x2 - 2.0 * x1 + x0);
204 let c3 = x3 - 3.0 * x2 + 3.0 * x1 - x0;
205
206 // Find where along the curve we are at normalized time `t`
207 // See: https://github.com/jneem/linesweeper/blob/7d620ed004605141519037c681edac93e22f5855/src/curve/mod.rs#L355
208 let max_coeff = c0.abs().max(c1.abs()).max(c2.abs()).max(c3.abs());
209 let eps = 1e-12 * max_coeff.max(1.0);
210
211 let roots = kurbo::common::solve_cubic(c0 - t, c1, c2, c3);
212 let u = roots
213 .iter()
214 .copied()
215 .find(|&r| (-eps..=1.0 + eps).contains(&r))
216 .unwrap_or_else(|| {
217 // Fallback when no valid root found due to precision issues
218 // The cubic evaluates to (c0 - t) at u=0 and (c0 + c1 + c2 + c3 - t) at u=1
219 let val_at_0 = (c0 - t).abs();
220 let val_at_1 = (c0 + c1 + c2 + c3 - t).abs();
221
222 if val_at_0 <= val_at_1 { 0.0 } else { 1.0 }
223 });
224
225 // Evaluate the curve at that point to get the y value
226 let eased_y = curve.eval(u).y;
227
228 self + (other - self) * eased_y
229 }
230}
231
232impl Tween for kurbo::Point {
233 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self {
234 Self::new(
235 self.x.tween(&other.x, t, easing),
236 self.y.tween(&other.y, t, easing),
237 )
238 }
239}
240
241impl Tween for kurbo::Vec2 {
242 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self {
243 Self::new(
244 self.x.tween(&other.x, t, easing),
245 self.y.tween(&other.y, t, easing),
246 )
247 }
248}
249
250impl Tween for kurbo::Size {
251 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self {
252 Self::new(
253 self.width.tween(&other.width, t, easing),
254 self.height.tween(&other.height, t, easing),
255 )
256 }
257}
258
259impl Tween for peniko::Color {
260 fn tween(&self, other: &Self, t: f64, easing: &Easing) -> Self {
261 let [r1, g1, b1, a1] = self.components;
262 let [r2, g2, b2, a2] = other.components;
263 let r = (r1 as f64).tween(&(r2 as f64), t, easing);
264 let g = (g1 as f64).tween(&(g2 as f64), t, easing);
265 let b = (b1 as f64).tween(&(b2 as f64), t, easing);
266 let a = (a1 as f64).tween(&(a2 as f64), t, easing);
267 peniko::Color::new([r as f32, g as f32, b as f32, a as f32])
268 }
269}