Skip to main content

script/dom/animations/
animationeffect.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use cssparser::Parser;
6use dom_struct::dom_struct;
7use script_bindings::cell::DomRefCell;
8use script_bindings::codegen::GenericBindings::AnimationEffectBinding::{
9    AnimationEffectMethods, ComputedEffectTiming, EffectTiming, FillMode, OptionalEffectTiming,
10    PlaybackDirection,
11};
12use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
13use script_bindings::error::{Error, Fallible};
14use script_bindings::num::Finite;
15use script_bindings::reflector::Reflector;
16use script_bindings::root::Dom;
17use script_bindings::str::DOMString;
18use style::parser::Parse;
19use style::stylesheets::CssRuleType;
20use style::values::generics::easing::TimingKeyword;
21use style::values::specified::TimingFunction;
22use style_traits::{ParsingMode, ToCss};
23
24use crate::css::css::parser_context_for_document;
25use crate::dom::Window;
26use crate::dom::bindings::codegen::UnionTypes::UnrestrictedDoubleOrString;
27
28/// <https://drafts.csswg.org/web-animations-1/#animationeffect>
29#[dom_struct]
30pub(crate) struct AnimationEffect {
31    reflector: Reflector,
32
33    /// The window that this `AnimationEffect` was constructed in.
34    window: Dom<Window>,
35
36    specified_timing_properties: DomRefCell<SpecifiedTimingProperties>,
37}
38
39#[derive(Clone, JSTraceable, MallocSizeOf)]
40struct SpecifiedTimingProperties {
41    /// <https://drafts.csswg.org/web-animations-1/#start-delay>
42    start_delay: Finite<f64>,
43
44    /// <https://drafts.csswg.org/web-animations-1/#end-delay>
45    end_delay: Finite<f64>,
46
47    /// <https://drafts.csswg.org/web-animations-1/#fill-mode>
48    fill_mode: FillMode,
49
50    /// <https://drafts.csswg.org/web-animations-1/#iteration-count>
51    iteration_count: f64,
52
53    /// <https://drafts.csswg.org/web-animations-1/#iteration-start>
54    iteration_start: Finite<f64>,
55
56    /// <https://drafts.csswg.org/web-animations-1/#iteration-duration>
57    iteration_duration: IterationDurationOrAuto,
58
59    /// <https://drafts.csswg.org/web-animations-1/#playback-direction>
60    playback_direction: PlaybackDirection,
61
62    /// <https://drafts.csswg.org/css-easing-2/#easing-function>
63    #[no_trace]
64    easing_function: TimingFunction,
65}
66
67impl AnimationEffect {
68    pub(crate) fn new_inherited(window: &Window) -> Self {
69        Self {
70            reflector: Reflector::new(),
71            window: Dom::from_ref(window),
72
73            // The default values of the timing properties specified here don't matter.
74            // There is no way to construct a AnimationEffect without subsequently initializing them,
75            // even if they're not passed to the constructor.
76            specified_timing_properties: DomRefCell::new(SpecifiedTimingProperties {
77                start_delay: Default::default(),
78                end_delay: Default::default(),
79                fill_mode: FillMode::None,
80                iteration_count: Default::default(),
81                iteration_start: Default::default(),
82                iteration_duration: IterationDurationOrAuto::Auto,
83                playback_direction: PlaybackDirection::Normal,
84                easing_function: TimingFunction::Keyword(TimingKeyword::Linear),
85            }),
86        }
87    }
88
89    pub(crate) fn window(&self) -> &Window {
90        &self.window
91    }
92
93    /// <https://drafts.csswg.org/web-animations-1/#update-the-timing-properties-of-an-animation-effect>
94    pub(crate) fn update_the_timing_properties(
95        &self,
96        input: &OptionalEffectTiming,
97    ) -> Fallible<()> {
98        // Step 1. If the iterationStart member of input exists and is less than zero,
99        // throw a TypeError and abort this procedure.
100        if input
101            .iterationStart
102            .is_some_and(|iteration_start| *iteration_start < 0.0)
103        {
104            return Err(Error::Type(
105                c"Negative values for iterationStart are not allowed".to_owned(),
106            ));
107        }
108
109        // Step 2. If the iterations member of input exists, and is less than zero or is the value NaN,
110        // throw a TypeError and abort this procedure.
111        if input
112            .iterations
113            .is_some_and(|iterations| iterations < 0.0 || iterations.is_nan())
114        {
115            return Err(Error::Type(
116                c"\"iterations\" must be a positive number".to_owned(),
117            ));
118        }
119
120        // Step 3. If the duration member of input exists, and is less than zero, is the value NaN,
121        // or is a string other than the lowercase auto, throw a TypeError and abort this procedure.
122        let Ok(duration) = input
123            .duration
124            .as_ref()
125            .map(|duration| match duration {
126                UnrestrictedDoubleOrString::UnrestrictedDouble(double) => {
127                    if *double < 0.0 || double.is_nan() {
128                        Err(())
129                    } else {
130                        Ok(IterationDurationOrAuto::Duration(*double))
131                    }
132                },
133                UnrestrictedDoubleOrString::String(string) => {
134                    if string == "auto" {
135                        Ok(IterationDurationOrAuto::Auto)
136                    } else {
137                        Err(())
138                    }
139                },
140            })
141            .transpose()
142        else {
143            return Err(Error::Type(
144                c"\"duration\" must be a positive number".to_owned(),
145            ));
146        };
147
148        // Step 4. If the easing member of input exists but cannot be parsed using the <easing-function> production [CSS-EASING-1],
149        // throw a TypeError and abort this procedure.
150        let Ok(easing) = input
151            .easing
152            .as_ref()
153            .map(|easing| {
154                let easing = easing.str();
155                let mut parser = Parser::new(&easing);
156
157                // None of these values should matter
158                let document = self.window.Document();
159                let urlextradata = document.url().into_url().into();
160                let parser_context = parser_context_for_document(
161                    &document,
162                    CssRuleType::Style,
163                    ParsingMode::DEFAULT,
164                    &urlextradata,
165                );
166                TimingFunction::parse(&parser_context, &mut parser).map_err(|_| ())
167            })
168            .transpose()
169        else {
170            return Err(Error::Type(
171                c"\"easing\" is not a valid timing function".to_owned(),
172            ));
173        };
174
175        // Step 5. Assign each member that exists in input to the corresponding timing property of effect as follows:
176        // delay → start delay
177        let mut specified_timing_properties = self.specified_timing_properties.borrow_mut();
178        if let Some(start_delay) = input.delay {
179            specified_timing_properties.start_delay = start_delay;
180        }
181
182        // endDelay → end delay
183        if let Some(end_delay) = input.endDelay {
184            specified_timing_properties.end_delay = end_delay;
185        }
186
187        // fill → fill mode
188        if let Some(fill) = input.fill {
189            specified_timing_properties.fill_mode = fill;
190        }
191
192        // iterationStart → iteration start
193        if let Some(iteration_start) = input.iterationStart {
194            specified_timing_properties.iteration_start = iteration_start;
195        }
196
197        // iterations → iteration count
198        if let Some(iterations) = input.iterations {
199            specified_timing_properties.iteration_count = iterations;
200        }
201
202        // duration → iteration duration
203        if let Some(duration) = duration {
204            specified_timing_properties.iteration_duration = duration;
205        }
206
207        // direction → playback direction
208        if let Some(direction) = input.direction {
209            specified_timing_properties.playback_direction = direction;
210        }
211
212        // easing → easing function
213        if let Some(easing) = easing {
214            specified_timing_properties.easing_function = easing;
215        }
216        Ok(())
217    }
218}
219
220impl AnimationEffectMethods<crate::DomTypeHolder> for AnimationEffect {
221    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-gettiming>
222    fn GetTiming(&self) -> EffectTiming {
223        // > Returns the specified timing properties for this animation effect.
224        let specified_timing_properties = self.specified_timing_properties.borrow();
225        EffectTiming {
226            delay: specified_timing_properties.start_delay,
227            direction: specified_timing_properties.playback_direction,
228            duration: specified_timing_properties.iteration_duration.into(),
229            easing: specified_timing_properties
230                .easing_function
231                .to_css_string()
232                .into(),
233            endDelay: specified_timing_properties.end_delay,
234            fill: specified_timing_properties.fill_mode,
235            iterationStart: specified_timing_properties.iteration_start,
236            iterations: specified_timing_properties.iteration_count,
237        }
238    }
239
240    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-getcomputedtiming>
241    fn GetComputedTiming(&self) -> ComputedEffectTiming {
242        // > Returns the calculated timing properties for this animation effect.
243        let specified_timing_properties = self.specified_timing_properties.borrow();
244
245        // > while getTiming() can return the string auto, getComputedTiming() must return a number
246        // > corresponding to the calculated value of the iteration duration as defined in the description
247        // > of the duration member of the EffectTiming interface.
248        // > In this level of the specification, that simply means that an auto value is replaced by zero.
249        let computed_duration = specified_timing_properties
250            .iteration_duration
251            .computed_value();
252
253        // > likewise, while getTiming() can return the string auto, getComputedTiming() must return the
254        // > specific FillMode used for timing calculations as defined in the description of the fill
255        // > member of the EffectTiming interface.
256        // > In this level of the specification, that simply means that an auto value is replaced by the none FillMode.
257        let computed_fill_mode = if specified_timing_properties.fill_mode == FillMode::Auto {
258            FillMode::None
259        } else {
260            specified_timing_properties.fill_mode
261        };
262
263        ComputedEffectTiming {
264            parent: EffectTiming {
265                delay: specified_timing_properties.start_delay,
266                direction: specified_timing_properties.playback_direction,
267                duration: UnrestrictedDoubleOrString::UnrestrictedDouble(computed_duration),
268                easing: specified_timing_properties
269                    .easing_function
270                    .to_css_string()
271                    .into(),
272                endDelay: specified_timing_properties.end_delay,
273                fill: computed_fill_mode,
274                iterationStart: specified_timing_properties.iteration_start,
275                iterations: specified_timing_properties.iteration_count,
276            },
277            // FIXME: These are just placeholder values
278            endTime: None,
279            activeDuration: None,
280            localTime: None,
281            progress: None,
282            currentIteration: None,
283        }
284    }
285
286    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming>
287    fn UpdateTiming(&self, timing: &OptionalEffectTiming) -> Fallible<()> {
288        // > Updates the specified timing properties of this animation effect by performing the procedure
289        // > to update the timing properties of an animation effect passing the timing parameter as input.
290        self.update_the_timing_properties(timing)
291    }
292}
293
294#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
295enum IterationDurationOrAuto {
296    Duration(f64),
297    Auto,
298}
299
300impl IterationDurationOrAuto {
301    fn computed_value(&self) -> f64 {
302        match self {
303            IterationDurationOrAuto::Auto => 0.0,
304            IterationDurationOrAuto::Duration(double) => *double,
305        }
306    }
307}
308
309impl From<IterationDurationOrAuto> for UnrestrictedDoubleOrString {
310    fn from(value: IterationDurationOrAuto) -> Self {
311        match value {
312            IterationDurationOrAuto::Auto => {
313                UnrestrictedDoubleOrString::String(DOMString::from_static("auto"))
314            },
315            IterationDurationOrAuto::Duration(double) => {
316                UnrestrictedDoubleOrString::UnrestrictedDouble(double)
317            },
318        }
319    }
320}