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, ParserInput};
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 style::parser::Parse;
18use style::stylesheets::CssRuleType;
19use style::values::generics::easing::TimingKeyword;
20use style::values::specified::TimingFunction;
21use style_traits::{ParsingMode, ToCss};
22
23use crate::css::parser_context_for_document;
24use crate::dom::Window;
25use crate::dom::bindings::codegen::UnionTypes::UnrestrictedDoubleOrString;
26
27/// <https://drafts.csswg.org/web-animations-1/#animationeffect>
28#[dom_struct]
29pub(crate) struct AnimationEffect {
30    reflector: Reflector,
31
32    /// The window that this `AnimationEffect` was constructed in.
33    window: Dom<Window>,
34
35    specified_timing_properties: DomRefCell<SpecifiedTimingProperties>,
36}
37
38#[derive(Clone, JSTraceable, MallocSizeOf)]
39struct SpecifiedTimingProperties {
40    /// <https://drafts.csswg.org/web-animations-1/#start-delay>
41    start_delay: Finite<f64>,
42
43    /// <https://drafts.csswg.org/web-animations-1/#end-delay>
44    end_delay: Finite<f64>,
45
46    /// <https://drafts.csswg.org/web-animations-1/#fill-mode>
47    fill_mode: FillMode,
48
49    /// <https://drafts.csswg.org/web-animations-1/#iteration-count>
50    iteration_count: f64,
51
52    /// <https://drafts.csswg.org/web-animations-1/#iteration-start>
53    iteration_start: Finite<f64>,
54
55    /// <https://drafts.csswg.org/web-animations-1/#iteration-duration>
56    iteration_duration: IterationDurationOrAuto,
57
58    /// <https://drafts.csswg.org/web-animations-1/#playback-direction>
59    playback_direction: PlaybackDirection,
60
61    /// <https://drafts.csswg.org/css-easing-2/#easing-function>
62    #[no_trace]
63    easing_function: TimingFunction,
64}
65
66impl AnimationEffect {
67    pub(crate) fn new_inherited(window: &Window) -> Self {
68        Self {
69            reflector: Reflector::new(),
70            window: Dom::from_ref(window),
71
72            // The default values of the timing properties specified here don't matter.
73            // There is no way to construct a AnimationEffect without subsequently initializing them,
74            // even if they're not passed to the constructor.
75            specified_timing_properties: DomRefCell::new(SpecifiedTimingProperties {
76                start_delay: Default::default(),
77                end_delay: Default::default(),
78                fill_mode: FillMode::None,
79                iteration_count: Default::default(),
80                iteration_start: Default::default(),
81                iteration_duration: IterationDurationOrAuto::Auto,
82                playback_direction: PlaybackDirection::Normal,
83                easing_function: TimingFunction::Keyword(TimingKeyword::Linear),
84            }),
85        }
86    }
87
88    pub(crate) fn window(&self) -> &Window {
89        &self.window
90    }
91
92    /// <https://drafts.csswg.org/web-animations-1/#update-the-timing-properties-of-an-animation-effect>
93    pub(crate) fn update_the_timing_properties(
94        &self,
95        input: &OptionalEffectTiming,
96    ) -> Fallible<()> {
97        // Step 1. If the iterationStart member of input exists and is less than zero,
98        // throw a TypeError and abort this procedure.
99        if input
100            .iterationStart
101            .is_some_and(|iteration_start| *iteration_start < 0.0)
102        {
103            return Err(Error::Type(
104                c"Negative values for iterationStart are not allowed".to_owned(),
105            ));
106        }
107
108        // Step 2. If the iterations member of input exists, and is less than zero or is the value NaN,
109        // throw a TypeError and abort this procedure.
110        if input
111            .iterations
112            .is_some_and(|iterations| iterations < 0.0 || iterations.is_nan())
113        {
114            return Err(Error::Type(
115                c"\"iterations\" must be a positive number".to_owned(),
116            ));
117        }
118
119        // Step 3. If the duration member of input exists, and is less than zero, is the value NaN,
120        // or is a string other than the lowercase auto, throw a TypeError and abort this procedure.
121        let Ok(duration) = input
122            .duration
123            .as_ref()
124            .map(|duration| match duration {
125                UnrestrictedDoubleOrString::UnrestrictedDouble(double) => {
126                    if *double < 0.0 || double.is_nan() {
127                        Err(())
128                    } else {
129                        Ok(IterationDurationOrAuto::Duration(*double))
130                    }
131                },
132                UnrestrictedDoubleOrString::String(string) => {
133                    if string == "auto" {
134                        Ok(IterationDurationOrAuto::Auto)
135                    } else {
136                        Err(())
137                    }
138                },
139            })
140            .transpose()
141        else {
142            return Err(Error::Type(
143                c"\"duration\" must be a positive number".to_owned(),
144            ));
145        };
146
147        // Step 4. If the easing member of input exists but cannot be parsed using the <easing-function> production [CSS-EASING-1],
148        // throw a TypeError and abort this procedure.
149        let Ok(easing) = input
150            .easing
151            .as_ref()
152            .map(|easing| {
153                let easing = easing.str();
154                let mut parser_input = ParserInput::new(&easing);
155                let mut parser = Parser::new(&mut parser_input);
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 => UnrestrictedDoubleOrString::String("auto".into()),
313            IterationDurationOrAuto::Duration(double) => {
314                UnrestrictedDoubleOrString::UnrestrictedDouble(double)
315            },
316        }
317    }
318}