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}