1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::{Duration, Instant};
4
5use sequoia_animation::{Animator, Easing, Spring};
6use sequoia_core::{Length, Vec2};
7use sequoia_layout::LayoutStyle;
8
9use crate::any::AnyView;
10use crate::view::{PaintCtx, View};
11
12pub const MAX_ANIM_DT: f32 = 1.0 / 20.0;
13
14pub fn fade_sample(from: f32, delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
15 let from = from.clamp(0.0, 1.0);
16 let delay = delay.max(0.0);
17 if elapsed < delay {
18 return from;
19 }
20 if duration <= 0.0 {
21 return 1.0;
22 }
23 let t = (elapsed - delay) / duration;
24 if t <= 0.0 {
25 return from;
26 }
27 if t >= 1.0 {
28 return 1.0;
29 }
30 from + (1.0 - from) * easing.apply(t)
31}
32
33pub fn slide_progress(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
34 let delay = delay.max(0.0);
35 if elapsed < delay {
36 return 0.0;
37 }
38 if duration <= 0.0 {
39 return 1.0;
40 }
41 let t = (elapsed - delay) / duration;
42 if t <= 0.0 {
43 return 0.0;
44 }
45 if t >= 1.0 {
46 return 1.0;
47 }
48 easing.apply(t)
49}
50
51fn wall_elapsed(clock: &Rc<RefCell<Option<Instant>>>) -> f32 {
52 let now = Instant::now();
53 let mut slot = clock.borrow_mut();
54 let start = *slot.get_or_insert(now);
55 now.duration_since(start).as_secs_f32().min(600.0)
56}
57
58#[derive(Clone)]
59pub struct Opacity {
60 child: AnyView,
61 opacity: f32,
62}
63
64impl Opacity {
65 pub fn new<V: View + 'static>(child: V, opacity: f32) -> Self {
66 Self {
67 child: AnyView::new(child),
68 opacity,
69 }
70 }
71
72 pub fn opacity(&self) -> f32 {
73 self.opacity.clamp(0.0, 1.0)
74 }
75}
76
77impl View for Opacity {
78 fn body(&self) -> impl View + 'static {
79 crate::views::Unit
80 }
81 fn children_override(&self) -> Option<Vec<AnyView>> {
82 Some(vec![self.child.clone()])
83 }
84 fn layout_style(&self) -> LayoutStyle {
85 LayoutStyle {
86 width: Length::Fit,
87 height: Length::Fit,
88 ..LayoutStyle::default()
89 }
90 }
91 fn paint_opacity(&self) -> Option<f32> {
92 Some(self.opacity())
93 }
94}
95
96#[derive(Clone)]
97pub struct Translate {
98 child: AnyView,
99 offset: Vec2,
100}
101
102impl Translate {
103 pub fn new<V: View + 'static>(child: V, dx: f32, dy: f32) -> Self {
104 Self {
105 child: AnyView::new(child),
106 offset: Vec2::new(dx, dy),
107 }
108 }
109
110 pub fn offset(&self) -> Vec2 {
111 self.offset
112 }
113}
114
115impl View for Translate {
116 fn body(&self) -> impl View + 'static {
117 crate::views::Unit
118 }
119 fn children_override(&self) -> Option<Vec<AnyView>> {
120 Some(vec![self.child.clone()])
121 }
122 fn layout_style(&self) -> LayoutStyle {
123 LayoutStyle {
124 width: Length::Fit,
125 height: Length::Fit,
126 ..LayoutStyle::default()
127 }
128 }
129 fn paint_offset(&self) -> Vec2 {
130 self.offset
131 }
132}
133
134pub fn pop_sample(
135 from: Vec2,
136 fade_from: f32,
137 spring: Spring,
138 delay: f32,
139 elapsed: f32,
140) -> (f32, Vec2) {
141 let fade_from = fade_from.clamp(0.0, 1.0);
142 let delay = delay.max(0.0);
143 if elapsed <= delay {
144 return (fade_from, from);
145 }
146 let p = spring.value(elapsed - delay);
147 let opacity = (fade_from + (1.0 - fade_from) * p).clamp(0.0, 1.0);
148 let offset = Vec2::new(from.x * (1.0 - p), from.y * (1.0 - p));
149 (opacity, offset)
150}
151
152pub fn pulse_sample(min_opacity: f32, period: f32, elapsed: f32) -> f32 {
153 let min = min_opacity.clamp(0.0, 1.0);
154 let period = period.max(1e-3);
155 let phase = (elapsed.max(0.0) / period) % 1.0;
156
157 let wave = 0.5 - 0.5 * (phase * std::f32::consts::TAU).cos();
158 min + (1.0 - min) * wave
159}
160
161#[derive(Clone)]
162pub struct Pulse {
163 child: AnyView,
164 min_opacity: f32,
165 period: f32,
166 clock: Rc<RefCell<Option<Instant>>>,
167}
168
169impl Pulse {
170 pub fn new<V: View + 'static>(child: V) -> Self {
171 Self {
172 child: AnyView::new(child),
173 min_opacity: 0.35,
174 period: 1.2,
175 clock: Rc::new(RefCell::new(None)),
176 }
177 }
178
179 pub fn min_opacity(mut self, opacity: f32) -> Self {
180 self.min_opacity = opacity.clamp(0.0, 1.0);
181 self
182 }
183
184 pub fn period_duration(mut self, d: Duration) -> Self {
185 self.period = d.as_secs_f32().max(1e-3);
186 self
187 }
188
189 pub fn sample_at(&self, elapsed_secs: f32) -> f32 {
190 pulse_sample(self.min_opacity, self.period, elapsed_secs)
191 }
192
193 fn current(&self) -> f32 {
194 self.sample_at(wall_elapsed(&self.clock))
195 }
196}
197
198impl View for Pulse {
199 fn body(&self) -> impl View + 'static {
200 crate::views::Unit
201 }
202 fn children_override(&self) -> Option<Vec<AnyView>> {
203 Some(vec![self.child.clone()])
204 }
205 fn layout_style(&self) -> LayoutStyle {
206 LayoutStyle {
207 width: Length::Fit,
208 height: Length::Fit,
209 ..LayoutStyle::default()
210 }
211 }
212 fn paint_opacity(&self) -> Option<f32> {
213 Some(self.current())
214 }
215 fn paint_self(&self, ctx: &mut PaintCtx) {
216 ctx.request_frame();
217 }
218}
219
220pub fn shake_sample(amplitude: f32, period: f32, duration: f32, delay: f32, elapsed: f32) -> Vec2 {
221 let delay = delay.max(0.0);
222 if duration <= 0.0 || elapsed <= delay {
223 return Vec2::ZERO;
224 }
225 let t = elapsed - delay;
226 if t >= duration {
227 return Vec2::ZERO;
228 }
229 let decay = 1.0 - t / duration;
230 let period = period.max(1e-3);
231 let x = (t / period * std::f32::consts::TAU).sin() * amplitude * decay;
232 Vec2::new(x, 0.0)
233}
234
235#[derive(Clone)]
236pub struct Shake {
237 child: AnyView,
238 amplitude: f32,
239 period: f32,
240 duration: f32,
241 delay: f32,
242 clock: Rc<RefCell<Option<Instant>>>,
243}
244
245impl Shake {
246 pub fn new<V: View + 'static>(child: V) -> Self {
247 Self {
248 child: AnyView::new(child),
249 amplitude: 8.0,
250 period: 0.08,
251 duration: 0.36,
252 delay: 0.0,
253 clock: Rc::new(RefCell::new(None)),
254 }
255 }
256
257 pub fn amplitude(mut self, px: f32) -> Self {
258 self.amplitude = px;
259 self
260 }
261
262 pub fn period_duration(mut self, d: Duration) -> Self {
263 self.period = d.as_secs_f32().max(1e-3);
264 self
265 }
266
267 pub fn total_duration(mut self, d: Duration) -> Self {
268 self.duration = d.as_secs_f32().max(0.0);
269 self
270 }
271
272 pub fn delay_duration(mut self, d: Duration) -> Self {
273 self.delay = d.as_secs_f32().max(0.0);
274 self
275 }
276
277 pub fn sample_at(&self, elapsed_secs: f32) -> Vec2 {
278 shake_sample(
279 self.amplitude,
280 self.period,
281 self.duration,
282 self.delay,
283 elapsed_secs,
284 )
285 }
286
287 fn current(&self) -> (Vec2, bool) {
288 let elapsed = wall_elapsed(&self.clock);
289 let offset = self.sample_at(elapsed);
290 let running = elapsed < self.delay + self.duration;
291 (offset, running)
292 }
293}
294
295impl View for Shake {
296 fn body(&self) -> impl View + 'static {
297 crate::views::Unit
298 }
299 fn children_override(&self) -> Option<Vec<AnyView>> {
300 Some(vec![self.child.clone()])
301 }
302 fn layout_style(&self) -> LayoutStyle {
303 LayoutStyle {
304 width: Length::Fit,
305 height: Length::Fit,
306 ..LayoutStyle::default()
307 }
308 }
309 fn paint_offset(&self) -> Vec2 {
310 self.current().0
311 }
312 fn paint_self(&self, ctx: &mut PaintCtx) {
313 if self.current().1 {
314 ctx.request_frame();
315 }
316 }
317}
318
319const TICK_MEMO_WINDOW: Duration = Duration::from_millis(1);
320
321#[derive(Clone)]
322pub struct Animated {
323 child: AnyView,
324 animator: Animator,
325 clock: Rc<RefCell<Option<Instant>>>,
326 memo: Rc<RefCell<(Option<Instant>, bool)>>,
327}
328
329impl Animated {
330 pub fn new<V: View + 'static>(child: V, animator: Animator) -> Self {
331 Self {
332 child: AnyView::new(child),
333 animator,
334 clock: Rc::new(RefCell::new(None)),
335 memo: Rc::new(RefCell::new((None, false))),
336 }
337 }
338
339 pub fn animator(&self) -> Animator {
340 self.animator.clone()
341 }
342
343 fn tick(&self) -> bool {
344 let now = Instant::now();
345 {
346 let memo = self.memo.borrow();
347 if let (Some(last), running) = (memo.0, memo.1)
348 && now.saturating_duration_since(last) < TICK_MEMO_WINDOW
349 {
350 return running;
351 }
352 }
353 let dt = {
354 let mut slot = self.clock.borrow_mut();
355 let dt = slot
356 .map(|prev| (now - prev).as_secs_f32())
357 .unwrap_or(0.0)
358 .min(MAX_ANIM_DT);
359 *slot = Some(now);
360 dt
361 };
362 let running = self.animator.clone().advance(f64::from(dt));
363 *self.memo.borrow_mut() = (Some(now), running);
364 running
365 }
366}
367
368impl View for Animated {
369 fn body(&self) -> impl View + 'static {
370 crate::views::Unit
371 }
372 fn children_override(&self) -> Option<Vec<AnyView>> {
373 Some(vec![self.child.clone()])
374 }
375 fn layout_style(&self) -> LayoutStyle {
376 LayoutStyle {
377 width: Length::Fit,
378 height: Length::Fit,
379 ..LayoutStyle::default()
380 }
381 }
382 fn paint_opacity(&self) -> Option<f32> {
383 self.tick();
384 self.animator.opacity()
385 }
386 fn paint_offset(&self) -> Vec2 {
387 self.tick();
388 let p = self.animator.offset();
389 Vec2::new(p.x, p.y)
390 }
391 fn paint_self(&self, ctx: &mut PaintCtx) {
392 if self.tick() {
393 ctx.request_frame();
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn default_fade_at(elapsed: f32) -> f32 {
403 fade_sample(0.0, 0.0, 0.25, Easing::EaseOutCubic, elapsed)
404 }
405
406 #[test]
407 fn fade_sample_walks_from_to_one() {
408 assert_eq!(default_fade_at(-1.0), 0.0);
409 assert_eq!(default_fade_at(0.0), 0.0);
410 let mid = default_fade_at(0.125);
411 assert!(mid > 0.0 && mid < 1.0, "entrance center: {mid}");
412 assert_eq!(default_fade_at(10.0), 1.0);
413 }
414
415 #[test]
416 fn fade_sample_honors_from_delay_and_zero_duration() {
417 let at = |elapsed: f32| fade_sample(0.4, 0.5, 0.0, Easing::EaseOutCubic, elapsed);
418 assert_eq!(at(0.0), 0.4, "przed delayem trzyma `from`");
419 assert_eq!(at(10.0), 1.0, "zero time = immediately full");
420 }
421
422 fn default_slide_at(elapsed: f32) -> Vec2 {
423 let from = Vec2::new(0.0, 24.0);
424 let p = slide_progress(0.0, 0.3, Easing::EaseOutCubic, elapsed);
425 Vec2::new(from.x * (1.0 - p), from.y * (1.0 - p))
426 }
427
428 #[test]
429 fn slide_sample_returns_to_zero() {
430 let start = default_slide_at(0.0);
431 assert!((start.y - 24.0).abs() < 1e-4, "start: {start:?}");
432 let mid = default_slide_at(0.15);
433 assert!(mid.y > 0.0 && mid.y < 24.0, "middle: {mid:?}");
434 assert_eq!(default_slide_at(10.0), Vec2::ZERO);
435 }
436
437 #[test]
438 fn wrappers_report_paint_hooks() {
439 let o = Opacity::new(crate::views::Unit, 0.5);
440 assert_eq!(o.paint_opacity(), Some(0.5));
441 let o = Opacity::new(crate::views::Unit, 42.0);
442 assert_eq!(o.paint_opacity(), Some(1.0), "clamp do 1");
443
444 let t = Translate::new(crate::views::Unit, 3.0, -4.0);
445 assert_eq!(t.paint_offset(), Vec2::new(3.0, -4.0));
446 }
447
448 #[test]
449 fn html_easing_curves_drive_fade_and_slide() {
450 for ease in [
451 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
452 Easing::CubicBezier(0.3, 0.7, 0.3, 1.0),
453 Easing::CubicBezier(0.3, 1.3, 0.4, 1.0),
454 Easing::CubicBezier(0.3, 1.4, 0.4, 1.0),
455 Easing::CubicBezier(0.3, 1.2, 0.4, 1.0),
456 Easing::CubicBezier(0.4, 0.0, 0.2, 1.0),
457 ] {
458 let fade_at = |elapsed: f32| fade_sample(0.0, 0.0, 0.4, ease, elapsed);
459 assert_eq!(fade_at(0.0), 0.0);
460 assert_eq!(fade_at(10.0), 1.0);
461 let slide_at = |elapsed: f32| {
462 let p = slide_progress(0.0, 0.4, ease, elapsed);
463 Vec2::new(0.0, 10.0 * (1.0 - p))
464 };
465 assert!((slide_at(0.0).y - 10.0).abs() < 1e-4);
466 assert_eq!(slide_at(10.0), Vec2::ZERO);
467 }
468 }
469
470 #[test]
471 fn pop_sample_starts_at_from_and_overshoots_zero() {
472 let spring = Spring::new(0.4, 4.0);
473 let (op0, off0) = pop_sample(Vec2::new(0.0, 24.0), 0.0, spring, 0.0, 0.0);
474 assert_eq!(op0, 0.0);
475 assert_eq!(off0, Vec2::new(0.0, 24.0));
476
477 let mut min = f32::INFINITY;
478 let mut t = 0.0;
479 while t < 2.0 {
480 min = min.min(pop_sample(Vec2::new(0.0, 24.0), 0.0, spring, 0.0, t).1.y);
481 t += 0.005;
482 }
483 assert!(min < -0.5, "under-damped pop should overshoot: min={min}");
484 }
485
486 #[test]
487 fn pop_sample_reports_opacity_and_offset() {
488 let at = |elapsed: f32| {
489 pop_sample(
490 Vec2::new(0.0, 20.0),
491 0.2,
492 Spring::new(0.6, 5.0),
493 0.0,
494 elapsed,
495 )
496 };
497 assert_eq!(at(0.0).0, 0.2);
498 assert_eq!(at(0.0).1, Vec2::new(0.0, 20.0));
499
500 assert_eq!(at(10.0).0, 1.0);
501 assert_eq!(at(10.0).1, Vec2::ZERO);
502 }
503
504 #[test]
505 fn pulse_breathes_between_min_and_one() {
506 assert_eq!(pulse_sample(0.3, 1.0, 0.0), 0.3, "starts at min");
507 assert_eq!(pulse_sample(0.3, 1.0, 0.5), 1.0, "peak at half period");
508 assert_eq!(pulse_sample(0.3, 1.0, 1.0), 0.3, "wraps");
509 let p = Pulse::new(crate::views::Unit)
510 .min_opacity(0.2)
511 .period_duration(Duration::from_secs_f32(0.8));
512 assert!((p.sample_at(0.0) - 0.2).abs() < 1e-6);
513 let mut max = 0.0f32;
514 for i in 0..=100 {
515 max = max.max(p.sample_at(i as f32 * 0.008));
516 }
517 assert!((max - 1.0).abs() < 1e-3, "peak reaches 1.0: {max}");
518 assert_eq!(p.paint_opacity(), Some(0.2), "deterministic first sample");
519 }
520
521 #[test]
522 fn shake_decays_to_zero() {
523 assert_eq!(shake_sample(8.0, 0.1, 0.4, 0.0, 0.0), Vec2::ZERO);
524 let mut peak = 0.0f32;
525 let mut t = 0.0;
526 while t < 0.4 {
527 peak = peak.max(shake_sample(8.0, 0.1, 0.4, 0.0, t).x.abs());
528 t += 0.002;
529 }
530 assert!(peak > 3.0 && peak <= 8.0, "limits the amplitude: {peak}");
531 assert_eq!(shake_sample(8.0, 0.1, 0.4, 0.0, 0.4), Vec2::ZERO, "stops");
532 assert_eq!(
533 shake_sample(8.0, 0.1, 0.4, 0.0, 10.0),
534 Vec2::ZERO,
535 "stays put"
536 );
537 let s = Shake::new(crate::views::Unit).amplitude(4.0);
538 assert_eq!(s.sample_at(0.0), Vec2::ZERO);
539 }
540
541 #[test]
542 fn animated_applies_a_hosted_animation() {
543 use sequoia_animation::{Animation, Animator, Keyframe, Track, Value};
544 let clip = Animation::new("enter", 0.4)
545 .with_track(
546 "x",
547 Track::try_new(vec![
548 Keyframe::new(0.0, Value::px(10.0)),
549 Keyframe::new(0.4, Value::px(50.0)),
550 ])
551 .unwrap(),
552 )
553 .with_track(
554 "opacity",
555 Track::try_new(vec![
556 Keyframe::new(0.0, Value::f32(0.5)),
557 Keyframe::new(0.4, Value::f32(1.0)),
558 ])
559 .unwrap(),
560 );
561 let player = Animator::new().with_clip(clip);
562 let view = Animated::new(crate::views::Unit, player);
563
564 let opacity = view.paint_opacity().expect("clip has an opacity track");
565 assert!((0.5..0.55).contains(&opacity), "opacity: {opacity}");
566 let offset = view.paint_offset();
567 assert!((10.0..12.0).contains(&offset.x), "offset: {offset:?}");
568 assert_eq!(offset.y, 0.0);
569 assert!(view.animator().is_playing());
570 }
571
572 #[test]
573 fn animated_coalesces_same_frame_ticks() {
574 use sequoia_animation::{Animation, Animator, Keyframe, Track, Value};
575 let clip = Animation::new("enter", 0.4).with_track(
576 "opacity",
577 Track::try_new(vec![
578 Keyframe::new(0.0, Value::f32(0.5)),
579 Keyframe::new(0.4, Value::f32(1.0)),
580 ])
581 .unwrap(),
582 );
583 let player = Animator::new().with_clip(clip);
584 let view = Animated::new(crate::views::Unit, player);
585 let _ = view.paint_opacity();
586 let e1 = view.animator().elapsed();
587
588 let _ = view.paint_offset();
589 let e2 = view.animator().elapsed();
590 assert_eq!(e1, e2, "tick w tej samej klatce nie kroczy: {e1} vs {e2}");
591 }
592}