Skip to main content

dinamika_core/shape/
tween.rs

1//! Handles of animatable properties returned by the shape's setter methods:
2//! [`Tween`] for a single property, [`PaddingTween`] for all padding sides at
3//! once, [`TextEdit`] for text edits (with spawn, typing and smoothing
4//! animations) and [`HighlightEdit`] for range highlighting (animated via
5//! `over`).
6
7use std::ops::Deref;
8
9use crate::easing::Easing;
10use crate::signal::{Signal, Tweenable};
11use crate::timeline::{parallel, Action};
12
13use super::text::{HighlightStage, HighlightTween, TextMotion, TextTween};
14use super::{Padding, Shape, TextPos, NOT_TEXT};
15
16/// Binds the built action to its shape's timeline, so that a single animation
17/// can be added by simply writing it as an expression (see `Drop for Action`).
18/// If the shape is not registered on a timeline, the reference is empty and the
19/// action stays "free" — it can still be nested into [`sequence`]/[`parallel`]
20/// manually.
21fn bind_to_timeline(mut action: Action, shape: &Shape) -> Action {
22    action.attach_timeline(shape.timeline_weak());
23    action
24}
25
26/// A handle of an animatable property, returned by its setter method.
27///
28/// The value is already set (the setter applies it immediately), and the handle
29/// itself dereferences into [`Shape`] — so in the builder chain it behaves like
30/// an ordinary shape. To get an animation for the timeline instead of an instant
31/// set, append [`over`](Tween::over): it builds a tween from the previous value
32/// (captured **before** setting) to the new one.
33///
34/// ```
35/// # use dinamika_core::*;
36/// let tl = Timeline::new(320, 160, Color::BLACK, 30.0);
37/// let panel = Shape::rect().gap(20.0).on(&tl);
38/// // `gap(56.0)` sets 56 and returns a Tween; `over(...)` builds a tween 20 → 56.
39/// tl.parallel(vec![panel.gap(56.0).over(1.0, Easing::CubicInOut)]);
40/// ```
41pub struct Tween<T: Tweenable> {
42    shape: Shape,
43    signal: Signal<T>,
44    from: T,
45    to: T,
46}
47
48impl<T: Tweenable> Tween<T> {
49    /// Builds the handle over an already-set property. `from` is captured before
50    /// setting (called from the [`Shape`] setter method).
51    pub(super) fn new(shape: Shape, signal: Signal<T>, from: T, to: T) -> Self {
52        Tween { shape, signal, from, to }
53    }
54
55    /// Turns setting the property into an animation: a tween from the previous
56    /// value to the new one over `duration` seconds with easing `easing`.
57    /// Returns an [`Action`] for the timeline.
58    pub fn over(self, duration: f64, easing: Easing) -> Action {
59        let action = self.signal.tween_from(self.from, self.to, duration, easing);
60        bind_to_timeline(action, &self.shape)
61    }
62}
63
64impl<T: Tweenable> Deref for Tween<T> {
65    type Target = Shape;
66    fn deref(&self) -> &Shape {
67        &self.shape
68    }
69}
70
71impl<T: Tweenable> From<Tween<T>> for Shape {
72    fn from(t: Tween<T>) -> Shape {
73        t.shape
74    }
75}
76
77/// A handle of the [`padding`](Shape::padding) property — like [`Tween`], but
78/// animates all four padding sides at once. [`over`](PaddingTween::over) builds a
79/// parallel tween.
80pub struct PaddingTween {
81    shape: Shape,
82    from: Padding,
83    to: Padding,
84}
85
86impl PaddingTween {
87    /// Builds the handle over already-set padding (called from
88    /// [`Shape::padding`]).
89    pub(super) fn new(shape: Shape, from: Padding, to: Padding) -> Self {
90        PaddingTween { shape, from, to }
91    }
92
93    /// Animation of all four padding sides from the previous values to the new
94    /// ones over `duration` seconds (in parallel). Returns an [`Action`] for the
95    /// timeline.
96    pub fn over(self, duration: f64, easing: Easing) -> Action {
97        let action = {
98            let d = self.shape.inner.borrow();
99            parallel(vec![
100                d.pad_top.tween_from(self.from.top, self.to.top, duration, easing),
101                d.pad_right.tween_from(self.from.right, self.to.right, duration, easing),
102                d.pad_bottom.tween_from(self.from.bottom, self.to.bottom, duration, easing),
103                d.pad_left.tween_from(self.from.left, self.to.left, duration, easing),
104            ])
105        };
106        bind_to_timeline(action, &self.shape)
107    }
108}
109
110impl Deref for PaddingTween {
111    type Target = Shape;
112    fn deref(&self) -> &Shape {
113        &self.shape
114    }
115}
116
117impl From<PaddingTween> for Shape {
118    fn from(t: PaddingTween) -> Shape {
119        t.shape
120    }
121}
122
123/// A handle of a text edit, returned by the text shape's editor methods
124/// ([`content`](Shape::content), [`append`](Shape::append),
125/// [`prepend`](Shape::prepend), [`insert`](Shape::insert),
126/// [`rewrite`](Shape::rewrite)).
127///
128/// The edit is already applied (the committed text is updated immediately, like
129/// ordinary setters), and the handle itself dereferences into [`Shape`] — in the
130/// static builder chain it behaves like a shape. To turn the edit into a
131/// transition on the timeline, append an animation: [`spawn`](TextEdit::spawn)
132/// (instant), [`typing`](TextEdit::typing) (per-character typing) or
133/// [`smooth`](TextEdit::smooth) (a crossfade with a width morph).
134///
135/// ```
136/// # use dinamika_core::*;
137/// let tl = Timeline::new(320, 120, Color::BLACK, 30.0);
138/// let title = Shape::text("").on(&tl);
139/// // Type "Hello" over a second, then smoothly change to "Bye".
140/// tl.sequence(vec![
141///     title.content("Hello").typing(1.0, Easing::Linear),
142///     title.content("Bye").smooth(0.5, Easing::CubicInOut),
143/// ]);
144/// ```
145pub struct TextEdit {
146    shape: Shape,
147    /// The content before the edit — the "from" for typing and smoothing.
148    old: String,
149    /// The content after the edit — the "to" (already set on the shape).
150    new: String,
151}
152
153impl TextEdit {
154    /// Builds the handle over an already-applied edit (called from the [`Shape`]
155    /// editor methods). `old` is captured before setting the new content.
156    pub(super) fn new(shape: Shape, old: String, new: String) -> Self {
157        TextEdit { shape, old, new }
158    }
159
160    /// The shared animation-stage cell of the text shape.
161    fn stage(&self) -> std::rc::Rc<std::cell::RefCell<super::text::TextStage>> {
162        self.shape.inner.borrow().text.as_ref().expect(NOT_TEXT).stage_handle()
163    }
164
165    /// Applies one more edit on top of this one, **preserving the original
166    /// "from"**.
167    ///
168    /// The [`Shape`] editor methods capture `old` from the committed text at the
169    /// moment of the call, so several consecutive edits of one shape in a single
170    /// block cannot be animated separately (the second would see the result of
171    /// the first and "leak" before the animation starts). Chaining editors on the
172    /// handle itself, however, merges them into **one** edit: each step moves the
173    /// committed text toward the final via the shape's [`apply`](Shape), but `old`
174    /// stays the original — so `prepend(...).append(...).smooth(...)` morphs once
175    /// from the initial text to the final, rather than as two overlapping
176    /// transitions.
177    fn chain(self, apply: impl FnOnce(&Shape) -> TextEdit) -> TextEdit {
178        let next = apply(&self.shape).new;
179        TextEdit { shape: self.shape, old: self.old, new: next }
180    }
181
182    /// Fully replaces the content (see [`Shape::content`]), continuing this
183    /// handle's edit chain while preserving the original "from".
184    pub fn content(self, content: impl Into<String>) -> TextEdit {
185        self.chain(|s| s.content(content))
186    }
187
188    /// Appends `content` to the end (see [`Shape::append`]), continuing this
189    /// handle's edit chain.
190    pub fn append(self, content: impl Into<String>) -> TextEdit {
191        self.chain(|s| s.append(content))
192    }
193
194    /// Inserts `content` at the beginning (see [`Shape::prepend`]), continuing
195    /// this handle's edit chain.
196    pub fn prepend(self, content: impl Into<String>) -> TextEdit {
197        self.chain(|s| s.prepend(content))
198    }
199
200    /// Inserts `content` before character `char_index` (see [`Shape::insert`]),
201    /// continuing this handle's edit chain.
202    pub fn insert(self, char_index: usize, content: impl Into<String>) -> TextEdit {
203        self.chain(|s| s.insert(char_index, content))
204    }
205
206    /// Replaces the range `[from, to)` (see [`Shape::rewrite`]), continuing this
207    /// handle's edit chain.
208    pub fn rewrite(
209        self,
210        from: impl Into<super::TextPos>,
211        to: impl Into<super::TextPos>,
212        content: impl Into<String>,
213    ) -> TextEdit {
214        self.chain(|s| s.rewrite(from, to, content))
215    }
216
217    /// An instant spawn: the new text appears immediately at this moment of the
218    /// timeline. It has no duration, so `over`/`easing` are not needed; before
219    /// the moment the previous text is shown, after it — the new one.
220    pub fn spawn(self) -> Action {
221        let stage = self.stage();
222        let action = TextTween::action(stage, TextMotion::Spawn, self.old, self.new, 0.0, Easing::Linear);
223        bind_to_timeline(action, &self.shape)
224    }
225
226    /// Typing: characters appear one by one over `over` seconds (the speed is set
227    /// by the duration). The prefix already shared with the previous text stays
228    /// in place — only the added "tail" is typed; the block's width expands as it
229    /// is typed.
230    pub fn typing(self, over: f64, easing: Easing) -> Action {
231        let stage = self.stage();
232        let action = TextTween::action(stage, TextMotion::Typing, self.old, self.new, over, easing);
233        bind_to_timeline(action, &self.shape)
234    }
235
236    /// Smoothing (as in Motion Canvas): over `over` seconds the old text fades to
237    /// zero in the first half, the new one appears in the second half; all the
238    /// while the space for the text morphs from the old width to the new.
239    pub fn smooth(self, over: f64, easing: Easing) -> Action {
240        let stage = self.stage();
241        let action = TextTween::action(stage, TextMotion::Smooth, self.old, self.new, over, easing);
242        bind_to_timeline(action, &self.shape)
243    }
244}
245
246impl Deref for TextEdit {
247    type Target = Shape;
248    fn deref(&self) -> &Shape {
249        &self.shape
250    }
251}
252
253impl From<TextEdit> for Shape {
254    fn from(t: TextEdit) -> Shape {
255        t.shape
256    }
257}
258
259/// A handle of range highlighting, returned by [`highlight`](Shape::highlight)
260/// and [`clear_highlight`](Shape::clear_highlight).
261///
262/// The edit is already applied (the committed set of ranges is updated
263/// immediately), and the handle dereferences into [`Shape`] — in the static
264/// builder chain it behaves like a shape. Its main purpose, though, is
265/// animation: [`over`](HighlightEdit::over) turns the edit into a smooth
266/// transition on the timeline ("highlight/clear over so many seconds"). Several
267/// ranges are highlighted with several `highlight(..).over(..)` in a single
268/// [`parallel`](crate::parallel) — overlapping edits of one shape merge into one
269/// consistent transition from a common base to a common final.
270///
271/// ```
272/// # use dinamika_core::*;
273/// let tl = Timeline::new(320, 120, Color::BLACK, 30.0);
274/// let code = Shape::code("let answer = 42;").on(&tl);
275/// // Highlight two places at once over half a second:
276/// tl.parallel(vec![
277///     code.highlight(0, 3).over(0.5, Easing::CubicInOut),
278///     code.highlight(13, 15).over(0.5, Easing::CubicInOut),
279/// ]);
280/// // Later clear the highlighting smoothly:
281/// code.clear_highlight().over(0.5, Easing::CubicInOut);
282/// ```
283pub struct HighlightEdit {
284    shape: Shape,
285    /// The set of ranges before the edit — the transition's "from".
286    old: Vec<(TextPos, TextPos)>,
287    /// The set of ranges after the edit — the "to" (already committed on the shape).
288    new: Vec<(TextPos, TextPos)>,
289}
290
291impl HighlightEdit {
292    /// Builds the handle over an already-applied edit (called from
293    /// [`highlight`](Shape::highlight) / [`clear_highlight`](Shape::clear_highlight)).
294    /// `old` is captured before setting the new set.
295    pub(super) fn new(shape: Shape, old: Vec<(TextPos, TextPos)>, new: Vec<(TextPos, TextPos)>) -> Self {
296        HighlightEdit { shape, old, new }
297    }
298
299    /// The shared highlight-stage cell of the shape.
300    fn stage(&self) -> std::rc::Rc<std::cell::RefCell<HighlightStage>> {
301        self.shape.inner.borrow().text.as_ref().expect(NOT_TEXT).highlight_stage_handle()
302    }
303
304    /// Turns the highlight edit into an animation: over `over` seconds each
305    /// glyph's opacity smoothly transitions from the original set of ranges to
306    /// the new one (the highlight appears, is removed, or moves). Returns an
307    /// [`Action`] for the timeline.
308    pub fn over(self, over: f64, easing: Easing) -> Action {
309        let stage = self.stage();
310        let action = HighlightTween::action(stage, self.old, self.new, over, easing);
311        bind_to_timeline(action, &self.shape)
312    }
313}
314
315impl Deref for HighlightEdit {
316    type Target = Shape;
317    fn deref(&self) -> &Shape {
318        &self.shape
319    }
320}
321
322impl From<HighlightEdit> for Shape {
323    fn from(t: HighlightEdit) -> Shape {
324        t.shape
325    }
326}