wake-marquee 0.2.0 → 0.2.2

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,6 +8,60 @@ 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
+ ## [0.2.2] - 2026-09-02
12
+
13
+ A row whose container carries a transform was being measured on the axis the
14
+ screen sees rather than the one it travels along.
15
+
16
+ ### Fixed
17
+
18
+ - A container carrying a `transform` is measured on the axis the row actually
19
+ travels along. Every geometry read went through `getBoundingClientRect()`,
20
+ which reports the box after the transform, so a row under
21
+ `rotate(±90deg)` handed the loop its own thickness as its width and ran on
22
+ numbers wrong by its aspect ratio: three lanes where seven were needed,
23
+ leaving the last two thirds of the edge empty, a period that wrapped short,
24
+ and a wake and a percentage speed at a fraction of what was asked for. None
25
+ of it errored. The loop's geometry is now read in the row's own coordinate
26
+ system, which is where the lanes are translated and where the browser
27
+ resolves the overhang, so a rotated row behaves exactly like a flat one.
28
+ Any angle works, not only right angles.
29
+ - The wake amplitude is read back off the overhang the browser resolved,
30
+ rather than being worked out from the same percentage a second time. The two
31
+ could disagree, which under a `scale()` cost half the wake and on a
32
+ container with inline padding a little more than the reserve.
33
+
34
+ ### Changed
35
+
36
+ - A percentage `speed` under a `scale()` is now a fraction of the container
37
+ as the reader sees it, which is what `'8%'` reads as. It was measured
38
+ against the already scaled box and then rendered scaled again, so a row
39
+ inside `scale(0.5)` ran at a quarter of its speed rather than a half.
40
+
41
+ ## [0.2.1] - 2026-09-01
42
+
43
+ Two ways the row could still rearrange itself in front of the reader
44
+ after it had already started.
45
+
46
+ ### Fixed
47
+
48
+ - A row of images with no `width` and `height` no longer jerks sideways once
49
+ for every image that arrives. An unloaded, unsized `<img>` lays out zero
50
+ pixels wide, and the lane's width is the loop period, so the row was
51
+ starting on a period made of its gaps and nothing else: eight logos over a
52
+ phone connection measured 384px at the first frame and 1608px a second and a
53
+ half later, and yanked by up to 216px on each step in between. A row whose
54
+ items reserve no space now waits for them and hands over once, which is
55
+ invisible because what stands there meanwhile is the static row that was on
56
+ screen anyway. The wait is capped, so a request that never answers costs a
57
+ late start rather than a row that never runs. It is the safe behaviour and
58
+ not the good one, so it also warns, once per page: the fix is two attributes
59
+ in the markup, and they spare the rest of the page the same layout shift.
60
+ - A lane added by a resize is given the loop's current transform before it is
61
+ attached. `ResizeObserver` is notified after the same frame's
62
+ `requestAnimationFrame` callbacks, so a lane cloned from one was painted once
63
+ at its untransformed position, up to a whole period right of its siblings.
64
+
11
65
  ## [0.2.0] - 2026-09-01
12
66
 
13
67
  Three things adopting `0.1.0` in a real project turned up.
@@ -110,6 +164,8 @@ First release.
110
164
  - Right-to-left writing modes are not supported. Flex rows follow the writing
111
165
  direction while the transforms are physical, so the two disagree.
112
166
 
113
- [Unreleased]: https://github.com/robin-gogolok/wake-marquee/compare/v0.2.0...HEAD
167
+ [Unreleased]: https://github.com/robin-gogolok/wake-marquee/compare/v0.2.2...HEAD
168
+ [0.2.2]: https://github.com/robin-gogolok/wake-marquee/compare/v0.2.1...v0.2.2
169
+ [0.2.1]: https://github.com/robin-gogolok/wake-marquee/compare/v0.2.0...v0.2.1
114
170
  [0.2.0]: https://github.com/robin-gogolok/wake-marquee/compare/v0.1.0...v0.2.0
115
171
  [0.1.0]: https://github.com/robin-gogolok/wake-marquee/releases/tag/v0.1.0
package/README.md CHANGED
@@ -45,9 +45,9 @@ Write the container and its items. The direct children become the items; the lib
45
45
 
46
46
  ```html
47
47
  <div data-wake-marquee data-wake-speed="55" data-wake="10" data-wake-fade="6rem">
48
- <img src="/logos/a.svg" alt="Nordlicht">
49
- <img src="/logos/b.svg" alt="Kvist">
50
- <img src="/logos/c.svg" alt="Halden & Co">
48
+ <img src="/logos/a.svg" alt="Nordlicht" width="150" height="48">
49
+ <img src="/logos/b.svg" alt="Kvist" width="110" height="48">
50
+ <img src="/logos/c.svg" alt="Halden & Co" width="175" height="48">
51
51
  </div>
52
52
  ```
53
53
 
@@ -76,7 +76,7 @@ const row = createMarquee(document.querySelector('#logos'), {
76
76
  import WakeMarquee from 'wake-marquee/astro'
77
77
  ---
78
78
  <WakeMarquee speed="8%" wake={10} fade="6rem" aria-label="Brands we stock">
79
- {brands.map((b) => <img src={b.logo} alt={b.name} />)}
79
+ {brands.map((b) => <img src={b.logo} alt={b.name} width={b.width} height={48} />)}
80
80
  </WakeMarquee>
81
81
  ```
82
82
 
@@ -199,10 +199,16 @@ Same for `data-wake-fade` and `--wake-fade`. The library leaves a property
199
199
  that is already set alone, so there is no conflict. `wake-marquee/astro` does
200
200
  this for you.
201
201
 
202
- **Give images `width` and `height`.** An unsized image changes the lane width
203
- when it decodes, and the lane width is the loop period. The library restates
204
- the offset in the new period so the row does not leap, but the items around it
205
- still move: only the attributes prevent that.
202
+ **Give images `width` and `height`.** An unsized image is zero pixels wide
203
+ until it decodes, and the lane width is the loop period. A row of eight
204
+ unsized logos measures its gaps and nothing else at the first frame, then
205
+ grows into its real width one logo at a time.
206
+
207
+ The library will not start on a measurement like that: a row whose items
208
+ reserve no space waits for them, keeps standing as the static row the reader
209
+ was already looking at, and starts once. That is the safe behaviour, not the
210
+ good one, and it says so in the console. With the attributes there is nothing
211
+ to wait for and the row starts immediately.
206
212
 
207
213
  ## API
208
214
 
@@ -355,6 +361,23 @@ The same rule, the other way round. Declare the layer order before the imports,
355
361
 
356
362
  A layer registered late wins, and an `@import` after `tailwindcss` registers `wake-marquee` after `utilities`. Declared up front, it sits where it belongs: your utilities still win.
357
363
 
364
+ ## A row on its side
365
+
366
+ There is no vertical mode. Rotate the container:
367
+
368
+ ```css
369
+ .edge {
370
+ width: 30rem; /* the edge it runs along, not its thickness */
371
+ height: 2.75rem;
372
+ transform-origin: 0 0;
373
+ transform: rotate(90deg);
374
+ }
375
+ ```
376
+
377
+ The loop is measured in the row's own coordinate system rather than off the screen, so a rotated row gets the period, the lane count, the wake and a percentage speed it would have had lying flat. Any angle works, and so does a `scale()` on the row or on anything above it.
378
+
379
+ The one thing to write down is `width`: for a row on its side that is the edge it travels along, and nothing in CSS infers it from the height of whatever it is framing. The demo frames a block with four rows, two of them turned.
380
+
358
381
  ## Accessibility
359
382
 
360
383
  - **The content is announced once.** Clones are `inert` and `aria-hidden`, so a screen reader hears eight logos, not the forty that fill the track. Where `inert` is missing, focusable elements inside clones get `tabindex="-1"` instead, so they never become invisible tab stops.
@@ -386,9 +409,9 @@ There is no build-time or CSS-only version of this, and there cannot be one whil
386
409
 
387
410
  Filling the track means copying the row until it covers the container, typically two to four times. That is cheap for images and text and expensive for anything with its own runtime: iframes, videos, canvases and framework components with state get copied along with everything else. Rows of that kind want a different approach.
388
411
 
389
- ### Horizontal, and left-to-right
412
+ ### Left to right
390
413
 
391
- Vertical marquees are not supported. Neither is a right-to-left writing mode: the flex rows follow the writing direction while the transforms are physical, so the two disagree and the loop runs the wrong way. An `rtl` page can still use this inside an `ltr` container.
414
+ A right-to-left writing mode is not supported: the flex rows follow the writing direction while the transforms are physical, so the two disagree and the loop runs the wrong way. An `rtl` page can still use this inside an `ltr` container. `writing-mode` is the same story. For a vertical row, rotate the container instead, which is supported.
392
415
 
393
416
  ### One measurement, one period
394
417
 
@@ -1 +1 @@
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
+ var WakeMarquee=(()=>{var p=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var b=i=>{throw TypeError(i)};var Y=(i,e)=>{for(var t in e)p(i,t,{get:e[t],enumerable:!0})},J=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let h of W(e))!K.call(i,h)&&h!==t&&p(i,h,{get:()=>e[h],enumerable:!(s=V(e,h))||s.enumerable});return i};var X=i=>J(p({},"__esModule",{value:!0}),i);var Z=(i,e,t)=>e.has(i)||b("Cannot "+t);var E=(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)=>(Z(i,e,"access private method"),t);var pe={};Y(pe,{initMarquees:()=>f});function ee(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 S(i,e){if(typeof i=="number")return i;let t=parseFloat(i);return Number.isFinite(t)&&e>0?t*e/100:0}function M(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function C(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?ee((t-i)/s,0,1):0}function F(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 te=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),I="data-wake-marquee",ie="wake-lane",T="wake-track",se=1,re="200px 0px",ne=3e3,oe=/^\d*\.?\d+%$/;function ae(i){return typeof i=="string"?oe.test(i):Number.isFinite(i)&&i>=0}function he(i){return parseFloat(getComputedStyle(i).width)}function de(i){return i.firstElementChild?.classList.contains(T)===!0}var u=new Set,P=new WeakMap,m=new Map,w=0,l=0,L=!1,N=!1;function O(i){return i===window?window.scrollY:i.scrollTop}function ue(i){let e=m.get(i);if(!e){m.set(i,{last:O(i),sign:1});return}let t=O(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function z(i){return m.get(i)?.sign??1}function R(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function le(i={}){let e={...te};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()),!ae(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 ce(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,$,D,H,v,c,U,B,k,_,G,Q,y,g=class{constructor(e,t){E(this,r);this.element=e,this.options=le(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.width=0,this.aligned=!1,this.waiting=null,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",o(this,r,$).call(this),o(this,r,B).call(this),u.add(this),P.set(e,this),o(this,r,k).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,v).call(this),this)}refresh(){if(this.destroyed)return;let e=he(this.lane);if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.measured||o(this,r,G).call(this),this.period=e,this.measured=!0,o(this,r,D).call(this);let t=getComputedStyle(this.track),s=parseFloat(t.width);this.amplitude=-parseFloat(t.marginInlineStart),this.width=s-this.amplitude*2;let h=x(s,e,se);for(;this.lanes.length<h;){let n=this.lane.cloneNode(!0);n.inert=!0,n.setAttribute("aria-hidden","true"),"inert"in n||n.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(a=>a.setAttribute("tabindex","-1")),n.querySelectorAll('img[loading="lazy"]').forEach(a=>a.setAttribute("loading","eager")),n.style.transform=this.lane.style.transform,this.track.appendChild(n),this.lanes.push(n)}for(;this.lanes.length>h;)this.lanes.pop()?.remove();d()}read(e){if(!o(this,r,y).call(this))return;let t=this.element.getBoundingClientRect(),s=A(t.top,t.height,e);this.wakePx=F(s,this.amplitude,this.dirSign),this.speedPx=S(this.options.speed,this.width)}write(e,t){if(!o(this,r,y).call(this))return;this.aligned||o(this,r,Q).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=C(this.dirFactor,s,e,this.options.ease),this.offset=M(this.offset,this.dirFactor*this.speedPx,e,this.period);let n=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let j of this.lanes)j.style.transform=n;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let a=t===1?"forward":"reversed";a!==this.status&&(this.status=a,this.element.setAttribute("data-wake-travel",a))}play(){return this.destroyed?this:this.options.respectMotionPreference&&R()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),P.delete(this.element),this.waiting?.(),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,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=T,this.lane=t.createElement("div"),this.lane.className=ie;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(n,a)=>{e.hasAttribute(n)||(e.setAttribute(n,a),this.undo.push(()=>e.removeAttribute(n)))},h=(n,a)=>{e.style.getPropertyValue(n)||(e.style.setProperty(n,a),this.undo.push(()=>e.style.removeProperty(n)))};s(I,""),this.options.gap&&h("--wake-gap",this.options.gap),this.options.fade&&(h("--wake-fade",this.options.fade),s("data-wake-fade",""))},D=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},H=function(){let e=[];for(let t of this.lane.querySelectorAll("img"))!t.complete&&t.getBoundingClientRect().width===0&&e.push(t);return e},v=function(){if(this.waiting)return;let e=o(this,r,H).call(this);if(e.length===0){o(this,r,c).call(this);return}o(this,r,U).call(this);let t=e.length,s=()=>{--t>0||(n(),o(this,r,c).call(this))},h=setTimeout(()=>{n(),o(this,r,c).call(this)},ne),n=()=>{this.waiting=null,clearTimeout(h);for(let a of e)a.removeEventListener("load",s),a.removeEventListener("error",s)};this.waiting=n;for(let a of e)a.addEventListener("load",s),a.addEventListener("error",s)},c=function(){this.destroyed||(this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,z(this.options.scroller))))},U=function(){N||(N=!0,console.warn("wake-marquee: an item holds no width until it loads, so the loop period is not measurable yet and the row is waiting. Give each <img> its width and height attributes, or an aspect-ratio, and it starts immediately."))},B=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,v).call(this);d()},{rootMargin:re,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,k).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},k=function(){if(!this.options.respectMotionPreference){d();return}R()?(this.paused=!0,o(this,r,_).call(this)):(this.paused=!1,d())},_=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},G=function(){if(L)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||(L=!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;"))},Q=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},y=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function fe(i){let e=l===0?0:q(i-l);l=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&ue(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?z(s.options.scroller):1);w=0,d()}function d(){if(w!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){l=0;return}w=requestAnimationFrame(fe)}function f({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${I}]`))de(s)||t.push(new g(s,{...e,...ce(s)}));for(let s of t)s.prime();return t}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>f(),{once:!0}):f());return X(pe);})();
@@ -1 +1 @@
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};
1
+ var y=i=>{throw TypeError(i)};var j=(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)=>(j(i,e,"access private method"),t);function V(i,e,t){return i<e?e:i>t?t:i}function E(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function x(i,e){if(typeof i=="number")return i;let t=parseFloat(i);return Number.isFinite(t)&&e>0?t*e/100:0}function S(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function C(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function M(i,e,t){let s=t+e;return s>0?V((t-i)/s,0,1):0}function A(i,e,t){return-t*e*(1-2*i)}function F(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}),O="data-wake-marquee",K="wake-lane",R="wake-track",Y=1,J="200px 0px",X=3e3,Z=/^\d*\.?\d+%$/;function ee(i){return typeof i=="string"?Z.test(i):Number.isFinite(i)&&i>=0}function te(i){return parseFloat(getComputedStyle(i).width)}function I(i){return i.firstElementChild?.classList.contains(R)===!0}var l=new Set,p=new WeakMap,m=new Map,w=0,u=0,q=!1,P=!1;function L(i){return i===window?window.scrollY:i.scrollTop}function ie(i){let e=m.get(i);if(!e){m.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 T(i){return m.get(i)?.sign??1}function N(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function se(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()),!ee(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 re(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,z,$,D,g,c,H,U,v,B,_,G,k,f=class{constructor(e,t){b(this,r);this.element=e,this.options=se(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.width=0,this.aligned=!1,this.waiting=null,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",o(this,r,z).call(this),o(this,r,U).call(this),l.add(this),p.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=te(this.lane);if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.measured||o(this,r,_).call(this),this.period=e,this.measured=!0,o(this,r,$).call(this);let t=getComputedStyle(this.track),s=parseFloat(t.width);this.amplitude=-parseFloat(t.marginInlineStart),this.width=s-this.amplitude*2;let h=E(s,e,Y);for(;this.lanes.length<h;){let n=this.lane.cloneNode(!0);n.inert=!0,n.setAttribute("aria-hidden","true"),"inert"in n||n.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(a=>a.setAttribute("tabindex","-1")),n.querySelectorAll('img[loading="lazy"]').forEach(a=>a.setAttribute("loading","eager")),n.style.transform=this.lane.style.transform,this.track.appendChild(n),this.lanes.push(n)}for(;this.lanes.length>h;)this.lanes.pop()?.remove();d()}read(e){if(!o(this,r,k).call(this))return;let t=this.element.getBoundingClientRect(),s=M(t.top,t.height,e);this.wakePx=A(s,this.amplitude,this.dirSign),this.speedPx=x(this.options.speed,this.width)}write(e,t){if(!o(this,r,k).call(this))return;this.aligned||o(this,r,G).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=C(this.dirFactor,s,e,this.options.ease),this.offset=S(this.offset,this.dirFactor*this.speedPx,e,this.period);let n=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let Q of this.lanes)Q.style.transform=n;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let a=t===1?"forward":"reversed";a!==this.status&&(this.status=a,this.element.setAttribute("data-wake-travel",a))}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,l.delete(this),p.delete(this.element),this.waiting?.(),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,z=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=R,this.lane=t.createElement("div"),this.lane.className=K;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(n,a)=>{e.hasAttribute(n)||(e.setAttribute(n,a),this.undo.push(()=>e.removeAttribute(n)))},h=(n,a)=>{e.style.getPropertyValue(n)||(e.style.setProperty(n,a),this.undo.push(()=>e.style.removeProperty(n)))};s(O,""),this.options.gap&&h("--wake-gap",this.options.gap),this.options.fade&&(h("--wake-fade",this.options.fade),s("data-wake-fade",""))},$=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},D=function(){let e=[];for(let t of this.lane.querySelectorAll("img"))!t.complete&&t.getBoundingClientRect().width===0&&e.push(t);return e},g=function(){if(this.waiting)return;let e=o(this,r,D).call(this);if(e.length===0){o(this,r,c).call(this);return}o(this,r,H).call(this);let t=e.length,s=()=>{--t>0||(n(),o(this,r,c).call(this))},h=setTimeout(()=>{n(),o(this,r,c).call(this)},X),n=()=>{this.waiting=null,clearTimeout(h);for(let a of e)a.removeEventListener("load",s),a.removeEventListener("error",s)};this.waiting=n;for(let a of e)a.addEventListener("load",s),a.addEventListener("error",s)},c=function(){this.destroyed||(this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,T(this.options.scroller))))},H=function(){P||(P=!0,console.warn("wake-marquee: an item holds no width until it loads, so the loop period is not measurable yet and the row is waiting. Give each <img> its width and height attributes, or an aspect-ratio, and it starts immediately."))},U=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}N()?(this.paused=!0,o(this,r,B).call(this)):(this.paused=!1,d())},B=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},_=function(){if(q)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||(q=!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;"))},G=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 ne(i){let e=u===0?0:F(i-u);u=i;let t=window.innerHeight;for(let s of l)s.options.reverse&&ie(s.options.scroller),s.read(t);for(let s of l)s.write(e,s.options.reverse?T(s.options.scroller):1);w=0,d()}function d(){if(w!==0)return;let i=!1;for(let e of l)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){u=0;return}w=requestAnimationFrame(ne)}function le(i,e={}){if(!i||i.nodeType!==1)throw new TypeError("wake-marquee: createMarquee expects an element");if(I(i))throw new Error("wake-marquee: this element is already a marquee, call destroy() first");return new f(i,e).prime()}function ue({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${O}]`))I(s)||t.push(new f(s,{...e,...re(s)}));for(let s of t)s.prime();return t}function ce(i){return i&&p.get(i)||null}export{f as Marquee,le as createMarquee,W as defaults,ce as getMarquee,ue as initMarquees,se as normalizeOptions,re as readOptions};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wake-marquee",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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
@@ -84,6 +84,13 @@ const LANE_BUFFER = 1;
84
84
  /** How far outside the viewport an instance starts running, in px. */
85
85
  const ROOT_MARGIN = '200px 0px';
86
86
 
87
+ /**
88
+ * How long a row waits for an image that reserves no space of its own before
89
+ * it gives up and starts anyway, in ms. Generous on purpose: standing still a
90
+ * moment longer is invisible, and starting early is the bug this guards.
91
+ */
92
+ const MEDIA_TIMEOUT = 3000;
93
+
87
94
  /** The relative form of `speed`: `'8%'` of the container width, per second. */
88
95
  const PERCENT = /^\d*\.?\d+%$/;
89
96
 
@@ -103,6 +110,36 @@ function isSpeed(value) {
103
110
  return Number.isFinite(value) && /** @type {number} */ (value) >= 0;
104
111
  }
105
112
 
113
+ /**
114
+ * The width an element occupies in its own coordinate system, before any
115
+ * transform between it and the viewport.
116
+ *
117
+ * `getBoundingClientRect()` reports the box after every transform on the
118
+ * element and on its ancestors, and that is the wrong number for every
119
+ * distance the loop is built from. The lanes are translated *inside* that
120
+ * coordinate system, and the track's overhang is a CSS percentage the browser
121
+ * resolves against the untransformed box. A container carrying
122
+ * `rotate(90deg)` therefore reports its own thickness as its width, and the
123
+ * row runs on numbers wrong by its aspect ratio: three lanes where seven cover
124
+ * the track, a period that wraps short, and a wake a fraction of the one that
125
+ * was asked for. Nothing errors, which is what makes it worth a helper.
126
+ *
127
+ * The resolved value of `width` is the used width in layout px: free of the
128
+ * transform, and still sub-pixel exact where `offsetWidth` would round. That
129
+ * matters most for the lane, whose width *is* the loop period: rounded to
130
+ * whole pixels the seam would jump by up to half a pixel on every wrap.
131
+ *
132
+ * An element that is not being rendered resolves to `auto`, which parses as
133
+ * NaN. That is deliberate and load-bearing: it is what keeps refresh() from
134
+ * locking in a period for a row inside a `display: none` ancestor.
135
+ *
136
+ * @param {HTMLElement} el
137
+ * @returns {number} Width in px, or NaN when the element is not rendered.
138
+ */
139
+ function layoutWidth(el) {
140
+ return parseFloat(getComputedStyle(el).width);
141
+ }
142
+
106
143
  /**
107
144
  * Has this element already been turned into a marquee?
108
145
  *
@@ -138,6 +175,9 @@ let lastFrame = 0;
138
175
  /** Warned about collapsed spacing once? A global reset breaks every row. */
139
176
  let warnedSpacing = false;
140
177
 
178
+ /** Warned about an image with no reserved space once? Same reasoning. */
179
+ let warnedUnsized = false;
180
+
141
181
  /**
142
182
  * Read the scroll offset of either the window or an overflow container.
143
183
  * @param {Window | HTMLElement} scroller
@@ -287,11 +327,20 @@ class Marquee {
287
327
  this.speedPx = 0;
288
328
  /** Last written wake displacement in px, kept so a paused frame holds. */
289
329
  this.wakePx = 0;
290
- /** Last measured wake amplitude in px. */
330
+ /** Last measured wake amplitude in px, read back off the overhang. */
291
331
  this.amplitude = 0;
332
+ /** Container width in px, in its own coordinate system. */
333
+ this.width = 0;
292
334
  /** Has the first frame been lined up with the static row it replaces? */
293
335
  this.aligned = false;
294
336
 
337
+ /**
338
+ * @type {(() => void) | null} Cancels the wait for an image that has not
339
+ * reserved its space yet, while there is one. Doubles as the guard that
340
+ * keeps a second observer callback from starting a second wait.
341
+ */
342
+ this.waiting = null;
343
+
295
344
  this.visible = false;
296
345
  this.measured = false;
297
346
  this.paused = false;
@@ -393,11 +442,85 @@ class Marquee {
393
442
  }
394
443
 
395
444
  /**
396
- * Take the row from its static state to its running one inside a single
397
- * task: overhang, measurement, clones and the first transform together.
398
- * Anything left over for the next frame gets painted on its own.
445
+ * The images in the lane that will move the loop period when they arrive.
446
+ *
447
+ * An `<img>` with no width and height in the markup lays out zero pixels
448
+ * wide until it has decoded, and the lane's width *is* the period. Measure
449
+ * now and the row runs on a period that is a fraction of its real one, then
450
+ * grows into it a logo at a time: eight logos, eight sideways yanks, spread
451
+ * over however long the slowest of them takes. A row of eight unsized
452
+ * wordmarks measures 384px at the first frame and 1608px a second and a
453
+ * half later, and jerks by up to 216px on each step in between.
454
+ *
455
+ * Deliberately a measurement rather than a plain `!complete`. An image that
456
+ * already holds its own box cannot change the period whatever it turns out
457
+ * to contain, so markup that reserves its space needs no waiting at all and
458
+ * keeps the single synchronous handover it has today.
459
+ *
460
+ * @returns {HTMLImageElement[]}
461
+ */
462
+ #unsizedImages() {
463
+ const pending = [];
464
+ for (const img of this.lane.querySelectorAll('img')) {
465
+ if (!img.complete && img.getBoundingClientRect().width === 0) pending.push(img);
466
+ }
467
+ return pending;
468
+ }
469
+
470
+ /**
471
+ * Take the row from its static state to its running one, once the period it
472
+ * would measure is the period it is going to keep.
473
+ *
474
+ * Held back while the lane still contains images that reserve no space. The
475
+ * wait costs nothing anybody can see: what stands there in the meantime is
476
+ * the static clipped row, which is what was on screen anyway, and it now
477
+ * becomes a moving row exactly once instead of stepping into place.
399
478
  */
400
479
  #activate() {
480
+ if (this.waiting) return;
481
+
482
+ const pending = this.#unsizedImages();
483
+ if (pending.length === 0) {
484
+ this.#handover();
485
+ return;
486
+ }
487
+ this.#warnUnsized();
488
+
489
+ let left = pending.length;
490
+ const done = () => {
491
+ if (--left > 0) return;
492
+ release();
493
+ this.#handover();
494
+ };
495
+ // Never wait indefinitely: a request that never answers must not leave the
496
+ // row static for the rest of the session. Past the cap the old behaviour
497
+ // takes over, and refresh() restates the phase when the image does land.
498
+ const timer = setTimeout(() => {
499
+ release();
500
+ this.#handover();
501
+ }, MEDIA_TIMEOUT);
502
+ const release = () => {
503
+ this.waiting = null;
504
+ clearTimeout(timer);
505
+ for (const img of pending) {
506
+ img.removeEventListener('load', done);
507
+ img.removeEventListener('error', done);
508
+ }
509
+ };
510
+
511
+ this.waiting = release;
512
+ for (const img of pending) {
513
+ img.addEventListener('load', done);
514
+ img.addEventListener('error', done);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Overhang, measurement, clones and the first transform, in one task.
520
+ * Anything left over for the next frame gets painted on its own.
521
+ */
522
+ #handover() {
523
+ if (this.destroyed) return;
401
524
  this.refresh();
402
525
  if (this.period > 0 && !this.paused) {
403
526
  this.read(window.innerHeight);
@@ -405,6 +528,27 @@ class Marquee {
405
528
  }
406
529
  }
407
530
 
531
+ /**
532
+ * Say something when an item holds no space until it loads.
533
+ *
534
+ * The row recovers on its own now, but it recovers by standing still until
535
+ * the last image lands, which on a slow connection is the row not starting
536
+ * for a second or two. The fix belongs in the markup, costs one attribute
537
+ * pair, and is worth knowing about: `width` and `height` on the `<img>` also
538
+ * spare the reader the layout shift the rest of the page pays for.
539
+ *
540
+ * Once per page, like the spacing warning.
541
+ */
542
+ #warnUnsized() {
543
+ if (warnedUnsized) return;
544
+ warnedUnsized = true;
545
+ console.warn(
546
+ 'wake-marquee: an item holds no width until it loads, so the loop period is not ' +
547
+ 'measurable yet and the row is waiting. Give each <img> its width and height ' +
548
+ 'attributes, or an aspect-ratio, and it starts immediately.',
549
+ );
550
+ }
551
+
408
552
  #observe() {
409
553
  const root = this.element;
410
554
 
@@ -520,10 +664,12 @@ class Marquee {
520
664
  refresh() {
521
665
  if (this.destroyed) return;
522
666
 
523
- const period = this.lane.getBoundingClientRect().width;
667
+ const period = layoutWidth(this.lane);
524
668
  // A display:none ancestor, or a lane whose images have not laid out yet,
525
- // measures zero. Bailing leaves `measured` false so the next observer
526
- // callback tries again, rather than locking in a broken period.
669
+ // measures nothing. Bailing leaves `measured` false so the next observer
670
+ // callback tries again, rather than locking in a broken period. It has to
671
+ // stay ahead of the track read below: a hidden track still reports the
672
+ // `120%` and `-10%` it was given, which parse as plausible numbers.
527
673
  if (!(period > 0)) return;
528
674
 
529
675
  // The period is the unit the offset is expressed in, so when it changes
@@ -540,7 +686,19 @@ class Marquee {
540
686
  this.measured = true;
541
687
  this.#setOverhang();
542
688
 
543
- const needed = laneCount(this.track.getBoundingClientRect().width, period, LANE_BUFFER);
689
+ // Read the overhang back rather than working it out a second time. The
690
+ // browser has just resolved that percentage against the container's own
691
+ // box, so this is exactly the reserve the wake is allowed to spend,
692
+ // whatever padding, box-sizing or transform the caller's container
693
+ // carries. Derived twice the two can disagree; read back they cannot.
694
+ const overhang = getComputedStyle(this.track);
695
+ const trackWidth = parseFloat(overhang.width);
696
+ this.amplitude = -parseFloat(overhang.marginInlineStart);
697
+ // The track is the container plus that reserve on either side, so the
698
+ // container falls out of the two numbers already in hand.
699
+ this.width = trackWidth - this.amplitude * 2;
700
+
701
+ const needed = laneCount(trackWidth, period, LANE_BUFFER);
544
702
 
545
703
  while (this.lanes.length < needed) {
546
704
  const clone = /** @type {HTMLElement} */ (this.lane.cloneNode(true));
@@ -560,6 +718,11 @@ class Marquee {
560
718
  // hole in the row. The source is identical to the original, so this is
561
719
  // a cache hit rather than a second download.
562
720
  clone.querySelectorAll('img[loading="lazy"]').forEach((img) => img.setAttribute('loading', 'eager'));
721
+ // A ResizeObserver is notified after the same frame's rAF callbacks, so
722
+ // a lane added from one would be painted once before the loop could
723
+ // transform it, sitting up to a whole period right of its siblings for
724
+ // that frame. Matching it to them before it is attached costs nothing.
725
+ clone.style.transform = this.lane.style.transform;
563
726
  this.track.appendChild(clone);
564
727
  this.lanes.push(clone);
565
728
  }
@@ -608,17 +771,19 @@ class Marquee {
608
771
  /** Read geometry. Never writes, so it cannot force a layout mid-frame. */
609
772
  read(viewport) {
610
773
  if (!this.#running()) return;
774
+ // The transformed box, on purpose, and the only place it is wanted: the
775
+ // wake is driven by the element crossing the viewport, so where it is on
776
+ // screen is the honest question. For a rotated row that is its long side,
777
+ // which is right. Every distance the loop travels is read in refresh()
778
+ // instead, in the coordinate system the transforms are written in.
611
779
  const rect = this.element.getBoundingClientRect();
612
780
  const progress = viewProgress(rect.top, rect.height, viewport);
613
- // Kept because the first frame's alignment has to undo exactly the
614
- // overhang the track was given, and that is measured in these same px.
615
- this.amplitude = (this.options.wake * rect.width) / 100;
616
781
  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);
782
+ // Resolved here rather than in write() so an option changed at runtime
783
+ // takes effect on the next frame. The width it is a fraction of comes
784
+ // from the last measurement, and a container resized under a running row
785
+ // is still followed: that resize is what the ResizeObserver is for.
786
+ this.speedPx = resolveSpeed(this.options.speed, this.width);
622
787
  }
623
788
 
624
789
  /**
@@ -695,6 +860,7 @@ class Marquee {
695
860
  registry.delete(this);
696
861
  byElement.delete(this.element);
697
862
 
863
+ this.waiting?.();
698
864
  this.intersection?.disconnect();
699
865
  this.resize?.disconnect();
700
866
  if (this.onEnter) this.element.removeEventListener('pointerenter', this.onEnter);