Skip to main content

aura_anim_iced/
keyframes.rs

1//! Property-snapshot keyframe storage and lookup.
2
3mod frame;
4mod sample;
5#[cfg(test)]
6mod segment;
7#[cfg(test)]
8mod tests;
9mod track;
10
11use std::collections::HashMap;
12
13pub use frame::{Keyframe, normalize_offset};
14
15use crate::{
16    PropertySnapshot, Timing,
17    keyframes::{
18        sample::{sample_frames, sample_frames_into},
19        track::PropertyTrack,
20    },
21    nearly_equal_f32, property,
22};
23
24/// A collection of property snapshots keyed by normalized offsets.
25///
26/// `Keyframes` is the compact, sampled representation used by timelines and
27/// the runtime. Build one with [`KeyframesBuilder`].
28///
29/// # Example
30///
31/// ```
32/// use aura_anim_iced::{Easing, KeyframesBuilder, Timing, property};
33///
34/// let keyframes = KeyframesBuilder::new()
35///     .with_timing(Timing::new(180.0).with_easing(Easing::EaseOut))
36///     .at(0.0, (property::OPACITY, 0.0))
37///     .at(0.0, (property::SCALE, 0.95))
38///     .at(1.0, (property::OPACITY, 1.0))
39///     .at(1.0, (property::SCALE, 1.0))
40///     .finish();
41///
42/// let sample = keyframes.sample_at(0.5).expect("active sample");
43///
44/// assert!(sample.find_property(&property::OPACITY.raw()).is_some());
45/// assert!(sample.find_property(&property::SCALE.raw()).is_some());
46/// ```
47#[derive(Debug, Clone, PartialEq, Default)]
48pub struct Keyframes {
49    tracks: Vec<PropertyTrack>,
50    timing: Timing,
51}
52
53impl Keyframes {
54    /// Creates a keyframe builder.
55    #[must_use]
56    pub fn builder() -> KeyframesBuilder {
57        KeyframesBuilder::new()
58    }
59
60    /// Returns `true` when this keyframe set has no property tracks.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.tracks.is_empty()
64    }
65
66    /// Returns the number of property tracks in this keyframe set.
67    #[must_use]
68    pub fn track_count(&self) -> usize {
69        self.tracks.len()
70    }
71
72    /// Returns the timing attached to this keyframe set.
73    #[must_use]
74    pub fn timing(&self) -> &Timing {
75        &self.timing
76    }
77
78    /// Samples all property tracks at a normalized offset.
79    #[must_use]
80    pub fn sample_at(&self, offset: f32) -> Option<PropertySnapshot> {
81        sample_frames(&self.tracks, offset, self.timing.easing())
82    }
83
84    pub(crate) fn sample_into(&self, offset: f32, output: &mut PropertySnapshot) -> bool {
85        sample_frames_into(&self.tracks, offset, self.timing.easing(), output)
86    }
87
88    pub(crate) fn sample_completion(&self) -> Option<PropertySnapshot> {
89        let iteration_count = self.timing.iterations().finite_count()?;
90        let offset = self.timing.direction().end_progress(iteration_count);
91
92        #[allow(
93            clippy::cast_possible_truncation,
94            reason = "Normalized keyframe offsets are stored as f32 throughout the keyframe module."
95        )]
96        self.sample_at(offset as f32)
97    }
98
99    pub(crate) fn sample_completion_into(&self, output: &mut PropertySnapshot) -> bool {
100        let Some(iteration_count) = self.timing.iterations().finite_count() else {
101            output.clear();
102            return false;
103        };
104        let offset = self.timing.direction().end_progress(iteration_count);
105
106        #[allow(
107            clippy::cast_possible_truncation,
108            reason = "Normalized keyframe offsets are stored as f32 throughout the keyframe module."
109        )]
110        self.sample_into(offset as f32, output)
111    }
112}
113
114/// Builder that collects offset-keyed snapshots before compiling them into
115/// per-property tracks.
116///
117/// Duplicate offsets are merged as snapshots are inserted. If the same property
118/// appears more than once at an offset, the later value wins.
119#[derive(Debug, Clone, PartialEq, Default)]
120pub struct KeyframesBuilder {
121    frames: Vec<Keyframe>,
122    timing: Timing,
123}
124
125impl KeyframesBuilder {
126    /// Creates an empty keyframe builder with default timing.
127    #[must_use]
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Returns the timing that will be attached to the finished keyframes.
133    #[must_use]
134    pub const fn timing(&self) -> &Timing {
135        &self.timing
136    }
137
138    /// Sets the timing attached to the finished keyframes.
139    #[must_use]
140    pub const fn with_timing(mut self, timing: Timing) -> Self {
141        self.timing = timing;
142        self
143    }
144
145    /// Inserts a property snapshot at a normalized offset.
146    #[must_use]
147    pub fn at(mut self, offset: f32, snapshot: impl Into<PropertySnapshot>) -> Self {
148        self.push_at(offset, snapshot);
149        self
150    }
151
152    /// Inserts an opacity keyframe.
153    #[must_use]
154    pub fn opacity(self, offset: f32, value: f32) -> Self {
155        self.at(offset, (property::OPACITY, value))
156    }
157
158    /// Inserts a uniform scale keyframe.
159    #[must_use]
160    pub fn scale(self, offset: f32, value: f32) -> Self {
161        self.at(offset, (property::SCALE, value))
162    }
163
164    /// Inserts a translation keyframe.
165    #[must_use]
166    pub fn translation(self, offset: f32, x: f32, y: f32) -> Self {
167        self.at(offset, (property::TRANSLATE, iced::Vector::new(x, y)))
168    }
169
170    /// Inserts a background color keyframe.
171    #[must_use]
172    pub fn background_color(self, offset: f32, value: iced::Color) -> Self {
173        self.at(offset, (property::BACKGROUND, value))
174    }
175
176    /// Inserts a border color keyframe.
177    #[must_use]
178    pub fn border_color(self, offset: f32, value: iced::Color) -> Self {
179        self.at(offset, (property::BORDER_COLOR, value))
180    }
181
182    /// Inserts a text color keyframe.
183    #[must_use]
184    pub fn text_color(self, offset: f32, value: iced::Color) -> Self {
185        self.at(offset, (property::TEXT_COLOR, value))
186    }
187
188    /// Inserts a shadow keyframe.
189    #[must_use]
190    pub fn shadow(self, offset: f32, value: iced::Shadow) -> Self {
191        self.at(offset, (property::SHADOW, value))
192    }
193
194    /// Inserts a property snapshot at a normalized offset.
195    pub fn push_at(&mut self, offset: f32, snapshot: impl Into<PropertySnapshot>) {
196        self.upsert_frame(Keyframe::new(offset, snapshot.into()));
197    }
198
199    /// Inserts multiple property snapshots and normalizes them in one pass.
200    pub fn push_many<I, S>(&mut self, frames: I)
201    where
202        I: IntoIterator<Item = (f32, S)>,
203        S: Into<PropertySnapshot>,
204    {
205        self.frames.extend(
206            frames
207                .into_iter()
208                .map(|(offset, snapshot)| Keyframe::new(offset, snapshot.into())),
209        );
210        self.sort_and_merge_frames();
211    }
212
213    fn upsert_frame(&mut self, frame: Keyframe) {
214        if let Some(existing) = self
215            .frames
216            .iter_mut()
217            .find(|existing| nearly_equal_f32(existing.offset(), frame.offset()))
218        {
219            existing.merge_snapshot(frame.snapshot().clone());
220            return;
221        }
222
223        let insert_at = self
224            .frames
225            .partition_point(|existing| existing.offset() < frame.offset());
226        self.frames.insert(insert_at, frame);
227    }
228
229    fn sort_and_merge_frames(&mut self) {
230        self.frames
231            .sort_by(|left, right| left.offset().total_cmp(&right.offset()));
232
233        let mut merged = Vec::with_capacity(self.frames.len());
234
235        for frame in self.frames.drain(..) {
236            if let Some(existing) = merged.last_mut().filter(|existing: &&mut Keyframe| {
237                nearly_equal_f32(existing.offset(), frame.offset())
238            }) {
239                existing.merge_snapshot(frame.snapshot().clone());
240            } else {
241                merged.push(frame);
242            }
243        }
244
245        self.frames = merged;
246    }
247
248    /// Compiles the collected snapshots into per-property tracks.
249    #[must_use]
250    pub fn finish(self) -> Keyframes {
251        let mut track_map = HashMap::new();
252
253        for frame in &self.frames {
254            for entry in frame.snapshot().entries() {
255                let track = track_map
256                    .entry(*entry.spec())
257                    .or_insert_with(|| PropertyTrack::new(*entry.spec(), Vec::new()));
258                track.add_sample(frame.offset(), *entry.value());
259            }
260        }
261
262        let mut tracks: Vec<PropertyTrack> = track_map
263            .into_values()
264            .map(|mut track| {
265                track.sort_samples();
266                track
267            })
268            .collect();
269        tracks.sort_by_key(|track| track.spec().composition_order());
270
271        Keyframes {
272            tracks,
273            timing: self.timing,
274        }
275    }
276}