aura_anim_iced/
keyframes.rs1mod 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#[derive(Debug, Clone, PartialEq, Default)]
48pub struct Keyframes {
49 tracks: Vec<PropertyTrack>,
50 timing: Timing,
51}
52
53impl Keyframes {
54 #[must_use]
56 pub fn builder() -> KeyframesBuilder {
57 KeyframesBuilder::new()
58 }
59
60 #[must_use]
62 pub fn is_empty(&self) -> bool {
63 self.tracks.is_empty()
64 }
65
66 #[must_use]
68 pub fn track_count(&self) -> usize {
69 self.tracks.len()
70 }
71
72 #[must_use]
74 pub fn timing(&self) -> &Timing {
75 &self.timing
76 }
77
78 #[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#[derive(Debug, Clone, PartialEq, Default)]
120pub struct KeyframesBuilder {
121 frames: Vec<Keyframe>,
122 timing: Timing,
123}
124
125impl KeyframesBuilder {
126 #[must_use]
128 pub fn new() -> Self {
129 Self::default()
130 }
131
132 #[must_use]
134 pub const fn timing(&self) -> &Timing {
135 &self.timing
136 }
137
138 #[must_use]
140 pub const fn with_timing(mut self, timing: Timing) -> Self {
141 self.timing = timing;
142 self
143 }
144
145 #[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 #[must_use]
154 pub fn opacity(self, offset: f32, value: f32) -> Self {
155 self.at(offset, (property::OPACITY, value))
156 }
157
158 #[must_use]
160 pub fn scale(self, offset: f32, value: f32) -> Self {
161 self.at(offset, (property::SCALE, value))
162 }
163
164 #[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 #[must_use]
172 pub fn background_color(self, offset: f32, value: iced::Color) -> Self {
173 self.at(offset, (property::BACKGROUND, value))
174 }
175
176 #[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 #[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 #[must_use]
190 pub fn shadow(self, offset: f32, value: iced::Shadow) -> Self {
191 self.at(offset, (property::SHADOW, value))
192 }
193
194 pub fn push_at(&mut self, offset: f32, snapshot: impl Into<PropertySnapshot>) {
196 self.upsert_frame(Keyframe::new(offset, snapshot.into()));
197 }
198
199 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 #[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}