Skip to main content

autumn_plugin_gsap/
options.rs

1//! Options that [`Gsap`](crate::Gsap) and [`Timeline`](crate::Timeline) share.
2
3use std::time::Duration;
4
5use crate::attrs::{Attr, push_flag, push_opt};
6use crate::ease::Ease;
7use crate::fmt::secs;
8use crate::scroll::{Play, ScrollPos, Scrub, ToggleActions};
9
10/// The play time that `init.js` uses when you set no duration.
11#[cfg(test)]
12pub(crate) const DEFAULT_DURATION: Duration = Duration::from_millis(800);
13
14/// How many times an animation plays again after the first play.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Repeat {
17    /// Play again `n` more times.
18    Times(u32),
19    /// Play again with no end (`-1`).
20    Forever,
21}
22
23/// Shared playback and ScrollTrigger options.
24///
25/// Each `bool` maps to one bare attribute, so the flags stay separate fields.
26#[allow(clippy::struct_excessive_bools)]
27#[derive(Debug, Clone, Default, PartialEq)]
28pub(crate) struct Common {
29    pub(crate) play: Play,
30    pub(crate) delay: Option<Duration>,
31    pub(crate) duration: Option<Duration>,
32    pub(crate) ease: Option<Ease>,
33    pub(crate) repeat: Option<Repeat>,
34    pub(crate) yoyo: bool,
35    pub(crate) repeat_delay: Option<Duration>,
36    pub(crate) trigger: Option<String>,
37    pub(crate) start: Option<ScrollPos>,
38    pub(crate) end: Option<ScrollPos>,
39    pub(crate) toggle_actions: Option<ToggleActions>,
40    pub(crate) replay: bool,
41    pub(crate) scrub: Option<Scrub>,
42    pub(crate) pin: bool,
43    pub(crate) markers: bool,
44    pub(crate) animate_reduced: bool,
45    pub(crate) id: Option<String>,
46    pub(crate) class: Option<String>,
47}
48
49impl Common {
50    /// The `id` and `class` attributes for the wrapper element.
51    pub(crate) fn html_attrs(&self) -> Vec<Attr> {
52        let mut out = Vec::new();
53        push_opt(&mut out, "id", self.id.clone());
54        push_opt(&mut out, "class", self.class.clone());
55        out
56    }
57
58    /// Adds the attributes for the options that differ from the defaults.
59    pub(crate) fn push(&self, out: &mut Vec<Attr>) {
60        push_opt(
61            out,
62            "data-gsap-on",
63            (self.play == Play::Load).then(|| "load".to_owned()),
64        );
65        push_opt(out, "data-gsap-delay", self.delay.map(secs));
66        push_opt(out, "data-gsap-duration", self.duration.map(secs));
67        push_opt(out, "data-gsap-ease", self.ease.map(|e| e.to_string()));
68        push_opt(
69            out,
70            "data-gsap-repeat",
71            self.repeat.map(|r| match r {
72                Repeat::Times(n) => n.to_string(),
73                Repeat::Forever => "-1".to_owned(),
74            }),
75        );
76        push_flag(out, "data-gsap-yoyo", self.yoyo);
77        push_opt(out, "data-gsap-repeat-delay", self.repeat_delay.map(secs));
78        push_opt(out, "data-gsap-trigger", self.trigger.clone());
79        push_opt(out, "data-gsap-start", self.start.map(|p| p.to_string()));
80        push_opt(out, "data-gsap-end", self.end.map(|p| p.to_string()));
81        push_opt(
82            out,
83            "data-gsap-toggle-actions",
84            self.toggle_actions.map(|t| t.to_string()),
85        );
86        push_opt(
87            out,
88            "data-gsap-once",
89            self.replay.then(|| "false".to_owned()),
90        );
91        push_opt(out, "data-gsap-scrub", self.scrub.map(|s| s.to_string()));
92        push_flag(out, "data-gsap-pin", self.pin);
93        push_flag(out, "data-gsap-markers", self.markers);
94        push_opt(
95            out,
96            "data-gsap-reduced",
97            self.animate_reduced.then(|| "animate".to_owned()),
98        );
99    }
100}
101
102/// Adds the shared builder methods to a type with a `common: Common` field.
103macro_rules! common_setters {
104    () => {
105        /// Starts on scroll (default) or on load. See [`Play`](crate::Play).
106        /// With [`Play::Load`](crate::Play::Load), `init.js` ignores the ScrollTrigger options.
107        pub const fn play(mut self, play: crate::Play) -> Self {
108            self.common.play = play;
109            self
110        }
111
112        /// Waits this time before the start (`data-gsap-delay`).
113        pub const fn delay(mut self, delay: std::time::Duration) -> Self {
114            self.common.delay = Some(delay);
115            self
116        }
117
118        /// Sets the play time (`data-gsap-duration`). The default is 0.8 s.
119        /// On a [`Timeline`](crate::Timeline), this is the default for each step.
120        /// A step with no duration uses the timeline value. Times have millisecond precision.
121        pub const fn duration(mut self, duration: std::time::Duration) -> Self {
122            self.common.duration = Some(duration);
123            self
124        }
125
126        /// Sets the ease (`data-gsap-ease`). The default is `power3.out`.
127        /// On a [`Timeline`](crate::Timeline), this is the default for each step.
128        pub const fn ease(mut self, ease: crate::Ease) -> Self {
129            self.common.ease = Some(ease);
130            self
131        }
132
133        /// Plays again after the first play (`data-gsap-repeat`).
134        pub const fn repeat(mut self, repeat: crate::Repeat) -> Self {
135            self.common.repeat = Some(repeat);
136            self
137        }
138
139        /// Plays backward on each second repeat (`data-gsap-yoyo`).
140        pub const fn yoyo(mut self) -> Self {
141            self.common.yoyo = true;
142            self
143        }
144
145        /// Waits this time between repeats (`data-gsap-repeat-delay`).
146        pub const fn repeat_delay(mut self, delay: std::time::Duration) -> Self {
147            self.common.repeat_delay = Some(delay);
148            self
149        }
150
151        /// Uses another element as the ScrollTrigger trigger (`data-gsap-trigger`).
152        /// The value is a CSS selector. An unknown selector uses the element itself.
153        /// A blank string sets no trigger.
154        pub fn trigger(mut self, selector: impl Into<String>) -> Self {
155            self.common.trigger = Some(selector.into()).filter(|s| !s.trim().is_empty());
156            self
157        }
158
159        /// Sets the ScrollTrigger start (`data-gsap-start`). The default is `top 85%`.
160        pub const fn start(mut self, pos: crate::ScrollPos) -> Self {
161            self.common.start = Some(pos);
162            self
163        }
164
165        /// Sets the ScrollTrigger end (`data-gsap-end`).
166        pub const fn end(mut self, pos: crate::ScrollPos) -> Self {
167            self.common.end = Some(pos);
168            self
169        }
170
171        /// Sets the ScrollTrigger toggle actions (`data-gsap-toggle-actions`).
172        pub const fn toggle_actions(mut self, actions: crate::ToggleActions) -> Self {
173            self.common.toggle_actions = Some(actions);
174            self
175        }
176
177        /// Keeps the ScrollTrigger after the first play (`data-gsap-once="false"`).
178        /// The animation plays forward on enter and backward on leave back
179        /// (`play none none reverse`). The default plays one time.
180        /// [`toggle_actions`](Self::toggle_actions) and [`scrub`](Self::scrub) replace this option.
181        pub const fn replay(mut self) -> Self {
182            self.common.replay = true;
183            self
184        }
185
186        /// Links the progress to the scroll position (`data-gsap-scrub`).
187        /// With scrub, the default range is `top bottom` to `bottom top`.
188        /// Scrub ignores [`toggle_actions`](Self::toggle_actions) and [`replay`](Self::replay).
189        pub const fn scrub(mut self, scrub: crate::Scrub) -> Self {
190            self.common.scrub = Some(scrub);
191            self
192        }
193
194        /// Pins the trigger element while the animation is active (`data-gsap-pin`).
195        /// `init.js` refuses a pin when the trigger is outside the element.
196        pub const fn pin(mut self) -> Self {
197            self.common.pin = true;
198            self
199        }
200
201        /// Shows the ScrollTrigger debug markers (`data-gsap-markers`).
202        pub const fn markers(mut self) -> Self {
203            self.common.markers = true;
204            self
205        }
206
207        /// Sets the `id` of the wrapper element. A blank string sets no `id`.
208        pub fn id(mut self, id: impl Into<String>) -> Self {
209            self.common.id = Some(id.into()).filter(|s| !s.trim().is_empty());
210            self
211        }
212
213        /// Sets the `class` of the wrapper element. A blank string sets no `class`.
214        pub fn class(mut self, class: impl Into<String>) -> Self {
215            self.common.class = Some(class.into()).filter(|s| !s.trim().is_empty());
216            self
217        }
218
219        /// Puts `markup` in a `<div>` with the attributes.
220        #[allow(clippy::needless_pass_by_value)]
221        #[must_use]
222        pub fn wrap(self, markup: autumn_web::Markup) -> autumn_web::Markup {
223            self.wrap_in(crate::Tag::Div, markup)
224        }
225
226        /// Puts `markup` in a `tag` element with the `id`, `class` and animation attributes.
227        #[allow(clippy::needless_pass_by_value)]
228        #[must_use]
229        pub fn wrap_in(self, tag: crate::Tag, markup: autumn_web::Markup) -> autumn_web::Markup {
230            let mut attrs = self.common.html_attrs();
231            attrs.extend(self.attributes());
232            crate::attrs::wrap_el(tag, &attrs, &markup)
233        }
234
235        /// Also animates when the user prefers reduced motion
236        /// (`data-gsap-reduced="animate"`). Use this only for motion that is necessary.
237        pub const fn animate_on_reduced_motion(mut self) -> Self {
238            self.common.animate_reduced = true;
239            self
240        }
241    };
242}
243
244pub(crate) use common_setters;
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn init_js_uses_the_same_default_duration() {
252        let js = include_str!("../assets/init.js");
253        let want = format!("duration: {},", secs(DEFAULT_DURATION));
254        assert!(js.contains(&want), "init.js DEFAULTS must contain `{want}`");
255    }
256
257    #[test]
258    fn documented_defaults_match_init_js() {
259        let js = include_str!("../assets/init.js");
260        for want in [r#"ease: "power3.out","#, r#"start: "top 85%","#] {
261            assert!(js.contains(want), "init.js DEFAULTS must contain `{want}`");
262        }
263    }
264
265    #[test]
266    fn html_attrs_hold_only_id_and_class() {
267        let c = Common {
268            id: Some("a".to_owned()),
269            class: Some("b".to_owned()),
270            pin: true,
271            ..Common::default()
272        };
273        assert_eq!(
274            c.html_attrs(),
275            vec![("id", "a".to_owned()), ("class", "b".to_owned())]
276        );
277    }
278}