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
4/*!
5Representations of animated values.
6*/
7
8use super::*;
9
10use kurbo::PathEl;
11
12#[derive(Clone, Debug)]
13pub enum Position {
14 Value(Value<Point>),
15 SplitValues((Value<f64>, Value<f64>)),
16}
17
18/// Animated affine transformation.
19#[derive(Clone, Debug)]
20pub struct Transform {
21 /// Anchor point.
22 pub anchor: Value<Point>,
23 /// Translation.
24 pub position: Position,
25 /// Rotation angle.
26 pub rotation: Value<f64>,
27 /// Scale factor.
28 pub scale: Value<Vec2>,
29 /// Skew factor.
30 pub skew: Value<f64>,
31 /// Skew angle, 0 skews along the x axis, 90 along the y axis.
32 pub skew_angle: Value<f64>,
33}
34
35impl Transform {
36 /// Returns true if the transform is fixed.
37 pub fn is_fixed(&self) -> bool {
38 self.anchor.is_fixed()
39 && match &self.position {
40 Position::Value(value) => value.is_fixed(),
41 Position::SplitValues((x_value, y_value)) => {
42 x_value.is_fixed() && y_value.is_fixed()
43 }
44 }
45 && self.rotation.is_fixed()
46 && self.scale.is_fixed()
47 && self.skew.is_fixed()
48 && self.skew_angle.is_fixed()
49 }
50
51 /// Evaluates the transform at the specified frame.
52 pub fn evaluate(&self, frame: f64) -> Affine {
53 let anchor = self.anchor.evaluate(frame);
54 let position = match &self.position {
55 Position::Value(value) => value.evaluate(frame),
56 Position::SplitValues((x_value, y_value)) => kurbo::Point {
57 x: x_value.evaluate(frame),
58 y: y_value.evaluate(frame),
59 },
60 };
61 let rotation = self.rotation.evaluate(frame);
62 let scale = self.scale.evaluate(frame);
63 let skew = self.skew.evaluate(frame);
64 let skew_angle = self.skew_angle.evaluate(frame);
65 let skew_matrix = if skew != 0.0 {
66 let skew = skew.to_radians();
67 let angle = skew_angle.to_radians();
68 Affine::rotate(-angle) * Affine::skew(skew.tan(), 0.0) * Affine::rotate(angle)
69 } else {
70 Affine::IDENTITY
71 };
72 Affine::translate((position.x, position.y))
73 * Affine::rotate(rotation.to_radians())
74 * skew_matrix
75 * Affine::scale_non_uniform(scale.x / 100.0, scale.y / 100.0)
76 * Affine::translate((-anchor.x, -anchor.y))
77 }
78
79 /// Converts the animated value to its model representation.
80 pub fn into_model(self) -> super::Transform {
81 if self.is_fixed() {
82 super::Transform::Fixed(self.evaluate(0.0))
83 } else {
84 super::Transform::Animated(self)
85 }
86 }
87}
88
89/// Animated ellipse.
90#[derive(Clone, Debug)]
91pub struct Ellipse {
92 /// True if the ellipse should be drawn in CCW order.
93 pub is_ccw: bool,
94 /// Position of the ellipse.
95 pub position: Value<Point>,
96 /// Size of the ellipse.
97 pub size: Value<Size>,
98}
99
100impl Ellipse {
101 pub fn is_fixed(&self) -> bool {
102 self.position.is_fixed() && self.size.is_fixed()
103 }
104
105 pub fn evaluate(&self, frame: f64) -> kurbo::Ellipse {
106 let position = self.position.evaluate(frame);
107 let size = self.size.evaluate(frame);
108 let radii = (size.width * 0.5, size.height * 0.5);
109 kurbo::Ellipse::new(position, radii, 0.0)
110 }
111}
112
113/// Animated rounded rectangle.
114#[derive(Clone, Debug)]
115pub struct Rect {
116 /// True if the rect should be drawn in CCW order.
117 pub is_ccw: bool,
118 /// Position of the rectangle.
119 pub position: Value<Point>,
120 /// Size of the rectangle.
121 pub size: Value<Size>,
122 /// Radius of the rectangle corners.
123 pub corner_radius: Value<f64>,
124}
125
126impl Rect {
127 /// Returns true if the rectangle is fixed.
128 pub fn is_fixed(&self) -> bool {
129 self.position.is_fixed() && self.size.is_fixed() && self.corner_radius.is_fixed()
130 }
131
132 /// Evaluates the rectangle at the specified frame.
133 pub fn evaluate(&self, frame: f64) -> kurbo::RoundedRect {
134 let position = self.position.evaluate(frame);
135 let size = self.size.evaluate(frame);
136 let position = Point::new(
137 position.x - size.width * 0.5,
138 position.y - size.height * 0.5,
139 );
140 let radius = self.corner_radius.evaluate(frame);
141 kurbo::RoundedRect::new(
142 position.x,
143 position.y,
144 position.x + size.width,
145 position.y + size.height,
146 radius,
147 )
148 }
149}
150
151/// Animated star or polygon.
152#[derive(Clone, Debug)]
153pub struct Star {
154 pub is_polygon: bool,
155 pub direction: f64,
156 pub position: Value<Point>,
157 pub inner_radius: Value<f64>,
158 pub inner_roundness: Value<f64>,
159 pub outer_radius: Value<f64>,
160 pub outer_roundness: Value<f64>,
161 pub rotation: Value<f64>,
162 pub points: Value<f64>,
163}
164
165// TODO: Use this.
166//impl Star {
167// pub fn is_fixed(&self) -> bool {
168// self.position.is_fixed()
169// && self.inner_radius.is_fixed()
170// && self.inner_roundness.is_fixed()
171// && self.outer_radius.is_fixed()
172// && self.outer_roundness.is_fixed()
173// && self.rotation.is_fixed()
174// && self.points.is_fixed()
175// }
176//}
177
178/// Animated cubic spline.
179#[derive(Clone, Debug)]
180pub struct Spline {
181 /// True if the spline is closed.
182 pub is_closed: bool,
183 /// Collection of times.
184 pub times: Vec<Time>,
185 /// Collection of splines for each time. The splines at each point must have a start, but may also have an
186 /// end defined. When the end is not defined, then the next keyframe's start is used.
187 pub values: Vec<SplineKeyframeValues>,
188}
189
190/// The values of an animated Spline at a single keyframe. When end is None, the next frame's start is used.
191#[derive(Clone, Debug)]
192pub struct SplineKeyframeValues {
193 pub start: Vec<Point>,
194 pub end: Option<Vec<Point>>,
195}
196
197impl Spline {
198 /// Evaluates the spline at the given frame and emits the elements
199 /// to the specified path.
200 pub fn evaluate(&self, frame: f64, path: &mut Vec<PathEl>) -> bool {
201 let Some(([ix0, ix1], t, _easing, hold)) = Time::frames_and_weight(&self.times, frame)
202 else {
203 return false;
204 };
205 let t = if hold { 0.0 } else { t };
206 let (Some(from), Some(to)) = (self.values.get(ix0), self.values.get(ix1)) else {
207 return false;
208 };
209 let to_slice = {
210 if let Some(end) = &from.end {
211 end.as_slice()
212 } else {
213 to.start.as_slice()
214 }
215 };
216 (from.start.as_slice(), to_slice, t).to_path(self.is_closed, path);
217 true
218 }
219}
220
221/// Animated repeater effect.
222#[derive(Clone, Debug)]
223pub struct Repeater {
224 /// Number of times elements should be repeated.
225 pub copies: Value<f64>,
226 /// Offset applied to each element.
227 pub offset: Value<f64>,
228 /// Anchor point.
229 pub anchor_point: Value<Point>,
230 /// Translation.
231 pub position: Value<Point>,
232 /// Rotation in degrees.
233 pub rotation: Value<f64>,
234 /// Scale.
235 pub scale: Value<Vec2>,
236 /// Opacity of the first element.
237 pub start_opacity: Value<f64>,
238 /// Opacity of the last element.
239 pub end_opacity: Value<f64>,
240}
241
242impl Repeater {
243 /// Returns true if the repeater contains no animated properties.
244 pub fn is_fixed(&self) -> bool {
245 self.copies.is_fixed()
246 && self.offset.is_fixed()
247 && self.anchor_point.is_fixed()
248 && self.position.is_fixed()
249 && self.rotation.is_fixed()
250 && self.scale.is_fixed()
251 && self.start_opacity.is_fixed()
252 && self.end_opacity.is_fixed()
253 }
254
255 /// Evaluates the repeater at the specified frame.
256 pub fn evaluate(&self, frame: f64) -> fixed::Repeater {
257 let copies = self.copies.evaluate(frame).round() as usize;
258 let offset = self.offset.evaluate(frame);
259 let anchor_point = self.anchor_point.evaluate(frame);
260 let position = self.position.evaluate(frame);
261 let rotation = self.rotation.evaluate(frame);
262 let scale = self.scale.evaluate(frame);
263 let start_opacity = self.start_opacity.evaluate(frame);
264 let end_opacity = self.end_opacity.evaluate(frame);
265 fixed::Repeater {
266 copies,
267 offset,
268 anchor_point,
269 position,
270 rotation,
271 scale,
272 start_opacity,
273 end_opacity,
274 }
275 }
276
277 /// Converts the animated value to its model representation.
278 pub fn into_model(self) -> super::Repeater {
279 if self.is_fixed() {
280 super::Repeater::Fixed(self.evaluate(0.0))
281 } else {
282 super::Repeater::Animated(self)
283 }
284 }
285}
286
287/// Animated stroke properties.
288#[derive(Clone, Debug)]
289pub struct Stroke {
290 /// Width of the stroke.
291 pub width: Value<f64>,
292 /// Join style.
293 pub join: kurbo::Join,
294 /// Limit for miter joins.
295 pub miter_limit: Option<f64>,
296 /// Cap style.
297 pub cap: kurbo::Cap,
298}
299
300impl Stroke {
301 /// Returns true if the stroke is fixed.
302 pub fn is_fixed(&self) -> bool {
303 self.width.is_fixed()
304 }
305
306 /// Evaluates the stroke at the specified frame.
307 pub fn evaluate(&self, frame: f64) -> kurbo::Stroke {
308 let width = self.width.evaluate(frame);
309 let mut stroke = kurbo::Stroke::new(width)
310 .with_caps(self.cap)
311 .with_join(self.join);
312 if let Some(miter_limit) = self.miter_limit {
313 stroke.miter_limit = miter_limit;
314 }
315 stroke
316 }
317
318 /// Converts the animated value to its model representation.
319 pub fn into_model(self) -> super::Stroke {
320 if self.is_fixed() {
321 super::Stroke::Fixed(self.evaluate(0.0))
322 } else {
323 super::Stroke::Animated(self)
324 }
325 }
326}
327
328/// Animated linear or radial gradient.
329#[derive(Clone, Debug)]
330pub struct Gradient {
331 /// True if the gradient is radial.
332 pub is_radial: bool,
333 /// Starting point.
334 pub start_point: Value<Point>,
335 /// Ending point.
336 pub end_point: Value<Point>,
337 /// Stop offsets and color values.
338 pub stops: super::ColorStops,
339}
340
341impl Gradient {
342 /// Returns true if the value contains no animated properties.
343 pub fn is_fixed(&self) -> bool {
344 self.start_point.is_fixed() && self.end_point.is_fixed() && self.stops.is_fixed()
345 }
346
347 /// Evaluates the animated value at the given frame.
348 pub fn evaluate(&self, frame: f64) -> peniko::Brush {
349 let start = self.start_point.evaluate(frame);
350 let end = self.end_point.evaluate(frame);
351 let stops = self.stops.evaluate(frame).into_owned();
352 if self.is_radial {
353 let radius = (end.to_vec2() - start.to_vec2()).hypot();
354 let mut grad = peniko::Gradient::new_radial(start, radius as f32);
355 grad.stops = stops;
356 grad.into()
357 } else {
358 let mut grad = peniko::Gradient::new_linear(start, end);
359 grad.stops = stops;
360 grad.into()
361 }
362 }
363}
364
365#[derive(Clone, Debug)]
366pub struct ColorStops {
367 pub frames: Vec<Time>,
368 pub values: Vec<Vec<f64>>,
369 pub count: usize,
370}
371
372impl ColorStops {
373 pub fn evaluate(&self, frame: f64) -> fixed::ColorStops {
374 self.evaluate_inner(frame).unwrap_or_default()
375 }
376
377 fn evaluate_inner(&self, frame: f64) -> Option<fixed::ColorStops> {
378 let ([ix0, ix1], t, easing, hold) = Time::frames_and_weight(&self.frames, frame)?;
379
380 let v0 = self.values.get(ix0)?;
381 let v1 = self.values.get(ix1)?;
382
383 let mut stops: fixed::ColorStops = Default::default();
384 for i in 0..self.count {
385 let j = i * 5;
386 let offset = v0.get(j)?.tween(v1.get(j)?, t, &easing);
387 let t = if hold { 0.0 } else { t };
388 let r = v0.get(j + 1)?.tween(v1.get(j + 1)?, t, &easing);
389 let g = v0.get(j + 2)?.tween(v1.get(j + 2)?, t, &easing);
390 let b = v0.get(j + 3)?.tween(v1.get(j + 3)?, t, &easing);
391 let a = v0.get(j + 4)?.tween(v1.get(j + 4)?, t, &easing);
392 let stop = peniko::ColorStop::from((
393 offset as f32,
394 peniko::Color::new([r as f32, g as f32, b as f32, a as f32]),
395 ));
396 stops.push(stop);
397 }
398 Some(stops)
399 }
400}
401
402/// Animated brush.
403#[derive(Clone, Debug)]
404pub enum Brush {
405 /// Solid color.
406 Solid(Value<Color>),
407 /// Gradient color.
408 Gradient(Gradient),
409}
410
411impl Brush {
412 /// Returns true if the value contains no animated properties.
413 pub fn is_fixed(&self) -> bool {
414 match self {
415 Self::Solid(value) => value.is_fixed(),
416 Self::Gradient(value) => value.is_fixed(),
417 }
418 }
419
420 /// Evaluates the animation at the specified time.
421 pub fn evaluate(&self, alpha: f64, frame: f64) -> fixed::Brush {
422 match self {
423 Self::Solid(value) => value
424 .evaluate_or(frame, peniko::Color::TRANSPARENT)
425 .multiply_alpha(alpha as f32)
426 .into(),
427 Self::Gradient(value) => value.evaluate(frame),
428 }
429 }
430
431 /// Converts the animated value to its model representation.
432 pub fn into_model(self) -> super::Brush {
433 if self.is_fixed() {
434 super::Brush::Fixed(self.evaluate(1.0, 0.0))
435 } else {
436 super::Brush::Animated(self)
437 }
438 }
439}
440
441#[derive(Clone, Debug)]
442pub struct Trim {
443 /// Start of the visible segment (0.0 to 100.0).
444 pub start: Value<f64>,
445 /// End of the visible segment (0.0 to 100.0).
446 pub end: Value<f64>,
447 /// Offset rotation in degrees.
448 pub offset: Value<f64>,
449}
450
451impl Trim {
452 /// Returns true if the trim is fixed.
453 pub fn is_fixed(&self) -> bool {
454 self.start.is_fixed() && self.end.is_fixed() && self.offset.is_fixed()
455 }
456
457 /// Evaluates the trim at the specified frame.
458 pub fn evaluate(&self, frame: f64) -> fixed::Trim {
459 fixed::Trim {
460 start: self.start.evaluate(frame),
461 end: self.end.evaluate(frame),
462 offset: self.offset.evaluate(frame),
463 }
464 }
465
466 /// Converts the animated value to its model representation.
467 pub fn into_model(self) -> super::Trim {
468 if self.is_fixed() {
469 super::Trim::Fixed(self.evaluate(0.0))
470 } else {
471 super::Trim::Animated(self)
472 }
473 }
474}