@brandocms/jupiter 5.0.0-beta.14 → 5.0.0-beta.16
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/package.json +1 -1
- package/src/modules/Looper/index.js +66 -32
- package/src/modules/Marquee/index.js +438 -45
- package/src/utils/motion-helpers.js +12 -1
- package/types/modules/DoubleHeader/index.d.ts +6 -0
- package/types/modules/FixedHeader/index.d.ts +5 -0
- package/types/modules/Marquee/index.d.ts +59 -3
- package/types/modules/Moonwalk/index.d.ts +1 -1
- package/types/modules/StickyHeader/index.d.ts +5 -0
package/package.json
CHANGED
|
@@ -67,6 +67,10 @@ const DEFAULT_OPTIONS = {
|
|
|
67
67
|
mouseOut: { speed: 1, duration: 0.75 },
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
+
// Called when the looper is ready to reveal. Receives (wrapper, loop) args.
|
|
71
|
+
// Override to control the reveal animation yourself. Default fades in the wrapper.
|
|
72
|
+
onReveal: null,
|
|
73
|
+
|
|
70
74
|
selector: '[data-moonwalk-run="loop"]',
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -143,6 +147,23 @@ function horizontalLoop(app, items, config) {
|
|
|
143
147
|
// Cache track element reference (M3)
|
|
144
148
|
const trackElement = items[0].parentElement
|
|
145
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The element the pointer actually lands on.
|
|
152
|
+
*
|
|
153
|
+
* `trackElement` is what gets translated, so once the carousel has been
|
|
154
|
+
* dragged its box has left the viewport — and with it the `pointerdown`
|
|
155
|
+
* listener, the `touch-action` and the `grab` cursor. The visible row is then
|
|
156
|
+
* made of item boxes with dead gaps between them: pressing a slide still
|
|
157
|
+
* works (the event bubbles up through the track) but pressing between two
|
|
158
|
+
* slides hits nothing. It reads as "drag works until you have dragged".
|
|
159
|
+
*
|
|
160
|
+
* So when `container` is the track itself, the surface moves to the wrapper,
|
|
161
|
+
* which stays put and spans the same visible area. When `center` gave us a
|
|
162
|
+
* separate container it is already stationary and keeps the job.
|
|
163
|
+
*/
|
|
164
|
+
const dragSurface =
|
|
165
|
+
(container === trackElement ? config.wrapper : container) || container
|
|
166
|
+
|
|
146
167
|
/**
|
|
147
168
|
* Measure total width of all items as currently laid out
|
|
148
169
|
* @returns {number} Total width in pixels
|
|
@@ -873,6 +894,18 @@ function horizontalLoop(app, items, config) {
|
|
|
873
894
|
// Only handle primary pointer (left click, first touch)
|
|
874
895
|
if (e.button !== undefined && e.button !== 0) return
|
|
875
896
|
|
|
897
|
+
// The drag surface is the wrapper, and next/previous buttons live inside
|
|
898
|
+
// it — pressing one must not also arm a drag, or the tap that follows
|
|
899
|
+
// snaps the row on top of the navigation the button just did. Slides are
|
|
900
|
+
// deliberately not exempt: a slide may be a link and still be draggable.
|
|
901
|
+
// `[data-looper-no-drag]` opts anything else in the wrapper out.
|
|
902
|
+
if (
|
|
903
|
+
e.target.closest &&
|
|
904
|
+
e.target.closest('[data-panner-next], [data-panner-previous], [data-looper-no-drag]')
|
|
905
|
+
) {
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
|
|
876
909
|
isDragging = true
|
|
877
910
|
indexSetByNav = false
|
|
878
911
|
startX = e.clientX
|
|
@@ -928,13 +961,11 @@ function horizontalLoop(app, items, config) {
|
|
|
928
961
|
e.preventDefault()
|
|
929
962
|
}
|
|
930
963
|
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
container.addEventListener('pointerup', onPointerUp)
|
|
937
|
-
container.addEventListener('pointercancel', onPointerUp)
|
|
964
|
+
// Add move/up listeners to window so events are never lost
|
|
965
|
+
// (pointer capture can silently fail or be released mid-drag)
|
|
966
|
+
window.addEventListener('pointermove', onPointerMove, { passive: false })
|
|
967
|
+
window.addEventListener('pointerup', onPointerUp)
|
|
968
|
+
window.addEventListener('pointercancel', onPointerUp)
|
|
938
969
|
}
|
|
939
970
|
|
|
940
971
|
/**
|
|
@@ -962,10 +993,9 @@ function horizontalLoop(app, items, config) {
|
|
|
962
993
|
// Bias toward carousel interaction: vertical must be 1.2x horizontal to abort.
|
|
963
994
|
if (deltaY > deltaX * 1.2) {
|
|
964
995
|
isDragging = false
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
container.removeEventListener('pointercancel', onPointerUp)
|
|
996
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
997
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
998
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
969
999
|
// Resume crawl if we stopped it on pointerdown
|
|
970
1000
|
if (stoppedAnimation && config.crawl) {
|
|
971
1001
|
resumeCrawl()
|
|
@@ -975,7 +1005,7 @@ function horizontalLoop(app, items, config) {
|
|
|
975
1005
|
|
|
976
1006
|
// Horizontal — commit to drag
|
|
977
1007
|
hasDragged = true
|
|
978
|
-
|
|
1008
|
+
dragSurface.style.cursor = 'grabbing'
|
|
979
1009
|
trackElement.classList.add('looper-dragging')
|
|
980
1010
|
}
|
|
981
1011
|
|
|
@@ -1014,22 +1044,19 @@ function horizontalLoop(app, items, config) {
|
|
|
1014
1044
|
|
|
1015
1045
|
isDragging = false
|
|
1016
1046
|
|
|
1017
|
-
// Release pointer capture
|
|
1018
|
-
try { container.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
1019
|
-
|
|
1020
1047
|
// Clean up listeners
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1048
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
1049
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
1050
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
1024
1051
|
|
|
1025
1052
|
// Reset cursor and re-enable hover effects (only if we actually dragged)
|
|
1026
1053
|
if (hasDragged) {
|
|
1027
|
-
|
|
1054
|
+
dragSurface.style.cursor = 'grab'
|
|
1028
1055
|
trackElement.classList.remove('looper-dragging')
|
|
1029
1056
|
|
|
1030
1057
|
// Swallow the next click event so links don't navigate after a drag.
|
|
1031
1058
|
// The browser fires click after pointerup — capture it before it reaches any <a>.
|
|
1032
|
-
|
|
1059
|
+
dragSurface.addEventListener('click', swallowClick, { capture: true, once: true })
|
|
1033
1060
|
}
|
|
1034
1061
|
|
|
1035
1062
|
// If this was a click (not a drag):
|
|
@@ -1353,20 +1380,20 @@ function horizontalLoop(app, items, config) {
|
|
|
1353
1380
|
}
|
|
1354
1381
|
|
|
1355
1382
|
// Set up touch-action CSS for proper touch handling
|
|
1356
|
-
|
|
1383
|
+
dragSurface.style.touchAction = 'pan-y' // Allow vertical scroll, prevent horizontal
|
|
1357
1384
|
|
|
1358
1385
|
// Add pointer down listener
|
|
1359
|
-
|
|
1360
|
-
|
|
1386
|
+
dragSurface.style.cursor = 'grab'
|
|
1387
|
+
dragSurface.addEventListener('pointerdown', onPointerDown)
|
|
1361
1388
|
|
|
1362
1389
|
// Store cleanup function
|
|
1363
1390
|
dragState = {
|
|
1364
1391
|
cleanup: () => {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1392
|
+
dragSurface.removeEventListener('pointerdown', onPointerDown)
|
|
1393
|
+
dragSurface.removeEventListener('click', swallowClick, { capture: true })
|
|
1394
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
1395
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
1396
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
1370
1397
|
},
|
|
1371
1398
|
}
|
|
1372
1399
|
}
|
|
@@ -1750,9 +1777,9 @@ function horizontalLoop(app, items, config) {
|
|
|
1750
1777
|
trackElement.style.willChange = ''
|
|
1751
1778
|
trackElement.style.transform = ''
|
|
1752
1779
|
|
|
1753
|
-
// Clear inline styles on
|
|
1754
|
-
|
|
1755
|
-
|
|
1780
|
+
// Clear inline styles on the drag surface
|
|
1781
|
+
dragSurface.style.touchAction = ''
|
|
1782
|
+
dragSurface.style.cursor = ''
|
|
1756
1783
|
|
|
1757
1784
|
// Clear item wrap transforms
|
|
1758
1785
|
items.forEach(item => {
|
|
@@ -1937,7 +1964,14 @@ export default class Looper {
|
|
|
1937
1964
|
|
|
1938
1965
|
// Reveal lazyload images: immediately reveal off-screen items (no visible transition),
|
|
1939
1966
|
// defer reveal of viewport items until after wrapper fade-in for a nice per-image fade
|
|
1940
|
-
if (this.
|
|
1967
|
+
if (this.opts.onReveal) {
|
|
1968
|
+
// Custom reveal callback — caller handles animation and lazyload
|
|
1969
|
+
if (this.app?.lazyload && wrapper) {
|
|
1970
|
+
const pictures = Dom.all(wrapper, '[data-ll-srcset]')
|
|
1971
|
+
pictures.forEach(picture => this.app.lazyload.revealPicture(picture))
|
|
1972
|
+
}
|
|
1973
|
+
this.opts.onReveal(wrapper, loop)
|
|
1974
|
+
} else if (this.app?.lazyload && wrapper) {
|
|
1941
1975
|
const wrapperRect = wrapper.getBoundingClientRect()
|
|
1942
1976
|
const pictures = Dom.all(wrapper, '[data-ll-srcset]')
|
|
1943
1977
|
const viewportPictures = []
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { animate } from 'motion'
|
|
1
|
+
import { animate, motionValue } from 'motion'
|
|
2
2
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
3
3
|
import Dom from '../Dom'
|
|
4
4
|
import { set, clearProps } from '../../utils/motion-helpers'
|
|
5
5
|
|
|
6
|
+
// Velocity sampling window for throws (ms)
|
|
7
|
+
const VELOCITY_WINDOW_MS = 150
|
|
8
|
+
// Speed the crawl ramps up from after a throw settles
|
|
9
|
+
const MIN_CRAWL_SPEED = 0.001
|
|
10
|
+
const SPEED_RAMP_DURATION = 1.2
|
|
11
|
+
// Crawl speed while the pointer rests on the marquee (slowDownOnHover)
|
|
12
|
+
const HOVER_SPEED = 0.5
|
|
13
|
+
|
|
6
14
|
const DEFAULT_OPTIONS = {
|
|
7
15
|
speed: 100,
|
|
8
16
|
extraHeight: 0,
|
|
@@ -11,6 +19,18 @@ const DEFAULT_OPTIONS = {
|
|
|
11
19
|
startProgress: 0,
|
|
12
20
|
spacer: '<span> — </span>',
|
|
13
21
|
|
|
22
|
+
// Crawl direction. false = scroll content leftward (default), true = rightward.
|
|
23
|
+
// Speed is controlled independently via `speed` (and the live timeline.speed).
|
|
24
|
+
reversed: false,
|
|
25
|
+
|
|
26
|
+
// Drag + throw (inertia). Opt-in — existing marquees are unaffected.
|
|
27
|
+
draggable: false,
|
|
28
|
+
minimumMovement: 3, // Pixels for mouse - below this a press is a click, above is a drag
|
|
29
|
+
touchMinimumMovement: 10, // Higher threshold for touch (finger imprecision)
|
|
30
|
+
throwResistance: 325, // Inertia time constant (higher = longer glide)
|
|
31
|
+
throwPower: 0.8, // Deceleration curve (0-1)
|
|
32
|
+
throwVelocityMultiplier: 0.8, // Scale release velocity
|
|
33
|
+
|
|
14
34
|
onReveal: marqueeEl => {
|
|
15
35
|
animate(marqueeEl, { opacity: 1 }, { ease: 'linear' })
|
|
16
36
|
}
|
|
@@ -25,24 +45,83 @@ export default class Marquee {
|
|
|
25
45
|
this.elements.$marquee = Dom.find(this.elements.$el, '[data-marquee]')
|
|
26
46
|
this.elements.$holder = Dom.find(this.elements.$el, '[data-marquee-holder]')
|
|
27
47
|
this.elements.$item = Dom.find(this.elements.$el, '[data-marquee-item]')
|
|
28
|
-
|
|
48
|
+
|
|
49
|
+
// Position model: a single motionValue is the source of truth. The crawl
|
|
50
|
+
// animation, drag and inertia all write to it; a change listener applies a
|
|
51
|
+
// seamless (modulo holderWidth) transform to the marquee element.
|
|
52
|
+
this.position = motionValue(0)
|
|
53
|
+
this.holderWidth = 0
|
|
54
|
+
this.duration = 0
|
|
55
|
+
|
|
56
|
+
this.timeline = null // crawl animation (kept named `timeline` for back-compat)
|
|
57
|
+
this.speedAnimation = null
|
|
58
|
+
this.inertiaAnimation = null
|
|
29
59
|
this.observer = null
|
|
60
|
+
this.playing = false
|
|
61
|
+
|
|
62
|
+
// Tracks whether the pointer is over the marquee, so a crawl recreated
|
|
63
|
+
// after a drag or throw resumes at the hover speed rather than full speed.
|
|
64
|
+
this.hovering = false
|
|
65
|
+
|
|
66
|
+
// Bound handlers (stored so they can be removed in destroy)
|
|
67
|
+
this._onResize = this.updateMarquee.bind(this)
|
|
68
|
+
this._onReveal = this.revealMarquee.bind(this)
|
|
69
|
+
this._onMouseEnter = this.slowDown.bind(this)
|
|
70
|
+
this._onMouseLeave = this.speedUp.bind(this)
|
|
71
|
+
this._positionUnsubscribe = null
|
|
72
|
+
this._dragCleanup = null
|
|
30
73
|
|
|
31
74
|
this.initialize()
|
|
32
75
|
}
|
|
33
76
|
|
|
34
77
|
initialize() {
|
|
35
|
-
set(this.elements.$marquee, { opacity: 0 })
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
set(this.elements.$marquee, { opacity: 0, willChange: 'transform' })
|
|
79
|
+
|
|
80
|
+
// Apply the position to the DOM whenever it changes (crawl, drag or inertia)
|
|
81
|
+
this._positionUnsubscribe = this.position.on('change', latest => {
|
|
82
|
+
this.render(latest)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
window.addEventListener('APPLICATION:RESIZE', this._onResize)
|
|
86
|
+
window.addEventListener('APPLICATION:REVEALED', this._onReveal)
|
|
38
87
|
this.updateMarquee()
|
|
39
88
|
this.setupObserver()
|
|
89
|
+
|
|
40
90
|
if (this.opts.slowDownOnHover) {
|
|
41
|
-
this.elements.$el.addEventListener('mouseenter', this.
|
|
42
|
-
this.elements.$el.addEventListener('mouseleave', this.
|
|
91
|
+
this.elements.$el.addEventListener('mouseenter', this._onMouseEnter)
|
|
92
|
+
this.elements.$el.addEventListener('mouseleave', this._onMouseLeave)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this.opts.draggable) {
|
|
96
|
+
this.setupDrag()
|
|
43
97
|
}
|
|
44
98
|
}
|
|
45
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Apply the current raw position to the marquee element.
|
|
102
|
+
* Holders are laid out side by side at i * holderWidth, so translating by
|
|
103
|
+
* the position modulo holderWidth produces a seamless infinite scroll.
|
|
104
|
+
*/
|
|
105
|
+
render(rawPos) {
|
|
106
|
+
if (!this.holderWidth) return
|
|
107
|
+
const m = ((rawPos % this.holderWidth) + this.holderWidth) % this.holderWidth
|
|
108
|
+
this.elements.$marquee.style.transform = `translateX(${-m}px) translateZ(0)`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detect whether the marquee element is horizontally mirrored (scaleX(-1)),
|
|
113
|
+
* as used for "reverse" rows. Read from the live computed transform so it
|
|
114
|
+
* works no matter how the flip is applied (and survives responsive changes).
|
|
115
|
+
*/
|
|
116
|
+
isAxisFlipped() {
|
|
117
|
+
const t = getComputedStyle(this.elements.$el).transform
|
|
118
|
+
if (!t || t === 'none') return false
|
|
119
|
+
// matrix(a, b, c, d, e, f) / matrix3d(a, ...) — a < 0 means a horizontal flip
|
|
120
|
+
const inner = t.slice(t.indexOf('(') + 1)
|
|
121
|
+
const a = parseFloat(inner)
|
|
122
|
+
return Number.isFinite(a) && a < 0
|
|
123
|
+
}
|
|
124
|
+
|
|
46
125
|
revealMarquee(e) {
|
|
47
126
|
this.updateMarquee()
|
|
48
127
|
this.opts.onReveal(this.elements.$marquee)
|
|
@@ -61,14 +140,10 @@ export default class Marquee {
|
|
|
61
140
|
this.fillText()
|
|
62
141
|
this.setHeight()
|
|
63
142
|
|
|
64
|
-
|
|
143
|
+
this.holderWidth = this.elements.$holder.offsetWidth
|
|
65
144
|
const $allHolders = Dom.all(this.elements.$el, '[data-marquee-holder]')
|
|
66
|
-
const marqueeWidth = holderWidth * $allHolders.length
|
|
67
|
-
|
|
68
|
-
this.duration = Math.min(
|
|
69
|
-
(holderWidth + marqueeWidth) / this.opts.speed,
|
|
70
|
-
40
|
|
71
|
-
)
|
|
145
|
+
const marqueeWidth = this.holderWidth * $allHolders.length
|
|
146
|
+
this.duration = this.holderWidth / this.opts.speed
|
|
72
147
|
|
|
73
148
|
set(this.elements.$marquee, { width: marqueeWidth })
|
|
74
149
|
this.initializeTween()
|
|
@@ -84,16 +159,23 @@ export default class Marquee {
|
|
|
84
159
|
}
|
|
85
160
|
|
|
86
161
|
killTweens() {
|
|
87
|
-
if (this.timeline) {
|
|
88
|
-
this.timeline.stop()
|
|
89
|
-
this.timeline = null
|
|
90
|
-
}
|
|
91
162
|
if (this.speedAnimation) {
|
|
92
163
|
this.speedAnimation.stop()
|
|
93
164
|
this.speedAnimation = null
|
|
94
165
|
}
|
|
166
|
+
if (this.inertiaAnimation) {
|
|
167
|
+
this.inertiaAnimation.stop()
|
|
168
|
+
this.inertiaAnimation = null
|
|
169
|
+
}
|
|
170
|
+
if (this.timeline) {
|
|
171
|
+
this.timeline.stop()
|
|
172
|
+
this.timeline = null
|
|
173
|
+
}
|
|
95
174
|
}
|
|
96
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Lay out holders and create the (paused) crawl animation.
|
|
178
|
+
*/
|
|
97
179
|
initializeTween() {
|
|
98
180
|
const $allHolders = Dom.all(this.elements.$el, '[data-marquee-holder]')
|
|
99
181
|
|
|
@@ -106,50 +188,85 @@ export default class Marquee {
|
|
|
106
188
|
})
|
|
107
189
|
})
|
|
108
190
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
191
|
+
// Set initial progress before creating the crawl so it starts from there.
|
|
192
|
+
// On resize rebuilds the existing position is preserved (modulo render
|
|
193
|
+
// keeps it seamless regardless of the new holderWidth).
|
|
194
|
+
if (!this._initialized && this.opts.startProgress > 0) {
|
|
195
|
+
this.position.set(this.opts.startProgress * this.holderWidth)
|
|
196
|
+
}
|
|
197
|
+
this._initialized = true
|
|
198
|
+
this.render(this.position.get())
|
|
199
|
+
|
|
200
|
+
this.timeline = this.createCrawl()
|
|
114
201
|
this.timeline.pause()
|
|
202
|
+
}
|
|
115
203
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Create an infinite crawl animation from the current position.
|
|
206
|
+
* Animating exactly one holderWidth and relying on the modulo render keeps
|
|
207
|
+
* the loop reset (repeat) invisible. Always recreated from the live position
|
|
208
|
+
* so it stays correct after drags and throws.
|
|
209
|
+
*/
|
|
210
|
+
createCrawl() {
|
|
211
|
+
const from = this.position.get()
|
|
212
|
+
// Direction is baked into the target; speed stays positive so timeline.speed
|
|
213
|
+
// (hover slow-down, pause ramps, etc.) keeps working regardless of direction.
|
|
214
|
+
const target = this.opts.reversed ? from - this.holderWidth : from + this.holderWidth
|
|
215
|
+
return animate(this.position, target, {
|
|
216
|
+
duration: this.duration,
|
|
217
|
+
ease: 'linear',
|
|
218
|
+
repeat: Infinity
|
|
219
|
+
})
|
|
220
|
+
}
|
|
120
221
|
|
|
121
|
-
|
|
122
|
-
|
|
222
|
+
/**
|
|
223
|
+
* The speed the crawl should settle at. Half speed while the pointer rests
|
|
224
|
+
* on the marquee, so a crawl recreated after a drag or throw doesn't lose
|
|
225
|
+
* the hover slow-down.
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
targetSpeed() {
|
|
229
|
+
return this.opts.slowDownOnHover && this.hovering ? HOVER_SPEED : 1
|
|
123
230
|
}
|
|
124
231
|
|
|
125
232
|
play(rampUp = false) {
|
|
126
233
|
this.playing = true
|
|
127
234
|
if (this.speedAnimation) {
|
|
128
235
|
this.speedAnimation.stop()
|
|
236
|
+
this.speedAnimation = null
|
|
129
237
|
}
|
|
130
238
|
|
|
239
|
+
if (!this.holderWidth) return
|
|
240
|
+
|
|
241
|
+
// Recreate the crawl from the live position so it is always seamless,
|
|
242
|
+
// regardless of where a drag/throw left the marquee.
|
|
243
|
+
if (this.timeline) this.timeline.stop()
|
|
244
|
+
this.timeline = this.createCrawl()
|
|
245
|
+
|
|
131
246
|
if (rampUp) {
|
|
132
247
|
this.timeline.play()
|
|
133
|
-
const state = { speed:
|
|
248
|
+
const state = { speed: MIN_CRAWL_SPEED }
|
|
249
|
+
this.timeline.speed = MIN_CRAWL_SPEED
|
|
134
250
|
this.speedAnimation = animate(
|
|
135
251
|
state,
|
|
136
|
-
{ speed:
|
|
252
|
+
{ speed: this.targetSpeed() },
|
|
137
253
|
{
|
|
138
254
|
duration: 0.8,
|
|
139
255
|
ease: 'easeIn',
|
|
140
256
|
onUpdate: () => {
|
|
141
|
-
this.timeline.speed = state.speed
|
|
257
|
+
if (this.timeline) this.timeline.speed = state.speed
|
|
142
258
|
}
|
|
143
259
|
}
|
|
144
260
|
)
|
|
145
261
|
} else {
|
|
146
|
-
this.timeline.speed =
|
|
262
|
+
this.timeline.speed = this.targetSpeed()
|
|
147
263
|
this.timeline.play()
|
|
148
264
|
}
|
|
149
265
|
}
|
|
150
266
|
|
|
151
267
|
pause() {
|
|
152
268
|
this.playing = false
|
|
269
|
+
if (!this.timeline) return
|
|
153
270
|
const state = { speed: this.timeline.speed || 1 }
|
|
154
271
|
this.speedAnimation = animate(
|
|
155
272
|
state,
|
|
@@ -157,41 +274,48 @@ export default class Marquee {
|
|
|
157
274
|
{
|
|
158
275
|
duration: 0.8,
|
|
159
276
|
onUpdate: () => {
|
|
160
|
-
this.timeline.speed = state.speed
|
|
277
|
+
if (this.timeline) this.timeline.speed = state.speed
|
|
161
278
|
}
|
|
162
279
|
}
|
|
163
280
|
)
|
|
164
|
-
this.speedAnimation.finished
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
281
|
+
this.speedAnimation.finished
|
|
282
|
+
.then(() => {
|
|
283
|
+
if (!this.playing && this.timeline) {
|
|
284
|
+
this.timeline.pause()
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
// The ramp is routinely stopped by slowDown/play/resumeCrawl — that
|
|
288
|
+
// rejects `finished`, and an unhandled rejection is not our problem.
|
|
289
|
+
.catch(() => {})
|
|
170
290
|
}
|
|
171
291
|
|
|
172
292
|
slowDown() {
|
|
293
|
+
this.hovering = true
|
|
173
294
|
if (this.speedAnimation) {
|
|
174
295
|
this.speedAnimation.stop()
|
|
175
296
|
}
|
|
297
|
+
if (!this.timeline) return
|
|
176
298
|
const state = { speed: this.timeline.speed || 1 }
|
|
177
299
|
this.speedAnimation = animate(
|
|
178
300
|
state,
|
|
179
|
-
{ speed:
|
|
301
|
+
{ speed: HOVER_SPEED },
|
|
180
302
|
{
|
|
181
303
|
duration: 0.3,
|
|
182
304
|
ease: [0.4, 0, 0.2, 1], // ease-out
|
|
183
305
|
onUpdate: () => {
|
|
184
|
-
this.timeline.speed = state.speed
|
|
306
|
+
if (this.timeline) this.timeline.speed = state.speed
|
|
185
307
|
}
|
|
186
308
|
}
|
|
187
309
|
)
|
|
188
310
|
}
|
|
189
311
|
|
|
190
312
|
speedUp() {
|
|
313
|
+
this.hovering = false
|
|
191
314
|
if (this.speedAnimation) {
|
|
192
315
|
this.speedAnimation.stop()
|
|
193
316
|
}
|
|
194
|
-
|
|
317
|
+
if (!this.timeline) return
|
|
318
|
+
const state = { speed: this.timeline.speed || HOVER_SPEED }
|
|
195
319
|
this.speedAnimation = animate(
|
|
196
320
|
state,
|
|
197
321
|
{ speed: 1 },
|
|
@@ -199,7 +323,241 @@ export default class Marquee {
|
|
|
199
323
|
duration: 0.3,
|
|
200
324
|
ease: [0.4, 0, 0.2, 1], // ease-out
|
|
201
325
|
onUpdate: () => {
|
|
202
|
-
this.timeline.speed = state.speed
|
|
326
|
+
if (this.timeline) this.timeline.speed = state.speed
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Setup pointer-based drag + throw interaction.
|
|
334
|
+
* Mirrors the Looper module's physics (velocity sampling + Motion inertia).
|
|
335
|
+
*/
|
|
336
|
+
setupDrag() {
|
|
337
|
+
const $el = this.elements.$el
|
|
338
|
+
|
|
339
|
+
let startX = 0
|
|
340
|
+
let startY = 0
|
|
341
|
+
let startPosition = 0
|
|
342
|
+
let velocityTracker = []
|
|
343
|
+
let isDragging = false
|
|
344
|
+
let hasDragged = false
|
|
345
|
+
let axisDecided = false
|
|
346
|
+
let activeMinimumMovement = this.opts.minimumMovement
|
|
347
|
+
let dragDir = 1 // -1 when the marquee is horizontally mirrored (scaleX(-1))
|
|
348
|
+
|
|
349
|
+
const swallowClick = e => {
|
|
350
|
+
e.preventDefault()
|
|
351
|
+
e.stopPropagation()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const getVelocity = () => {
|
|
355
|
+
if (velocityTracker.length < 2) return 0
|
|
356
|
+
const recent = velocityTracker.slice(-6)
|
|
357
|
+
let totalVelocity = 0
|
|
358
|
+
let totalWeight = 0
|
|
359
|
+
for (let i = 1; i < recent.length; i++) {
|
|
360
|
+
const prev = recent[i - 1]
|
|
361
|
+
const curr = recent[i]
|
|
362
|
+
const deltaX = curr.x - prev.x
|
|
363
|
+
const deltaTime = curr.time - prev.time
|
|
364
|
+
if (deltaTime > 0) {
|
|
365
|
+
const weight = i / recent.length
|
|
366
|
+
const velocity = (deltaX / deltaTime) * 1000 // px/second
|
|
367
|
+
totalVelocity += velocity * weight
|
|
368
|
+
totalWeight += weight
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return totalWeight > 0 ? totalVelocity / totalWeight : 0
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const onPointerDown = e => {
|
|
375
|
+
if (e.button !== undefined && e.button !== 0) return
|
|
376
|
+
|
|
377
|
+
isDragging = true
|
|
378
|
+
startX = e.clientX
|
|
379
|
+
startY = e.clientY
|
|
380
|
+
startPosition = this.position.get()
|
|
381
|
+
velocityTracker = [{ x: e.clientX, time: e.timeStamp }]
|
|
382
|
+
hasDragged = false
|
|
383
|
+
axisDecided = false
|
|
384
|
+
activeMinimumMovement =
|
|
385
|
+
e.pointerType === 'touch' ? this.opts.touchMinimumMovement : this.opts.minimumMovement
|
|
386
|
+
|
|
387
|
+
// A reversed row is mirrored with scaleX(-1), so screen-space pointer
|
|
388
|
+
// movement maps to the opposite direction in the marquee's own space.
|
|
389
|
+
dragDir = this.isAxisFlipped() ? -1 : 1
|
|
390
|
+
|
|
391
|
+
// Stop crawl + any ongoing throw / speed ramp
|
|
392
|
+
if (this.speedAnimation) {
|
|
393
|
+
this.speedAnimation.stop()
|
|
394
|
+
this.speedAnimation = null
|
|
395
|
+
}
|
|
396
|
+
if (this.inertiaAnimation) {
|
|
397
|
+
this.inertiaAnimation.stop()
|
|
398
|
+
this.inertiaAnimation = null
|
|
399
|
+
}
|
|
400
|
+
if (this.timeline) {
|
|
401
|
+
this.timeline.stop()
|
|
402
|
+
this.timeline = null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (e.pointerType === 'mouse') {
|
|
406
|
+
e.preventDefault()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
window.addEventListener('pointermove', onPointerMove, { passive: false })
|
|
410
|
+
window.addEventListener('pointerup', onPointerUp)
|
|
411
|
+
window.addEventListener('pointercancel', onPointerUp)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const onPointerMove = e => {
|
|
415
|
+
if (!isDragging) return
|
|
416
|
+
|
|
417
|
+
const currentX = e.clientX
|
|
418
|
+
const currentTime = e.timeStamp
|
|
419
|
+
|
|
420
|
+
if (!axisDecided) {
|
|
421
|
+
const dX = Math.abs(currentX - startX)
|
|
422
|
+
const dY = Math.abs(e.clientY - startY)
|
|
423
|
+
if (Math.max(dX, dY) < activeMinimumMovement) return
|
|
424
|
+
axisDecided = true
|
|
425
|
+
|
|
426
|
+
// Clearly vertical → abort, let the page scroll
|
|
427
|
+
if (dY > dX * 1.2) {
|
|
428
|
+
isDragging = false
|
|
429
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
430
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
431
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
432
|
+
if (this.playing) this.resumeCrawl()
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
hasDragged = true
|
|
437
|
+
$el.style.cursor = 'grabbing'
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
e.preventDefault()
|
|
441
|
+
|
|
442
|
+
velocityTracker.push({ x: currentX, time: currentTime })
|
|
443
|
+
while (
|
|
444
|
+
velocityTracker.length > 0 &&
|
|
445
|
+
currentTime - velocityTracker[0].time > VELOCITY_WINDOW_MS
|
|
446
|
+
) {
|
|
447
|
+
velocityTracker.shift()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Drag left (cursor decreases) scrolls content left (position increases).
|
|
451
|
+
// dragDir flips this for mirrored (reversed) rows.
|
|
452
|
+
const deltaX = (startX - currentX) * dragDir
|
|
453
|
+
this.position.set(startPosition + deltaX)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const onPointerUp = e => {
|
|
457
|
+
if (!isDragging) return
|
|
458
|
+
isDragging = false
|
|
459
|
+
|
|
460
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
461
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
462
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
463
|
+
|
|
464
|
+
if (hasDragged) {
|
|
465
|
+
$el.style.cursor = 'grab'
|
|
466
|
+
// Swallow the click the browser fires after pointerup so item links
|
|
467
|
+
// don't navigate at the end of a drag.
|
|
468
|
+
$el.addEventListener('click', swallowClick, { capture: true, once: true })
|
|
469
|
+
} else {
|
|
470
|
+
// A tap that didn't move — just resume crawling.
|
|
471
|
+
if (this.playing) this.resumeCrawl()
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let velocity = getVelocity()
|
|
476
|
+
const lastTrackedX =
|
|
477
|
+
velocityTracker.length > 0 ? velocityTracker[velocityTracker.length - 1].x : e.clientX
|
|
478
|
+
const overallDelta = lastTrackedX - startX
|
|
479
|
+
|
|
480
|
+
// Velocity direction must match the overall drag direction
|
|
481
|
+
if (overallDelta > 0 && velocity < 0) velocity = 0
|
|
482
|
+
if (overallDelta < 0 && velocity > 0) velocity = 0
|
|
483
|
+
|
|
484
|
+
if (Math.abs(velocity) > 1) {
|
|
485
|
+
// Flip the throw velocity into the marquee's own space for mirrored rows
|
|
486
|
+
this.startInertia(velocity * dragDir)
|
|
487
|
+
} else if (this.playing) {
|
|
488
|
+
this.resumeCrawl()
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
$el.style.touchAction = 'pan-y'
|
|
493
|
+
$el.style.cursor = 'grab'
|
|
494
|
+
$el.addEventListener('pointerdown', onPointerDown)
|
|
495
|
+
|
|
496
|
+
this._dragCleanup = () => {
|
|
497
|
+
$el.removeEventListener('pointerdown', onPointerDown)
|
|
498
|
+
$el.removeEventListener('click', swallowClick, { capture: true })
|
|
499
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
500
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
501
|
+
window.removeEventListener('pointercancel', onPointerUp)
|
|
502
|
+
$el.style.touchAction = ''
|
|
503
|
+
$el.style.cursor = ''
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Throw the marquee with momentum after a drag release.
|
|
509
|
+
* @param {number} velocity - Cursor velocity in px/second
|
|
510
|
+
*/
|
|
511
|
+
startInertia(velocity) {
|
|
512
|
+
const currentPos = this.position.get()
|
|
513
|
+
// Cursor and content velocity are opposite: drag left → position increases
|
|
514
|
+
const motionVelocity = -velocity * this.opts.throwVelocityMultiplier
|
|
515
|
+
const estimatedDistance = this.opts.throwPower * motionVelocity
|
|
516
|
+
const targetPos = currentPos + estimatedDistance
|
|
517
|
+
|
|
518
|
+
this.inertiaAnimation = animate(this.position, targetPos, {
|
|
519
|
+
type: 'inertia',
|
|
520
|
+
velocity: motionVelocity,
|
|
521
|
+
power: this.opts.throwPower,
|
|
522
|
+
timeConstant: this.opts.throwResistance,
|
|
523
|
+
restSpeed: 10,
|
|
524
|
+
restDelta: 0.5
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
this.inertiaAnimation.finished
|
|
528
|
+
.then(() => {
|
|
529
|
+
this.inertiaAnimation = null
|
|
530
|
+
if (this.playing) this.resumeCrawl()
|
|
531
|
+
})
|
|
532
|
+
.catch(() => {
|
|
533
|
+
this.inertiaAnimation = null
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Resume the auto-crawl from the current position, ramping back up to full
|
|
539
|
+
* speed so it eases out of a throw rather than snapping.
|
|
540
|
+
*/
|
|
541
|
+
resumeCrawl() {
|
|
542
|
+
if (this.speedAnimation) {
|
|
543
|
+
this.speedAnimation.stop()
|
|
544
|
+
this.speedAnimation = null
|
|
545
|
+
}
|
|
546
|
+
if (this.timeline) this.timeline.stop()
|
|
547
|
+
|
|
548
|
+
this.timeline = this.createCrawl()
|
|
549
|
+
this.timeline.play()
|
|
550
|
+
|
|
551
|
+
const state = { speed: MIN_CRAWL_SPEED }
|
|
552
|
+
this.timeline.speed = MIN_CRAWL_SPEED
|
|
553
|
+
this.speedAnimation = animate(
|
|
554
|
+
state,
|
|
555
|
+
{ speed: this.targetSpeed() },
|
|
556
|
+
{
|
|
557
|
+
duration: SPEED_RAMP_DURATION,
|
|
558
|
+
ease: 'easeIn',
|
|
559
|
+
onUpdate: () => {
|
|
560
|
+
if (this.timeline) this.timeline.speed = state.speed
|
|
203
561
|
}
|
|
204
562
|
}
|
|
205
563
|
)
|
|
@@ -255,7 +613,17 @@ export default class Marquee {
|
|
|
255
613
|
}
|
|
256
614
|
}
|
|
257
615
|
|
|
258
|
-
|
|
616
|
+
// Duplicate holders until they cover the viewport plus a full holder
|
|
617
|
+
// width — guarantees seamless coverage across the whole modulo cycle.
|
|
618
|
+
const holderWidth = this.elements.$holder.offsetWidth
|
|
619
|
+
const required = this.app.size.width + holderWidth
|
|
620
|
+
let totalWidth = holderWidth
|
|
621
|
+
let guard = 0
|
|
622
|
+
while (totalWidth < required && guard < 10) {
|
|
623
|
+
this.elements.$marquee.appendChild(this.elements.$holder.cloneNode(true))
|
|
624
|
+
totalWidth += holderWidth
|
|
625
|
+
guard += 1
|
|
626
|
+
}
|
|
259
627
|
} else {
|
|
260
628
|
console.error(
|
|
261
629
|
'no textWidth! probably image? Set width to elements inside holder',
|
|
@@ -271,4 +639,29 @@ export default class Marquee {
|
|
|
271
639
|
set(this.elements.$el, { height })
|
|
272
640
|
set(this.elements.$marquee, { height })
|
|
273
641
|
}
|
|
642
|
+
|
|
643
|
+
destroy() {
|
|
644
|
+
this.killTweens()
|
|
645
|
+
|
|
646
|
+
if (this.observer) {
|
|
647
|
+
this.observer.disconnect()
|
|
648
|
+
this.observer = null
|
|
649
|
+
}
|
|
650
|
+
if (this._positionUnsubscribe) {
|
|
651
|
+
this._positionUnsubscribe()
|
|
652
|
+
this._positionUnsubscribe = null
|
|
653
|
+
}
|
|
654
|
+
if (this._dragCleanup) {
|
|
655
|
+
this._dragCleanup()
|
|
656
|
+
this._dragCleanup = null
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (this.opts.slowDownOnHover) {
|
|
660
|
+
this.elements.$el.removeEventListener('mouseenter', this._onMouseEnter)
|
|
661
|
+
this.elements.$el.removeEventListener('mouseleave', this._onMouseLeave)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
window.removeEventListener('APPLICATION:RESIZE', this._onResize)
|
|
665
|
+
window.removeEventListener('APPLICATION:REVEALED', this._onReveal)
|
|
666
|
+
}
|
|
274
667
|
}
|
|
@@ -60,9 +60,20 @@ export function set(target, values) {
|
|
|
60
60
|
element.style.transform = transformProps.join(' ')
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Properties that are unitless (don't need 'px')
|
|
64
|
+
const unitless = new Set([
|
|
65
|
+
'opacity', 'zIndex', 'fontWeight', 'lineHeight', 'orphans',
|
|
66
|
+
'widows', 'order', 'flexGrow', 'flexShrink', 'columnCount',
|
|
67
|
+
'fillOpacity', 'strokeOpacity', 'tabSize',
|
|
68
|
+
])
|
|
69
|
+
|
|
63
70
|
// Apply other styles
|
|
64
71
|
Object.entries(styleProps).forEach(([key, value]) => {
|
|
65
|
-
|
|
72
|
+
if (typeof value === 'number' && !unitless.has(key)) {
|
|
73
|
+
element.style[key] = value + 'px'
|
|
74
|
+
} else {
|
|
75
|
+
element.style[key] = value
|
|
76
|
+
}
|
|
66
77
|
})
|
|
67
78
|
})
|
|
68
79
|
}
|
|
@@ -46,6 +46,12 @@ export default class DoubleHeader {
|
|
|
46
46
|
pin(): void;
|
|
47
47
|
notSmall(): void;
|
|
48
48
|
small(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Update the --header-height CSS variable on :root.
|
|
51
|
+
* Uses el height when pinned (el is the main header, auxEl is secondary).
|
|
52
|
+
* Set to 0px when unpinned.
|
|
53
|
+
*/
|
|
54
|
+
_updateHeaderHeight(): void;
|
|
49
55
|
shouldUnpin(toleranceExceeded: any): any;
|
|
50
56
|
shouldPin(toleranceExceeded: any): any;
|
|
51
57
|
isOutOfBounds(): boolean;
|
|
@@ -108,6 +108,11 @@ export default class FixedHeader {
|
|
|
108
108
|
pin(): void;
|
|
109
109
|
notSmall(): void;
|
|
110
110
|
small(): void;
|
|
111
|
+
/**
|
|
112
|
+
* Update the --header-height CSS variable on :root.
|
|
113
|
+
* Set to the header's current height when pinned, 0px when unpinned.
|
|
114
|
+
*/
|
|
115
|
+
_updateHeaderHeight(): void;
|
|
111
116
|
notAltBg(): void;
|
|
112
117
|
altBg(): void;
|
|
113
118
|
shouldUnpin(toleranceExceeded: any): any;
|
|
@@ -3,23 +3,79 @@ export default class Marquee {
|
|
|
3
3
|
opts: any;
|
|
4
4
|
app: any;
|
|
5
5
|
elements: {};
|
|
6
|
+
position: any;
|
|
7
|
+
holderWidth: number;
|
|
8
|
+
duration: number;
|
|
6
9
|
timeline: any;
|
|
10
|
+
speedAnimation: any;
|
|
11
|
+
inertiaAnimation: any;
|
|
7
12
|
observer: IntersectionObserver;
|
|
13
|
+
playing: boolean;
|
|
14
|
+
hovering: boolean;
|
|
15
|
+
_onResize: any;
|
|
16
|
+
_onReveal: any;
|
|
17
|
+
_onMouseEnter: any;
|
|
18
|
+
_onMouseLeave: any;
|
|
19
|
+
_positionUnsubscribe: any;
|
|
20
|
+
_dragCleanup: () => void;
|
|
8
21
|
initialize(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Apply the current raw position to the marquee element.
|
|
24
|
+
* Holders are laid out side by side at i * holderWidth, so translating by
|
|
25
|
+
* the position modulo holderWidth produces a seamless infinite scroll.
|
|
26
|
+
*/
|
|
27
|
+
render(rawPos: any): void;
|
|
28
|
+
/**
|
|
29
|
+
* Detect whether the marquee element is horizontally mirrored (scaleX(-1)),
|
|
30
|
+
* as used for "reverse" rows. Read from the live computed transform so it
|
|
31
|
+
* works no matter how the flip is applied (and survives responsive changes).
|
|
32
|
+
*/
|
|
33
|
+
isAxisFlipped(): boolean;
|
|
9
34
|
revealMarquee(e: any): void;
|
|
10
35
|
updateMarquee(e: any): void;
|
|
11
|
-
duration: number;
|
|
12
36
|
clearHolders(): void;
|
|
13
37
|
killTweens(): void;
|
|
14
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Lay out holders and create the (paused) crawl animation.
|
|
40
|
+
*/
|
|
15
41
|
initializeTween(): void;
|
|
42
|
+
_initialized: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Create an infinite crawl animation from the current position.
|
|
45
|
+
* Animating exactly one holderWidth and relying on the modulo render keeps
|
|
46
|
+
* the loop reset (repeat) invisible. Always recreated from the live position
|
|
47
|
+
* so it stays correct after drags and throws.
|
|
48
|
+
*/
|
|
49
|
+
createCrawl(): any;
|
|
50
|
+
/**
|
|
51
|
+
* The speed the crawl should settle at. Half speed while the pointer rests
|
|
52
|
+
* on the marquee, so a crawl recreated after a drag or throw doesn't lose
|
|
53
|
+
* the hover slow-down.
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
targetSpeed(): number;
|
|
16
57
|
play(rampUp?: boolean): void;
|
|
17
|
-
playing: boolean;
|
|
18
58
|
pause(): void;
|
|
19
59
|
slowDown(): void;
|
|
20
60
|
speedUp(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Setup pointer-based drag + throw interaction.
|
|
63
|
+
* Mirrors the Looper module's physics (velocity sampling + Motion inertia).
|
|
64
|
+
*/
|
|
65
|
+
setupDrag(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Throw the marquee with momentum after a drag release.
|
|
68
|
+
* @param {number} velocity - Cursor velocity in px/second
|
|
69
|
+
*/
|
|
70
|
+
startInertia(velocity: number): void;
|
|
71
|
+
/**
|
|
72
|
+
* Resume the auto-crawl from the current position, ramping back up to full
|
|
73
|
+
* speed so it eases out of a throw rather than snapping.
|
|
74
|
+
*/
|
|
75
|
+
resumeCrawl(): void;
|
|
21
76
|
setupObserver(): void;
|
|
22
77
|
fillText(): void;
|
|
23
78
|
measuredHeight: any;
|
|
24
79
|
setHeight(): void;
|
|
80
|
+
destroy(): void;
|
|
25
81
|
}
|
|
@@ -57,6 +57,11 @@ export default class StickyHeader {
|
|
|
57
57
|
pin(): void;
|
|
58
58
|
notSmall(): void;
|
|
59
59
|
small(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Update the --header-height CSS variable on :root.
|
|
62
|
+
* Set to the header's current height when pinned, 0px when unpinned.
|
|
63
|
+
*/
|
|
64
|
+
_updateHeaderHeight(): void;
|
|
60
65
|
notAltBg(): void;
|
|
61
66
|
altBg(): void;
|
|
62
67
|
shouldUnpin(toleranceExceeded: any): any;
|