wake-marquee 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,30 +8,51 @@ While the version is below `1.0.0` the API may still change in a minor release.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
- ### Fixed
12
-
13
- - The row no longer jumps as it starts. Three separate causes, all of them
14
- the same shape: work spread across frames that the browser paints one by
15
- one.
16
- - Measuring, cloning and the first transform were left to three different
17
- observer callbacks. They now happen together, in the task that starts the
18
- row, for any row already on screen.
19
- - The track's overhang was applied at construction while the transform that
20
- cancels it was written on first sight. Between the two, the row sat a full
21
- wake amplitude off to one side. Both now happen in the same frame.
22
- - The first animated frame landed on the loop's zero point rather than on
23
- the position the static row already occupied. It is now aligned to it,
24
- which matters most for a row reached by anchor link or a restored scroll
25
- position, where the two are furthest apart.
26
- - A lane that resizes under a running row, from a late web font or an image
27
- with no `width` and `height`, no longer shifts the row by the difference.
28
- The offset is restated in the new period, holding the phase.
11
+ ## [0.2.0] - 2026-09-01
12
+
13
+ Three things adopting `0.1.0` in a real project turned up.
14
+
15
+ ### Added
16
+
17
+ - `getMarquee(element)` returns the marquee running on an element, or `null`.
18
+ `initMarquees()` hands back only the instances that call created, so the
19
+ declarative integrations were a one-way door: `wake-marquee/astro` starts the
20
+ rows in a hoisted script with nowhere to return a handle to, and calling
21
+ `initMarquees()` again from application code returns an empty array because
22
+ every row is already running. There was no way to reach `pause()`, `refresh()`
23
+ or `destroy()` at all without giving up the integration and hand-rolling the
24
+ markup.
25
+ - `speed` accepts a percentage of the container width per second, `'8%'` as
26
+ well as `60`. Pixels a second is a physical unit on a page whose elements are
27
+ not physically constant: a logo row is 40px tall with a 72px gap on a desktop
28
+ and 16px with a 32px gap on a phone, so one value is calm on the first and
29
+ hectic on the second, with three times as many items going past. The
30
+ percentage is resolved against the width the wake is already measuring every
31
+ frame, so it follows a resize and costs nothing.
32
+ - A console warning when the item spacing has collapsed to zero while
33
+ `--wake-gap` asks for more. An unlayered global reset outranks every cascade
34
+ layer whatever the specificity says, so `* { margin: 0 }` quietly beats the
35
+ library's `margin-inline-end` and the items sit flush. Nothing else gives it
36
+ away: the row still loops and the value still reads correctly in the devtools,
37
+ which makes it look like a mistake in the consuming project. Said once per
38
+ page, and never when `--wake-gap` is genuinely `0`.
29
39
 
30
40
  ### Changed
31
41
 
32
- - `wake-marquee/astro` renders `--wake-gap` and `--wake-fade` as inline custom
33
- properties, so the static row is laid out correctly before the script runs.
34
- The `data-wake-*` attributes still carry the same values for the script.
42
+ - The README's layer-order guidance was only under "Tailwind CSS v4", which is
43
+ not where somebody with a hand-written reset goes looking. Styling now has a
44
+ "Cascade layers" section covering both, and it records why the item spacing
45
+ stays inside the layer: it is the rule you are most likely to want to
46
+ override, and unlayered library CSS would beat your own utilities.
47
+ - The demo's size figure is weighed at build time rather than written down.
48
+
49
+ ### Notes
50
+
51
+ - A duration form of `speed`, `'9.6s'` for one container width, was considered
52
+ and dropped. It reverses the option on itself next to the number form, where
53
+ `'12s'` would be slower than `'6s'` while `120` is faster than `60`, and it
54
+ invites the reading "per lap", which would be the lane width and would mean
55
+ adding one logo slowed the row down.
35
56
 
36
57
  ## [0.1.0] - 2026-09-01
37
58
 
@@ -58,6 +79,14 @@ First release.
58
79
  than one per row. The loop stops when nothing is on screen.
59
80
  - Rows are measured and cloned on first sight, not at construction, so a page
60
81
  of ten marquees does no work for the nine nobody has scrolled to.
82
+ - The handover from the static row to the running one happens inside a single
83
+ task, so no half-built state is ever painted, and the first frame is aligned
84
+ to the position the static row already occupied rather than to the loop's
85
+ own zero point. A row reached by anchor link or a restored scroll position
86
+ starts as cleanly as one scrolled to.
87
+ - A lane resized under a running row, by a late web font or an image with no
88
+ `width` and `height`, does not shift it: the offset is restated in the new
89
+ period, holding the phase.
61
90
  - `wake-marquee/css`: the loop geometry in an `@layer wake-marquee` cascade
62
91
  layer, 407 B gzipped. It owns nothing about how items look.
63
92
  - `wake-marquee/astro`: Astro component rendering the same attributes.
@@ -70,6 +99,10 @@ First release.
70
99
 
71
100
  ### Notes
72
101
 
102
+ - Spacing and fade width change the layout of the static row, so declaring
103
+ them only in `data-wake-gap` or `data-wake-fade` leaves the items shifting
104
+ when the script runs. Set the matching custom property too; the Astro
105
+ component does it for you. Documented in the README.
73
106
  - The spacing between items is a `margin-inline-end` on the item rather than
74
107
  `gap` on the row. The lane's width is the loop period, so the space after
75
108
  the last item has to belong to the lane; with `gap` there is none between
@@ -77,5 +110,6 @@ First release.
77
110
  - Right-to-left writing modes are not supported. Flex rows follow the writing
78
111
  direction while the transforms are physical, so the two disagree.
79
112
 
80
- [Unreleased]: https://github.com/robin-gogolok/wake-marquee/compare/v0.1.0...HEAD
113
+ [Unreleased]: https://github.com/robin-gogolok/wake-marquee/compare/v0.2.0...HEAD
114
+ [0.2.0]: https://github.com/robin-gogolok/wake-marquee/compare/v0.1.0...v0.2.0
81
115
  [0.1.0]: https://github.com/robin-gogolok/wake-marquee/releases/tag/v0.1.0
package/README.md CHANGED
@@ -5,7 +5,7 @@ An endless marquee that answers to the scroll. It reverses when the reader scrol
5
5
  **[Live demo →](https://robin-gogolok.github.io/wake-marquee/)**
6
6
 
7
7
  ```
8
- 3.0 kB gzipped JS
8
+ 3.5 kB gzipped JS
9
9
  0.4 kB gzipped CSS
10
10
  0 runtime dependencies
11
11
  1 rAF loop, however many rows are on the page
@@ -75,11 +75,20 @@ const row = createMarquee(document.querySelector('#logos'), {
75
75
  ---
76
76
  import WakeMarquee from 'wake-marquee/astro'
77
77
  ---
78
- <WakeMarquee speed={55} wake={10} fade="6rem" aria-label="Brands we stock">
78
+ <WakeMarquee speed="8%" wake={10} fade="6rem" aria-label="Brands we stock">
79
79
  {brands.map((b) => <img src={b.logo} alt={b.name} />)}
80
80
  </WakeMarquee>
81
81
  ```
82
82
 
83
+ The component's script starts the row and keeps no handle, because a hoisted
84
+ Astro script has nowhere to hand one to. Ask for it by element instead:
85
+
86
+ ```js
87
+ import { getMarquee } from 'wake-marquee'
88
+
89
+ getMarquee(document.querySelector('#logos'))?.pause()
90
+ ```
91
+
83
92
  ### React
84
93
 
85
94
  The library takes an element, so a ref and an effect are the whole integration. No adapter package.
@@ -133,6 +142,36 @@ The easing is framerate independent, so the same turn takes the same time on a 6
133
142
 
134
143
  Set `reverse: false` to keep a fixed heading and keep the wake.
135
144
 
145
+ ## Speed that survives a phone
146
+
147
+ `speed: 150` is 150 pixels a second, and pixels a second is a physical unit on
148
+ a page whose elements are not physically constant. The same row of logos is
149
+ 40px tall with a 72px gap on a desktop and 16px with a 32px gap on a phone, so
150
+ that one number crosses a 1440px container in nine and a half seconds and a
151
+ 390px one in under three, with three times as many items going past. Calm on
152
+ the desktop, hectic on the phone, and no breakpoint anywhere in sight because
153
+ the mismatch is in the unit rather than in the value.
154
+
155
+ So `speed` also takes a percentage, meaning that fraction of the container
156
+ width per second:
157
+
158
+ ```html
159
+ <div data-wake-marquee data-wake-speed="8%">
160
+ ```
161
+
162
+ The width is the one measured every frame for the wake, so this costs nothing
163
+ and follows a resize, a rotation or a container that changes under the row.
164
+
165
+ A rate rather than a duration on purpose. "One container width per 9.6
166
+ seconds" reads well on its own and reverses the option on itself the moment it
167
+ sits beside the number form: `'12s'` would be slower than `'6s'` while `120` is
168
+ faster than `60`. It also invites the reading "per lap", which would be the
169
+ lane width, so adding one logo would slow the row down.
170
+
171
+ There is no floor. A percentage on a very narrow viewport is a very slow row,
172
+ and `8%` of 390px is 31 px/s, which may be slower than you want it. Pick the
173
+ percentage for the width you care about most and check the other end.
174
+
136
175
  ## Starting without a flash
137
176
 
138
177
  A row has two lives: the static one the server sends, and the running one the
@@ -174,7 +213,7 @@ Turns an element and its children into a marquee. Returns a handle.
174
213
  | Option | Default | |
175
214
  |---|---|---|
176
215
  | `direction` | `'left'` | `'left'` or `'right'`, while scrolling down |
177
- | `speed` | `60` | Travel speed in pixels per second |
216
+ | `speed` | `60` | Pixels per second, or a percentage of the container width per second (`'8%'`) |
178
217
  | `wake` | `8` | Scroll displacement, as a percentage of the container width. `0` turns it off |
179
218
  | `reverse` | `true` | Whether scrolling up reverses travel |
180
219
  | `ease` | `5` | How sharply a reversal settles, per second. Around `1` is a long, heavy turn |
@@ -195,6 +234,24 @@ The handle:
195
234
  | `refresh()` | Re-measure and top up the clones. Call it after changing the items yourself |
196
235
  | `destroy()` | Undo everything: clones, wrappers, observers, listeners, attributes |
197
236
 
237
+ ### `getMarquee(element)`
238
+
239
+ The marquee running on an element, or `null`.
240
+
241
+ ```js
242
+ import { getMarquee } from 'wake-marquee'
243
+
244
+ const row = getMarquee(document.querySelector('#logos'))
245
+ row?.pause()
246
+ ```
247
+
248
+ `initMarquees()` returns only the instances that call created, which is the
249
+ right answer for something safe to call repeatedly and the wrong one for
250
+ anybody who needs a handle later: asking a second time over the same content
251
+ hands back an empty array, because every row is already running. This is the
252
+ way back in, and it is what makes the declarative path as capable as the
253
+ imperative one.
254
+
198
255
  ### `initMarquees({ root?, defaults? })`
199
256
 
200
257
  Finds every `[data-wake-marquee]` under `root` and starts it, reading each element's own attributes for its options. Skips rows that are already running, so it is safe to call again after new content arrives. Returns only the handles it created.
@@ -216,7 +273,7 @@ Every option except `scroller` and `respectMotionPreference` can be declared in
216
273
  |---|---|
217
274
  | `data-wake-marquee` | Marks the container. Also the stylesheet's hook |
218
275
  | `data-wake-direction="right"` | `direction` |
219
- | `data-wake-speed="55"` | `speed` |
276
+ | `data-wake-speed="55"` or `"8%"` | `speed` |
220
277
  | `data-wake="10"` | `wake` |
221
278
  | `data-wake-ease="2.5"` | `ease` |
222
279
  | `data-wake-gap="4rem"` | `gap` |
@@ -255,11 +312,39 @@ The library owns the loop geometry and nothing else. Items look however you styl
255
312
 
256
313
  The spacing is a `margin-inline-end` on each item rather than `gap` on the row, and that is load-bearing rather than a style choice: **the lane's width is the loop's repeat distance**, so the space after the last item has to be part of the lane. With `gap`, there is no space between the last item of one lane and the first of the next, and the seam is visible on every pass.
257
314
 
258
- All rules live in an `@layer wake-marquee` cascade layer, so your own unlayered CSS wins without `!important`.
315
+ ### Cascade layers
316
+
317
+ All rules live in an `@layer wake-marquee` cascade layer, so your own unlayered CSS wins without `!important`. That is worth knowing in both directions, because the rule behind it is absolute: **an unlayered declaration beats a layered one whatever the specificity says.**
318
+
319
+ The one rule of the library's that this reaches is the item spacing. An ordinary global reset
320
+
321
+ ```css
322
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
323
+ ```
324
+
325
+ beats `margin-inline-end: var(--wake-gap)` at 0,0,0 against 0,1,0, and the gap collapses to zero. Nothing else gives it away: the row still loops, `--wake-gap` still resolves correctly in the devtools, and the items simply sit flush against one another. It reads as a mistake in your own stylesheet, so the library says so in the console when it sees it.
326
+
327
+ Put the reset in a layer, and declare the order before the library's import:
328
+
329
+ ```css
330
+ @layer reset, wake-marquee;
331
+
332
+ @import "wake-marquee/css";
333
+
334
+ @layer reset {
335
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
336
+ }
337
+ ```
338
+
339
+ The order declaration comes first and the imports come next, because `@import`
340
+ has to precede every rule that is not a layer statement. Where the reset is a
341
+ file of its own, `@import "reset.css" layer(reset);` does the same job.
342
+
343
+ The spacing stays in the layer rather than being lifted out of it, because it is also the rule you are most likely to want to override: a `me-10` utility or a class of your own on the items should win, and unlayered library CSS would beat both. The library measures whatever spacing ends up applied, so any of that is fine. What it cannot do is guess that you meant a gap and got none.
259
344
 
260
345
  ### Tailwind CSS v4
261
346
 
262
- Declare the layer order before the imports, or the library will outrank every utility:
347
+ The same rule, the other way round. Declare the layer order before the imports, or the library will outrank every utility:
263
348
 
264
349
  ```css
265
350
  @layer theme, base, components, wake-marquee, utilities;
@@ -313,7 +398,7 @@ The lane is measured once per resize, and its width is the loop period. Content
313
398
 
314
399
  | | Size | Scroll-linked | Needs JS |
315
400
  |---|---|---|---|
316
- | **wake-marquee** | 3.4 kB | yes, direction and displacement | yes |
401
+ | **wake-marquee** | 3.9 kB | yes, direction and displacement | yes |
317
402
  | CSS `@keyframes` marquee | 0 | no | no |
318
403
  | [Marquee3k](https://github.com/ezekielaquino/Marquee3000) | ~2 kB | no | yes |
319
404
  | [react-fast-marquee](https://github.com/justin-chu/react-fast-marquee) | ~4 kB | no | yes, React only |
@@ -1 +1 @@
1
- var WakeMarquee=(()=>{var l=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var b=i=>{throw TypeError(i)};var Q=(i,e)=>{for(var t in e)l(i,t,{get:e[t],enumerable:!0})},_=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of H(e))!B.call(i,n)&&n!==t&&l(i,n,{get:()=>e[n],enumerable:!(s=D(e,n))||s.enumerable});return i};var G=i=>_(l({},"__esModule",{value:!0}),i);var U=(i,e,t)=>e.has(i)||b("Cannot "+t);var y=(i,e,t)=>e.has(i)?b("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,t);var o=(i,e,t)=>(U(i,e,"access private method"),t);var ie={};Q(ie,{initMarquees:()=>f});function j(i,e,t){return i<e?e:i>t?t:i}function x(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function E(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function M(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function A(i,e,t){let s=t+e;return s>0?j((t-i)/s,0,1):0}function C(i,e,t){return-t*e*(1-2*i)}function S(i,e=.1){return!Number.isFinite(i)||i<=0?0:Math.min(i/1e3,e)}var K=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),L="data-wake-marquee",V="wake-lane",O="wake-track",Y=1,J="200px 0px";function X(i){return i.firstElementChild?.classList.contains(O)===!0}var u=new Set,p=new Map,m=0,c=0;function q(i){return i===window?window.scrollY:i.scrollTop}function Z(i){let e=p.get(i);if(!e){p.set(i,{last:q(i),sign:1});return}let t=q(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function P(i){return p.get(i)?.sign??1}function F(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function W(i={}){let e={...K};for(let[t,s]of Object.entries(i))s!==void 0&&(e[t]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(!Number.isFinite(e.speed)||e.speed<0)throw new RangeError(`wake-marquee: speed must be a non-negative number, received ${e.speed}`);if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function ee(i){let e=i.dataset,t={};return e.wakeDirection&&(t.direction=e.wakeDirection),e.wakeSpeed&&(t.speed=Number(e.wakeSpeed)),e.wake&&(t.wake=Number(e.wake)),e.wakeEase&&(t.ease=Number(e.wakeEase)),e.wakeGap&&(t.gap=e.wakeGap),e.wakeFade&&(t.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(t.pauseOnHover=!0),e.wakeReverse!==void 0&&(t.reverse=e.wakeReverse!=="false"),t}var r,R,N,g,I,v,T,$,k,w=class{constructor(e,t){y(this,r);this.element=e,this.options=W(t),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",o(this,r,R).call(this),o(this,r,I).call(this),u.add(this),o(this,r,v).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,t=this.element.getBoundingClientRect(),s=200;return t.bottom<-s||t.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),o(this,r,g).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.period=e,this.measured=!0,o(this,r,N).call(this);let t=x(this.track.getBoundingClientRect().width,e,Y);for(;this.lanes.length<t;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(n=>n.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(n=>n.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>t;)this.lanes.pop()?.remove();d()}read(e){if(!o(this,r,k).call(this))return;let t=this.element.getBoundingClientRect(),s=A(t.top,t.height,e);this.amplitude=this.options.wake*t.width/100,this.wakePx=C(s,this.amplitude,this.dirSign)}write(e,t){if(!o(this,r,k).call(this))return;this.aligned||o(this,r,$).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=M(this.dirFactor,s,e,this.options.ease),this.offset=E(this.offset,this.dirFactor*this.options.speed,e,this.period);let a=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let z of this.lanes)z.style.transform=a;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=t===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&F()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,R=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=O,this.lane=t.createElement("div"),this.lane.className=V;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(a,h)=>{e.hasAttribute(a)||(e.setAttribute(a,h),this.undo.push(()=>e.removeAttribute(a)))},n=(a,h)=>{e.style.getPropertyValue(a)||(e.style.setProperty(a,h),this.undo.push(()=>e.style.removeProperty(a)))};s(L,""),this.options.gap&&n("--wake-gap",this.options.gap),this.options.fade&&(n("--wake-fade",this.options.fade),s("data-wake-fade",""))},N=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},g=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,P(this.options.scroller)))},I=function(){let e=this.element;this.intersection=new IntersectionObserver(t=>{for(let s of t)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&o(this,r,g).call(this);d()},{rootMargin:J,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>o(this,r,v).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},v=function(){if(!this.options.respectMotionPreference){d();return}F()?(this.paused=!0,o(this,r,T).call(this)):(this.paused=!1,d())},T=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},$=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},k=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function te(i){let e=c===0?0:S(i-c);c=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&Z(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?P(s.options.scroller):1);m=0,d()}function d(){if(m!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){c=0;return}m=requestAnimationFrame(te)}function f({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${L}]`))X(s)||t.push(new w(s,{...e,...ee(s)}));for(let s of t)s.prime();return t}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>f(),{once:!0}):f());return G(ie);})();
1
+ var WakeMarquee=(()=>{var l=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var y=i=>{throw TypeError(i)};var j=(i,e)=>{for(var t in e)l(i,t,{get:e[t],enumerable:!0})},V=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of G(e))!U.call(i,n)&&n!==t&&l(i,n,{get:()=>e[n],enumerable:!(s=_(e,n))||s.enumerable});return i};var K=i=>V(l({},"__esModule",{value:!0}),i);var Y=(i,e,t)=>e.has(i)||y("Cannot "+t);var b=(i,e,t)=>e.has(i)?y("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,t);var o=(i,e,t)=>(Y(i,e,"access private method"),t);var he={};j(he,{initMarquees:()=>f});function J(i,e,t){return i<e?e:i>t?t:i}function x(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function E(i,e){if(typeof i=="number")return i;let t=parseFloat(i);return Number.isFinite(t)&&e>0?t*e/100:0}function C(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function M(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function S(i,e,t){let s=t+e;return s>0?J((t-i)/s,0,1):0}function A(i,e,t){return-t*e*(1-2*i)}function q(i,e=.1){return!Number.isFinite(i)||i<=0?0:Math.min(i/1e3,e)}var W=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),R="data-wake-marquee",X="wake-lane",O="wake-track",Z=1,ee="200px 0px",te=/^\d*\.?\d+%$/;function ie(i){return typeof i=="string"?te.test(i):Number.isFinite(i)&&i>=0}function se(i){return i.firstElementChild?.classList.contains(O)===!0}var u=new Set,F=new WeakMap,p=new Map,m=0,c=0,P=!1;function L(i){return i===window?window.scrollY:i.scrollTop}function re(i){let e=p.get(i);if(!e){p.set(i,{last:L(i),sign:1});return}let t=L(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function $(i){return p.get(i)?.sign??1}function N(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function ne(i={}){let e={...W};for(let[t,s]of Object.entries(i))s!==void 0&&(e[t]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(typeof e.speed=="string"&&(e.speed=e.speed.trim()),!ie(e.speed)){let t=typeof e.speed=="string"?`"${e.speed}"`:e.speed;throw new RangeError(`wake-marquee: speed must be a non-negative number of pixels per second, or a percentage of the container width per second such as "8%", received ${t}`)}if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function oe(i){let e=i.dataset,t={};if(e.wakeDirection&&(t.direction=e.wakeDirection),e.wakeSpeed){let s=e.wakeSpeed.trim();t.speed=Number.isFinite(Number(s))?Number(s):s}return e.wake&&(t.wake=Number(e.wake)),e.wakeEase&&(t.ease=Number(e.wakeEase)),e.wakeGap&&(t.gap=e.wakeGap),e.wakeFade&&(t.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(t.pauseOnHover=!0),e.wakeReverse!==void 0&&(t.reverse=e.wakeReverse!=="false"),t}var r,I,T,g,z,v,D,H,B,k,w=class{constructor(e,t){b(this,r);this.element=e,this.options=ne(t),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.speedPx=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",o(this,r,I).call(this),o(this,r,z).call(this),u.add(this),F.set(e,this),o(this,r,v).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,t=this.element.getBoundingClientRect(),s=200;return t.bottom<-s||t.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),o(this,r,g).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.measured||o(this,r,H).call(this),this.period=e,this.measured=!0,o(this,r,T).call(this);let t=x(this.track.getBoundingClientRect().width,e,Z);for(;this.lanes.length<t;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(n=>n.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(n=>n.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>t;)this.lanes.pop()?.remove();d()}read(e){if(!o(this,r,k).call(this))return;let t=this.element.getBoundingClientRect(),s=S(t.top,t.height,e);this.amplitude=this.options.wake*t.width/100,this.wakePx=A(s,this.amplitude,this.dirSign),this.speedPx=E(this.options.speed,t.width)}write(e,t){if(!o(this,r,k).call(this))return;this.aligned||o(this,r,B).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=M(this.dirFactor,s,e,this.options.ease),this.offset=C(this.offset,this.dirFactor*this.speedPx,e,this.period);let a=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let Q of this.lanes)Q.style.transform=a;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=t===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&N()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),F.delete(this.element),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,I=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=O,this.lane=t.createElement("div"),this.lane.className=X;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(a,h)=>{e.hasAttribute(a)||(e.setAttribute(a,h),this.undo.push(()=>e.removeAttribute(a)))},n=(a,h)=>{e.style.getPropertyValue(a)||(e.style.setProperty(a,h),this.undo.push(()=>e.style.removeProperty(a)))};s(R,""),this.options.gap&&n("--wake-gap",this.options.gap),this.options.fade&&(n("--wake-fade",this.options.fade),s("data-wake-fade",""))},T=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},g=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,$(this.options.scroller)))},z=function(){let e=this.element;this.intersection=new IntersectionObserver(t=>{for(let s of t)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&o(this,r,g).call(this);d()},{rootMargin:ee,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>o(this,r,v).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},v=function(){if(!this.options.respectMotionPreference){d();return}N()?(this.paused=!0,o(this,r,D).call(this)):(this.paused=!1,d())},D=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},H=function(){if(P)return;let e=this.lane.firstElementChild;if(!e)return;let t=getComputedStyle(e),s=t.getPropertyValue("--wake-gap").trim();!((s===""?32:parseFloat(s))>0)||parseFloat(t.marginInlineEnd)!==0||(P=!0,console.warn("wake-marquee: the items have no spacing. An unlayered CSS reset outranks @layer wake-marquee whatever the specificity says. Put the reset in a layer and name the order first: @layer reset, wake-marquee;"))},B=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},k=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function ae(i){let e=c===0?0:q(i-c);c=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&re(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?$(s.options.scroller):1);m=0,d()}function d(){if(m!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){c=0;return}m=requestAnimationFrame(ae)}function f({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${R}]`))se(s)||t.push(new w(s,{...e,...oe(s)}));for(let s of t)s.prime();return t}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>f(),{once:!0}):f());return K(he);})();
@@ -1 +1 @@
1
- var v=i=>{throw TypeError(i)};var z=(i,e,t)=>e.has(i)||v("Cannot "+t);var k=(i,e,t)=>e.has(i)?v("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,t);var n=(i,e,t)=>(z(i,e,"access private method"),t);function D(i,e,t){return i<e?e:i>t?t:i}function b(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function y(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function x(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function E(i,e,t){let s=t+e;return s>0?D((t-i)/s,0,1):0}function A(i,e,t){return-t*e*(1-2*i)}function M(i,e=.1){return!Number.isFinite(i)||i<=0?0:Math.min(i/1e3,e)}var H=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),q="data-wake-marquee",B="wake-lane",F="wake-track",Q=1,_="200px 0px";function P(i){return i.firstElementChild?.classList.contains(F)===!0}var u=new Set,l=new Map,p=0,c=0;function C(i){return i===window?window.scrollY:i.scrollTop}function G(i){let e=l.get(i);if(!e){l.set(i,{last:C(i),sign:1});return}let t=C(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function R(i){return l.get(i)?.sign??1}function S(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function U(i={}){let e={...H};for(let[t,s]of Object.entries(i))s!==void 0&&(e[t]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(!Number.isFinite(e.speed)||e.speed<0)throw new RangeError(`wake-marquee: speed must be a non-negative number, received ${e.speed}`);if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function j(i){let e=i.dataset,t={};return e.wakeDirection&&(t.direction=e.wakeDirection),e.wakeSpeed&&(t.speed=Number(e.wakeSpeed)),e.wake&&(t.wake=Number(e.wake)),e.wakeEase&&(t.ease=Number(e.wakeEase)),e.wakeGap&&(t.gap=e.wakeGap),e.wakeFade&&(t.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(t.pauseOnHover=!0),e.wakeReverse!==void 0&&(t.reverse=e.wakeReverse!=="false"),t}var r,L,O,w,N,m,I,T,g,f=class{constructor(e,t){k(this,r);this.element=e,this.options=U(t),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",n(this,r,L).call(this),n(this,r,N).call(this),u.add(this),n(this,r,m).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,t=this.element.getBoundingClientRect(),s=200;return t.bottom<-s||t.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),n(this,r,w).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.period=e,this.measured=!0,n(this,r,O).call(this);let t=b(this.track.getBoundingClientRect().width,e,Q);for(;this.lanes.length<t;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(a=>a.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(a=>a.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>t;)this.lanes.pop()?.remove();d()}read(e){if(!n(this,r,g).call(this))return;let t=this.element.getBoundingClientRect(),s=E(t.top,t.height,e);this.amplitude=this.options.wake*t.width/100,this.wakePx=A(s,this.amplitude,this.dirSign)}write(e,t){if(!n(this,r,g).call(this))return;this.aligned||n(this,r,T).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=x(this.dirFactor,s,e,this.options.ease),this.offset=y(this.offset,this.dirFactor*this.options.speed,e,this.period);let o=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let $ of this.lanes)$.style.transform=o;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=t===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&S()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,L=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=F,this.lane=t.createElement("div"),this.lane.className=B;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(o,h)=>{e.hasAttribute(o)||(e.setAttribute(o,h),this.undo.push(()=>e.removeAttribute(o)))},a=(o,h)=>{e.style.getPropertyValue(o)||(e.style.setProperty(o,h),this.undo.push(()=>e.style.removeProperty(o)))};s(q,""),this.options.gap&&a("--wake-gap",this.options.gap),this.options.fade&&(a("--wake-fade",this.options.fade),s("data-wake-fade",""))},O=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},w=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,R(this.options.scroller)))},N=function(){let e=this.element;this.intersection=new IntersectionObserver(t=>{for(let s of t)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&n(this,r,w).call(this);d()},{rootMargin:_,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>n(this,r,m).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},m=function(){if(!this.options.respectMotionPreference){d();return}S()?(this.paused=!0,n(this,r,I).call(this)):(this.paused=!1,d())},I=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},T=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},g=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function K(i){let e=c===0?0:M(i-c);c=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&G(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?R(s.options.scroller):1);p=0,d()}function d(){if(p!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){c=0;return}p=requestAnimationFrame(K)}function Z(i,e={}){if(!i||i.nodeType!==1)throw new TypeError("wake-marquee: createMarquee expects an element");if(P(i))throw new Error("wake-marquee: this element is already a marquee, call destroy() first");return new f(i,e).prime()}function W({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${q}]`))P(s)||t.push(new f(s,{...e,...j(s)}));for(let s of t)s.prime();return t}export{f as Marquee,Z as createMarquee,H as defaults,W as initMarquees,U as normalizeOptions,j as readOptions};
1
+ var k=t=>{throw TypeError(t)};var Q=(t,e,i)=>e.has(t)||k("Cannot "+i);var y=(t,e,i)=>e.has(t)?k("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i);var n=(t,e,i)=>(Q(t,e,"access private method"),i);function _(t,e,i){return t<e?e:t>i?i:t}function b(t,e,i=1){return!(e>0)||!Number.isFinite(t)?1:Math.ceil(t/e)+1+i}function x(t,e){if(typeof t=="number")return t;let i=parseFloat(t);return Number.isFinite(i)&&e>0?i*e/100:0}function E(t,e,i,s){return s>0?((t+e*i)%s+s)%s:0}function C(t,e,i,s){return t+(e-t)*(1-Math.exp(-i*s))}function S(t,e,i){let s=i+e;return s>0?_((i-t)/s,0,1):0}function M(t,e,i){return-i*e*(1-2*t)}function A(t,e=.1){return!Number.isFinite(t)||t<=0?0:Math.min(t/1e3,e)}var G=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),N="data-wake-marquee",U="wake-lane",R="wake-track",j=1,V="200px 0px",K=/^\d*\.?\d+%$/;function Y(t){return typeof t=="string"?K.test(t):Number.isFinite(t)&&t>=0}function L(t){return t.firstElementChild?.classList.contains(R)===!0}var c=new Set,l=new WeakMap,p=new Map,m=0,u=0,F=!1;function P(t){return t===window?window.scrollY:t.scrollTop}function J(t){let e=p.get(t);if(!e){p.set(t,{last:P(t),sign:1});return}let i=P(t);Math.abs(i-e.last)>.5&&(e.sign=i>e.last?1:-1,e.last=i)}function O(t){return p.get(t)?.sign??1}function q(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function W(t={}){let e={...G};for(let[i,s]of Object.entries(t))s!==void 0&&(e[i]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(typeof e.speed=="string"&&(e.speed=e.speed.trim()),!Y(e.speed)){let i=typeof e.speed=="string"?`"${e.speed}"`:e.speed;throw new RangeError(`wake-marquee: speed must be a non-negative number of pixels per second, or a percentage of the container width per second such as "8%", received ${i}`)}if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function X(t){let e=t.dataset,i={};if(e.wakeDirection&&(i.direction=e.wakeDirection),e.wakeSpeed){let s=e.wakeSpeed.trim();i.speed=Number.isFinite(Number(s))?Number(s):s}return e.wake&&(i.wake=Number(e.wake)),e.wakeEase&&(i.ease=Number(e.wakeEase)),e.wakeGap&&(i.gap=e.wakeGap),e.wakeFade&&(i.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(i.pauseOnHover=!0),e.wakeReverse!==void 0&&(i.reverse=e.wakeReverse!=="false"),i}var r,$,I,w,T,g,z,D,H,v,f=class{constructor(e,i){y(this,r);this.element=e,this.options=W(i),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.speedPx=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",n(this,r,$).call(this),n(this,r,T).call(this),c.add(this),l.set(e,this),n(this,r,g).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,i=this.element.getBoundingClientRect(),s=200;return i.bottom<-s||i.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),n(this,r,w).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.measured||n(this,r,D).call(this),this.period=e,this.measured=!0,n(this,r,I).call(this);let i=b(this.track.getBoundingClientRect().width,e,j);for(;this.lanes.length<i;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(o=>o.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(o=>o.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>i;)this.lanes.pop()?.remove();d()}read(e){if(!n(this,r,v).call(this))return;let i=this.element.getBoundingClientRect(),s=S(i.top,i.height,e);this.amplitude=this.options.wake*i.width/100,this.wakePx=M(s,this.amplitude,this.dirSign),this.speedPx=x(this.options.speed,i.width)}write(e,i){if(!n(this,r,v).call(this))return;this.aligned||n(this,r,H).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*i:this.dirSign;this.dirFactor=C(this.dirFactor,s,e,this.options.ease),this.offset=E(this.offset,this.dirFactor*this.speedPx,e,this.period);let a=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let B of this.lanes)B.style.transform=a;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=i===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&q()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,c.delete(this),l.delete(this.element),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,$=function(){let e=this.element,i=e.ownerDocument;for(this.track=i.createElement("div"),this.track.className=R,this.lane=i.createElement("div"),this.lane.className=U;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(a,h)=>{e.hasAttribute(a)||(e.setAttribute(a,h),this.undo.push(()=>e.removeAttribute(a)))},o=(a,h)=>{e.style.getPropertyValue(a)||(e.style.setProperty(a,h),this.undo.push(()=>e.style.removeProperty(a)))};s(N,""),this.options.gap&&o("--wake-gap",this.options.gap),this.options.fade&&(o("--wake-fade",this.options.fade),s("data-wake-fade",""))},I=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},w=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,O(this.options.scroller)))},T=function(){let e=this.element;this.intersection=new IntersectionObserver(i=>{for(let s of i)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&n(this,r,w).call(this);d()},{rootMargin:V,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>n(this,r,g).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},g=function(){if(!this.options.respectMotionPreference){d();return}q()?(this.paused=!0,n(this,r,z).call(this)):(this.paused=!1,d())},z=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},D=function(){if(F)return;let e=this.lane.firstElementChild;if(!e)return;let i=getComputedStyle(e),s=i.getPropertyValue("--wake-gap").trim();!((s===""?32:parseFloat(s))>0)||parseFloat(i.marginInlineEnd)!==0||(F=!0,console.warn("wake-marquee: the items have no spacing. An unlayered CSS reset outranks @layer wake-marquee whatever the specificity says. Put the reset in a layer and name the order first: @layer reset, wake-marquee;"))},H=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},v=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function Z(t){let e=u===0?0:A(t-u);u=t;let i=window.innerHeight;for(let s of c)s.options.reverse&&J(s.options.scroller),s.read(i);for(let s of c)s.write(e,s.options.reverse?O(s.options.scroller):1);m=0,d()}function d(){if(m!==0)return;let t=!1;for(let e of c)if(!e.destroyed&&!e.paused&&e.visible){t=!0;break}if(!t){u=0;return}m=requestAnimationFrame(Z)}function re(t,e={}){if(!t||t.nodeType!==1)throw new TypeError("wake-marquee: createMarquee expects an element");if(L(t))throw new Error("wake-marquee: this element is already a marquee, call destroy() first");return new f(t,e).prime()}function ne({root:t=document,defaults:e={}}={}){let i=[];for(let s of t.querySelectorAll(`[${N}]`))L(s)||i.push(new f(s,{...e,...X(s)}));for(let s of i)s.prime();return i}function oe(t){return t&&l.get(t)||null}export{f as Marquee,re as createMarquee,G as defaults,oe as getMarquee,ne as initMarquees,W as normalizeOptions,X as readOptions};
@@ -8,12 +8,22 @@
8
8
  * Options are rendered as `data-wake-*` attributes and read back by the
9
9
  * script, so the server sends a plain clipped row and the client enhances it.
10
10
  * The row is on screen and correctly laid out before any JavaScript arrives.
11
+ *
12
+ * The script below starts the row and keeps no handle, because a hoisted Astro
13
+ * script has nowhere to hand one to. Ask for it by element instead:
14
+ *
15
+ * import { getMarquee } from 'wake-marquee'
16
+ * getMarquee(document.querySelector('#logos'))?.pause()
11
17
  */
12
18
  interface Props {
13
19
  /** Travel direction while scrolling down. Scrolling up reverses it. */
14
20
  direction?: 'left' | 'right';
15
- /** Travel speed in pixels per second. */
16
- speed?: number;
21
+ /**
22
+ * Travel speed in pixels per second, or a percentage of the container width
23
+ * per second (`"8%"`). The percentage keeps the pace the same on a phone
24
+ * and on a desktop; a fixed pixel speed does not.
25
+ */
26
+ speed?: number | `${number}%`;
17
27
  /** Scroll-driven displacement, in percent of the container width. 0 turns it off. */
18
28
  wake?: number;
19
29
  /** Whether scrolling up reverses travel. */
@@ -74,7 +84,9 @@ const style = [gap && `--wake-gap:${gap}`, fade && `--wake-fade:${fade}`]
74
84
 
75
85
  <script>
76
86
  // Astro hoists and deduplicates this, so a page with ten marquees still
77
- // ships one copy of the library and runs one initialisation pass.
87
+ // ships one copy of the library and runs one initialisation pass. The
88
+ // handles it returns are dropped on purpose: there is no way to pass them
89
+ // back to the page from here, and getMarquee(element) finds them again.
78
90
  import { initMarquees } from 'wake-marquee';
79
91
  initMarquees();
80
92
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wake-marquee",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An endless marquee that answers to the scroll: it reverses when the reader scrolls back, and drags against its own direction of travel as it crosses the viewport.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/marquee.js CHANGED
@@ -23,13 +23,24 @@
23
23
  * @module wake-marquee
24
24
  */
25
25
 
26
- import { advance, clamp, easeDirection, frameDelta, laneCount, viewProgress, wakeOffset } from './motion.js';
26
+ import {
27
+ advance,
28
+ clamp,
29
+ easeDirection,
30
+ frameDelta,
31
+ laneCount,
32
+ resolveSpeed,
33
+ viewProgress,
34
+ wakeOffset,
35
+ } from './motion.js';
27
36
 
28
37
  /**
29
38
  * @typedef {object} MarqueeOptions
30
39
  * @property {'left' | 'right'} [direction='left'] Travel direction while the
31
40
  * reader scrolls down. Scrolling up reverses it unless `reverse` is off.
32
- * @property {number} [speed=60] Travel speed in pixels per second.
41
+ * @property {number | string} [speed=60] Travel speed in pixels per second,
42
+ * or a percentage of the container width per second, e.g. `'8%'`. The
43
+ * percentage is the one that reads the same on a phone and on a desktop.
33
44
  * @property {number} [wake=8] Scroll-driven displacement, as a percentage of
34
45
  * the container width. `0` turns the wake off and leaves a plain loop.
35
46
  * @property {boolean} [reverse=true] Whether scrolling up reverses travel.
@@ -73,6 +84,25 @@ const LANE_BUFFER = 1;
73
84
  /** How far outside the viewport an instance starts running, in px. */
74
85
  const ROOT_MARGIN = '200px 0px';
75
86
 
87
+ /** The relative form of `speed`: `'8%'` of the container width, per second. */
88
+ const PERCENT = /^\d*\.?\d+%$/;
89
+
90
+ /**
91
+ * Is this a speed the loop can run at: px per second, or a percentage of the
92
+ * container width per second?
93
+ *
94
+ * Deliberately no time form. `'9.6s'` for one container width reads well until
95
+ * it sits next to the number form, where it reverses the direction of the
96
+ * option: `'12s'` would be slower than `'6s'` while `120` is faster than `60`.
97
+ *
98
+ * @param {unknown} value
99
+ * @returns {boolean}
100
+ */
101
+ function isSpeed(value) {
102
+ if (typeof value === 'string') return PERCENT.test(value);
103
+ return Number.isFinite(value) && /** @type {number} */ (value) >= 0;
104
+ }
105
+
76
106
  /**
77
107
  * Has this element already been turned into a marquee?
78
108
  *
@@ -91,12 +121,23 @@ function isInitialised(el) {
91
121
  /** @type {Set<Marquee>} Every live instance, driven by the one shared loop. */
92
122
  const registry = new Set();
93
123
 
124
+ /**
125
+ * @type {WeakMap<Element, Marquee>} The same instances, indexed by element, so
126
+ * a handle can be found again by whoever ends up needing one. Weak because the
127
+ * key is the caller's element: a row removed from the document should not be
128
+ * held alive by this.
129
+ */
130
+ const byElement = new WeakMap();
131
+
94
132
  /** @type {Map<Window | HTMLElement, {last: number, sign: number}>} */
95
133
  const scrollers = new Map();
96
134
 
97
135
  let rafId = 0;
98
136
  let lastFrame = 0;
99
137
 
138
+ /** Warned about collapsed spacing once? A global reset breaks every row. */
139
+ let warnedSpacing = false;
140
+
100
141
  /**
101
142
  * Read the scroll offset of either the window or an overflow container.
102
143
  * @param {Window | HTMLElement} scroller
@@ -167,8 +208,13 @@ export function normalizeOptions(options = {}) {
167
208
  if (config.direction !== 'left' && config.direction !== 'right') {
168
209
  throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${config.direction}"`);
169
210
  }
170
- if (!Number.isFinite(config.speed) || config.speed < 0) {
171
- throw new RangeError(`wake-marquee: speed must be a non-negative number, received ${config.speed}`);
211
+ if (typeof config.speed === 'string') config.speed = config.speed.trim();
212
+ if (!isSpeed(config.speed)) {
213
+ const received = typeof config.speed === 'string' ? `"${config.speed}"` : config.speed;
214
+ throw new RangeError(
215
+ 'wake-marquee: speed must be a non-negative number of pixels per second, or a percentage ' +
216
+ `of the container width per second such as "8%", received ${received}`,
217
+ );
172
218
  }
173
219
  if (!Number.isFinite(config.wake) || config.wake < 0) {
174
220
  throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${config.wake}`);
@@ -197,7 +243,13 @@ export function readOptions(el) {
197
243
  const options = {};
198
244
 
199
245
  if (d.wakeDirection) options.direction = /** @type {'left' | 'right'} */ (d.wakeDirection);
200
- if (d.wakeSpeed) options.speed = Number(d.wakeSpeed);
246
+ // A percentage passes through as the string it is. Anything else that is
247
+ // not a number passes through too, so normalisation can name it in the
248
+ // error rather than reporting the NaN that Number() would have made of it.
249
+ if (d.wakeSpeed) {
250
+ const speed = d.wakeSpeed.trim();
251
+ options.speed = Number.isFinite(Number(speed)) ? Number(speed) : speed;
252
+ }
201
253
  if (d.wake) options.wake = Number(d.wake);
202
254
  if (d.wakeEase) options.ease = Number(d.wakeEase);
203
255
  if (d.wakeGap) options.gap = d.wakeGap;
@@ -231,6 +283,8 @@ class Marquee {
231
283
  this.offset = 0;
232
284
  /** Width of one lane in px: the loop's repeat distance. */
233
285
  this.period = 0;
286
+ /** Last resolved travel speed in px per second. */
287
+ this.speedPx = 0;
234
288
  /** Last written wake displacement in px, kept so a paused frame holds. */
235
289
  this.wakePx = 0;
236
290
  /** Last measured wake amplitude in px. */
@@ -250,6 +304,7 @@ class Marquee {
250
304
  this.#observe();
251
305
 
252
306
  registry.add(this);
307
+ byElement.set(root, this);
253
308
  this.#applyMotionPreference();
254
309
  }
255
310
 
@@ -480,6 +535,7 @@ class Marquee {
480
535
  this.offset = (this.offset / this.period) * period;
481
536
  }
482
537
 
538
+ if (!this.measured) this.#checkSpacing();
483
539
  this.period = period;
484
540
  this.measured = true;
485
541
  this.#setOverhang();
@@ -515,6 +571,40 @@ class Marquee {
515
571
  schedule();
516
572
  }
517
573
 
574
+ /**
575
+ * Say something when a global CSS reset has eaten the spacing between items.
576
+ *
577
+ * `margin-inline-end` on the item lives in `@layer wake-marquee`, and every
578
+ * unlayered declaration beats every layered one whatever the specificity
579
+ * says, so an ordinary `* { margin: 0 }` reset wins over it. Nothing breaks
580
+ * loudly: the row still loops, `--wake-gap` still resolves correctly in the
581
+ * devtools, and the items simply sit flush against one another. A failure
582
+ * that quiet, in the interaction between two stylesheets, is worth a line in
583
+ * the console; it otherwise reads as a mistake in the consuming project.
584
+ *
585
+ * Runs once per instance, and never again on any of them once it has fired.
586
+ */
587
+ #checkSpacing() {
588
+ if (warnedSpacing) return;
589
+ const item = this.lane.firstElementChild;
590
+ if (!item) return;
591
+
592
+ const style = getComputedStyle(item);
593
+ // Unset resolves to the stylesheet's own 2rem, which is not zero either.
594
+ const declared = style.getPropertyValue('--wake-gap').trim();
595
+ const asked = declared === '' ? 32 : parseFloat(declared);
596
+ // A calc() or a var() chain parses as NaN. Unknowable, so say nothing:
597
+ // a false warning about someone else's CSS is worse than none.
598
+ if (!(asked > 0) || parseFloat(style.marginInlineEnd) !== 0) return;
599
+
600
+ warnedSpacing = true;
601
+ console.warn(
602
+ 'wake-marquee: the items have no spacing. An unlayered CSS reset outranks ' +
603
+ '@layer wake-marquee whatever the specificity says. Put the reset in a layer ' +
604
+ 'and name the order first: @layer reset, wake-marquee;',
605
+ );
606
+ }
607
+
518
608
  /** Read geometry. Never writes, so it cannot force a layout mid-frame. */
519
609
  read(viewport) {
520
610
  if (!this.#running()) return;
@@ -524,6 +614,11 @@ class Marquee {
524
614
  // overhang the track was given, and that is measured in these same px.
525
615
  this.amplitude = (this.options.wake * rect.width) / 100;
526
616
  this.wakePx = wakeOffset(progress, this.amplitude, this.dirSign);
617
+ // Resolved here rather than in write(), because a percentage speed is a
618
+ // fraction of this same width and this is where the width is honest. It
619
+ // is re-read every frame, so an option changed at runtime takes effect on
620
+ // the next one, and a container resized under a running row is followed.
621
+ this.speedPx = resolveSpeed(this.options.speed, rect.width);
527
622
  }
528
623
 
529
624
  /**
@@ -554,7 +649,7 @@ class Marquee {
554
649
  const target = this.hovered ? 0 : this.options.reverse ? this.dirSign * scrollDir : this.dirSign;
555
650
  this.dirFactor = easeDirection(this.dirFactor, target, dt, this.options.ease);
556
651
 
557
- this.offset = advance(this.offset, this.dirFactor * this.options.speed, dt, this.period);
652
+ this.offset = advance(this.offset, this.dirFactor * this.speedPx, dt, this.period);
558
653
 
559
654
  // One period of lead, so travelling left never exposes the origin.
560
655
  const x = this.offset - this.period;
@@ -598,6 +693,7 @@ class Marquee {
598
693
  if (this.destroyed) return;
599
694
  this.destroyed = true;
600
695
  registry.delete(this);
696
+ byElement.delete(this.element);
601
697
 
602
698
  this.intersection?.disconnect();
603
699
  this.resize?.disconnect();
@@ -713,4 +809,25 @@ export function initMarquees({ root = document, defaults = {} } = {}) {
713
809
  return created;
714
810
  }
715
811
 
812
+ /**
813
+ * The marquee running on an element, or `null` if there is none.
814
+ *
815
+ * `initMarquees()` returns only the instances that call created, which is the
816
+ * right answer for something safe to call repeatedly and the wrong one for
817
+ * anybody who needs a handle later: the second call over the same content
818
+ * returns an empty array, because every row is already running. Without a way
819
+ * back in, a declarative integration is a one-way door: `wake-marquee/astro`
820
+ * starts the rows for you and there is nothing to pause, refresh or destroy.
821
+ *
822
+ * @param {Element | null | undefined} element
823
+ * @returns {Marquee | null}
824
+ *
825
+ * @example
826
+ * const row = getMarquee(document.querySelector('#logos'))
827
+ * row?.pause()
828
+ */
829
+ export function getMarquee(element) {
830
+ return (element && byElement.get(element)) || null;
831
+ }
832
+
716
833
  export { DEFAULTS as defaults, Marquee };
package/src/motion.js CHANGED
@@ -37,6 +37,32 @@ export function laneCount(trackWidth, period, buffer = 1) {
37
37
  return Math.ceil(trackWidth / period) + 1 + buffer;
38
38
  }
39
39
 
40
+ /**
41
+ * Resolve the `speed` option to pixels per second.
42
+ *
43
+ * A number is already px/s. A percentage is that fraction of the container
44
+ * width per second, and it exists because px/s is a physical unit on a page
45
+ * whose elements are not physically constant: a logo row 40px tall with a
46
+ * 72px gap on a desktop is 16px tall with a 32px gap on a phone, so a fixed
47
+ * 150 px/s crosses a 1440px container in ten seconds and a 390px one in under
48
+ * three, with three times as many items passing per second. One value, calm on
49
+ * the desktop and hectic on the phone. A percentage scales with everything
50
+ * else on the page and reads as the same pace on both.
51
+ *
52
+ * A rate rather than a duration on purpose. `speed` already means "larger is
53
+ * faster" in its number form, and seconds-per-width would reverse that inside
54
+ * the same option: `'12s'` slower than `'6s'` while `120` is faster than `60`.
55
+ *
56
+ * @param {number | string} speed px per second, or `'8%'` of the container.
57
+ * @param {number} width Container width in px.
58
+ * @returns {number} px per second.
59
+ */
60
+ export function resolveSpeed(speed, width) {
61
+ if (typeof speed === 'number') return speed;
62
+ const percent = parseFloat(speed);
63
+ return Number.isFinite(percent) && width > 0 ? (percent * width) / 100 : 0;
64
+ }
65
+
40
66
  /**
41
67
  * Move the loop on by one frame and wrap it back into `[0, period)`.
42
68
  *
@@ -4,6 +4,12 @@
4
4
  * Everything lives in one cascade layer, so your own unlayered CSS wins
5
5
  * without reaching for !important. Declare the order yourself if you use
6
6
  * layers too: @layer theme, wake-marquee, utilities;
7
+ *
8
+ * The same rule cuts the other way, and it is the one thing to watch for: an
9
+ * unlayered global reset beats every layer whatever the specificity says, so
10
+ * `* { margin: 0 }` collapses the item spacing below to zero and the items sit
11
+ * flush. Put the reset in a layer of its own and name the order first:
12
+ * @layer reset, wake-marquee;
7
13
  */
8
14
  @layer wake-marquee {
9
15
  /**