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 or is the value NaN,
120        // throw a TypeError and abort this procedure.
121        //
122        // Note: "auto" values are treated as zero: https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming
123        // > In this level of this specification, the string value auto is treated as the value zero
124        // > for the purpose of timing model calculations and for the result of the duration member returned
125        // > from getComputedTiming(). If the author specifies the auto value, user agents must, however,
126        // > return auto for the duration member returned from getTiming().
127        //
128        // Note: It is unspecified how non-"auto" strings should be treated. We choose to throw a TypeError.
129        //       See also https://github.com/w3c/csswg-drafts/issues/14206
130        let Ok(duration) = input
131            .duration
132            .as_ref()
133            .map(|duration| match duration {
134                UnrestrictedDoubleOrString::UnrestrictedDouble(double) => {
135                    if *double < 0.0 || double.is_nan() {
136                        Err(())
137                    } else {
138                        Ok(IterationDurationOrAuto::Duration(*double))
139                    }
140                },
141                UnrestrictedDoubleOrString::String(string) => {
142                    if string == "auto" {
143                        Ok(IterationDurationOrAuto::Auto)
144                    } else {
145                        Err(())
146                    }
147                },
148            })
149            .transpose()
150        else {
151            return Err(Error::Type(
152                c"\"duration\" must be a positive number".to_owned(),
153            ));
154        };
155
156        // Step 4. If the easing member of input exists but cannot be parsed using the <easing-function> production [CSS-EASING-1],
157        // throw a TypeError and abort this procedure.
158        let Ok(easing) = input
159            .easing
160            .as_ref()
161            .map(|easing| {
162                let easing = easing.str();
163                let mut parser_input = ParserInput::new(&easing);
164                let mut parser = Parser::new(&mut parser_input);
165
166                // None of these values should matter
167                let document = self.window.Document();
168                let urlextradata = document.url().into_url().into();
169                let parser_context = parser_context_for_document(
170                    &document,
171                    CssRuleType::Style,
172                    ParsingMode::DEFAULT,
173                    &urlextradata,
174                );
175                TimingFunction::parse(&parser_context, &mut parser).map_err(|_| ())
176            })
177            .transpose()
178        else {
179            return Err(Error::Type(
180                c"\"easing\" is not a valid timing function".to_owned(),
181            ));
182        };
183
184        // Step 5. Assign each member that exists in input to the corresponding timing property of effect as follows:
185        // delay → start delay
186        let mut specified_timing_properties = self.specified_timing_properties.borrow_mut();
187        if let Some(start_delay) = input.delay {
188            specified_timing_properties.start_delay = start_delay;
189        }
190
191        // endDelay → end delay
192        if let Some(end_delay) = input.endDelay {
193            specified_timing_properties.end_delay = end_delay;
194        }
195
196        // fill → fill mode
197        if let Some(fill) = input.fill {
198            specified_timing_properties.fill_mode = fill;
199        }
200
201        // iterationStart → iteration start
202        if let Some(iteration_start) = input.iterationStart {
203            specified_timing_properties.iteration_start = iteration_start;
204        }
205
206        // iterations → iteration count
207        if let Some(iterations) = input.iterations {
208            specified_timing_properties.iteration_count = iterations;
209        }
210
211        // duration → iteration duration
212        if let Some(duration) = duration {
213            specified_timing_properties.iteration_duration = duration;
214        }
215
216        // direction → playback direction
217        if let Some(direction) = input.direction {
218            specified_timing_properties.playback_direction = direction;
219        }
220
221        // easing → easing function
222        if let Some(easing) = easing {
223            specified_timing_properties.easing_function = easing;
224        }
225        Ok(())
226    }
227}
228
229impl AnimationEffectMethods<crate::DomTypeHolder> for AnimationEffect {
230    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-gettiming>
231    fn GetTiming(&self) -> EffectTiming {
232        // > Returns the specified timing properties for this animation effect.
233        let specified_timing_properties = self.specified_timing_properties.borrow();
234        EffectTiming {
235            delay: specified_timing_properties.start_delay,
236            direction: specified_timing_properties.playback_direction,
237            duration: specified_timing_properties.iteration_duration.into(),
238            easing: specified_timing_properties
239                .easing_function
240                .to_css_string()
241                .into(),
242            endDelay: specified_timing_properties.end_delay,
243            fill: specified_timing_properties.fill_mode,
244            iterationStart: specified_timing_properties.iteration_start,
245            iterations: specified_timing_properties.iteration_count,
246        }
247    }
248
249    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-getcomputedtiming>
250    fn GetComputedTiming(&self) -> ComputedEffectTiming {
251        // > Returns the calculated timing properties for this animation effect.
252        let specified_timing_properties = self.specified_timing_properties.borrow();
253
254        // > while getTiming() can return the string auto, getComputedTiming() must return a number
255        // > corresponding to the calculated value of the iteration duration as defined in the description
256        // > of the duration member of the EffectTiming interface.
257        // > In this level of the specification, that simply means that an auto value is replaced by zero.
258        let computed_duration = specified_timing_properties
259            .iteration_duration
260            .computed_value();
261
262        // > likewise, while getTiming() can return the string auto, getComputedTiming() must return the
263        // > specific FillMode used for timing calculations as defined in the description of the fill
264        // > member of the EffectTiming interface.
265        // > In this level of the specification, that simply means that an auto value is replaced by the none FillMode.
266        let computed_fill_mode = if specified_timing_properties.fill_mode == FillMode::Auto {
267            FillMode::None
268        } else {
269            specified_timing_properties.fill_mode
270        };
271
272        ComputedEffectTiming {
273            parent: EffectTiming {
274                delay: specified_timing_properties.start_delay,
275                direction: specified_timing_properties.playback_direction,
276                duration: UnrestrictedDoubleOrString::UnrestrictedDouble(computed_duration),
277                easing: specified_timing_properties
278                    .easing_function
279                    .to_css_string()
280                    .into(),
281                endDelay: specified_timing_properties.end_delay,
282                fill: computed_fill_mode,
283                iterationStart: specified_timing_properties.iteration_start,
284                iterations: specified_timing_properties.iteration_count,
285            },
286            // FIXME: These are just placeholder values
287            endTime: None,
288            activeDuration: None,
289            localTime: None,
290            progress: None,
291            currentIteration: None,
292        }
293    }
294
295    /// <https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming>
296    fn UpdateTiming(&self, timing: &OptionalEffectTiming) -> Fallible<()> {
297        // > Updates the specified timing properties of this animation effect by performing the procedure
298        // > to update the timing properties of an animation effect passing the timing parameter as input.
299        self.update_the_timing_properties(timing)
300    }
301}
302
303#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
304enum IterationDurationOrAuto {
305    Duration(f64),
306    Auto,
307}
308
309impl IterationDurationOrAuto {
310    fn computed_value(&self) -> f64 {
311        match self {
312            IterationDurationOrAuto::Auto => 0.0,
313            IterationDurationOrAuto::Duration(double) => *double,
314        }
315    }
316}
317
318impl From<IterationDurationOrAuto> for UnrestrictedDoubleOrString {
319    fn from(value: IterationDurationOrAuto) -> Self {
320        match value {
321            IterationDurationOrAuto::Auto => UnrestrictedDoubleOrString::String("auto".into()),
322            IterationDurationOrAuto::Duration(double) => {
323                UnrestrictedDoubleOrString::UnrestrictedDouble(double)
324            },
325        }
326    }
327}