@brandocms/jupiter 5.0.0-beta.15 → 5.0.0-beta.17
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/README.md +18 -0
- package/package.json +1 -1
- package/src/modules/FixedHeader/index.js +19 -2
- package/src/modules/Looper/index.js +40 -11
- 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 +27 -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/README.md
CHANGED
|
@@ -994,6 +994,8 @@ Same options as FixedHeader - see FixedHeader documentation below.
|
|
|
994
994
|
- `sections`
|
|
995
995
|
- Here you can set a config per section (body[data-script="section"])
|
|
996
996
|
- `unPinOnResize` - auto unpin while resizing
|
|
997
|
+
- `headerHeightTracksPin` - default `true`. Whether `--header-height` drops to `0px`
|
|
998
|
+
while the header is unpinned. Set `false` for a header that never retracts.
|
|
997
999
|
- `offset` - when is header triggered
|
|
998
1000
|
- `offsetBg` - when is offset background triggered (i.e. if there's another bg for content)
|
|
999
1001
|
- events are same as under default.
|
|
@@ -1034,6 +1036,22 @@ Same options as FixedHeader - see FixedHeader documentation below.
|
|
|
1034
1036
|
- `onMobileMenuClose`
|
|
1035
1037
|
- Triggers when mobile menu closes
|
|
1036
1038
|
|
|
1039
|
+
### CSS variables
|
|
1040
|
+
|
|
1041
|
+
`--header-height` is kept up to date on `:root`, republished whenever the header pins,
|
|
1042
|
+
unpins or toggles small/big. By default it is how much header is *visible* — the
|
|
1043
|
+
measured height while pinned, `0px` while unpinned — so anything positioned under the
|
|
1044
|
+
bar follows it out of the way as it retracts.
|
|
1045
|
+
|
|
1046
|
+
For a header that never retracts — through `preventUnpin`, or by no-opping
|
|
1047
|
+
`onPin`/`onUnpin` — set `headerHeightTracksPin: false`. The bar stays put, but the
|
|
1048
|
+
pinned state still flips on every change of scroll direction, so the variable would
|
|
1049
|
+
otherwise drop to `0px` and back while nothing moves. Any layout sized from it (a
|
|
1050
|
+
`padding-top` standing in for the fixed bar, say) then grows and shrinks the document
|
|
1051
|
+
under the reader. Scroll anchoring absorbs that mid-page, but not at the very bottom,
|
|
1052
|
+
where the scroll offset is clamped to the document and the page visibly jumps.
|
|
1053
|
+
`headerHeightTracksPin: false` publishes the measured height throughout.
|
|
1054
|
+
|
|
1037
1055
|
```
|
|
1038
1056
|
header[data-nav] {
|
|
1039
1057
|
@include container();
|
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ import { set } from '../../utils/motion-helpers'
|
|
|
50
50
|
/**
|
|
51
51
|
* @typedef {Object} FixedHeaderSectionOptions
|
|
52
52
|
* @property {boolean} [unPinOnResize=true] - Whether to unpin header on window resize
|
|
53
|
+
* @property {boolean} [headerHeightTracksPin=true] - Whether --header-height drops to 0 when the header unpins. Set false for a header that never retracts.
|
|
53
54
|
* @property {Window|HTMLElement} [canvas=window] - Scrolling element
|
|
54
55
|
* @property {string|null} [intersects=null] - Selector for elements to check intersection with
|
|
55
56
|
* @property {Function} [beforeEnter] - Called before header enters
|
|
@@ -153,6 +154,7 @@ const DEFAULT_OPTIONS = {
|
|
|
153
154
|
|
|
154
155
|
default: {
|
|
155
156
|
unPinOnResize: true,
|
|
157
|
+
headerHeightTracksPin: true,
|
|
156
158
|
canvas: window,
|
|
157
159
|
intersects: null,
|
|
158
160
|
beforeEnter: (h) => {
|
|
@@ -612,10 +614,25 @@ export default class FixedHeader {
|
|
|
612
614
|
|
|
613
615
|
/**
|
|
614
616
|
* Update the --header-height CSS variable on :root.
|
|
615
|
-
*
|
|
617
|
+
*
|
|
618
|
+
* By default this is how much header is *visible* — the measured height when
|
|
619
|
+
* pinned, 0 when unpinned — so anything positioned under the bar follows it
|
|
620
|
+
* out of the way as it retracts.
|
|
621
|
+
*
|
|
622
|
+
* That is wrong for a header configured never to retract, whether through
|
|
623
|
+
* `preventUnpin` or by no-opping `onPin` / `onUnpin`. The bar stays put, but
|
|
624
|
+
* `_pinned` still flips on every change of scroll direction, so the variable
|
|
625
|
+
* drops to 0 and back while nothing moves. Any layout sized from it — a
|
|
626
|
+
* `padding-top` standing in for the fixed bar, say — then grows and shrinks
|
|
627
|
+
* the document under the reader. Scroll anchoring absorbs that mid-page, but
|
|
628
|
+
* not at the very bottom, where the scroll offset is clamped to the document
|
|
629
|
+
* and the page visibly jumps instead.
|
|
630
|
+
*
|
|
631
|
+
* `headerHeightTracksPin: false` publishes the measured height throughout.
|
|
616
632
|
*/
|
|
617
633
|
_updateHeaderHeight() {
|
|
618
|
-
const
|
|
634
|
+
const tracksPin = this.opts.headerHeightTracksPin !== false
|
|
635
|
+
const height = tracksPin && !this._pinned ? '0px' : `${this.el.clientHeight}px`
|
|
619
636
|
document.documentElement.style.setProperty('--header-height', height)
|
|
620
637
|
}
|
|
621
638
|
|
|
@@ -147,6 +147,23 @@ function horizontalLoop(app, items, config) {
|
|
|
147
147
|
// Cache track element reference (M3)
|
|
148
148
|
const trackElement = items[0].parentElement
|
|
149
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
|
+
|
|
150
167
|
/**
|
|
151
168
|
* Measure total width of all items as currently laid out
|
|
152
169
|
* @returns {number} Total width in pixels
|
|
@@ -877,6 +894,18 @@ function horizontalLoop(app, items, config) {
|
|
|
877
894
|
// Only handle primary pointer (left click, first touch)
|
|
878
895
|
if (e.button !== undefined && e.button !== 0) return
|
|
879
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
|
+
|
|
880
909
|
isDragging = true
|
|
881
910
|
indexSetByNav = false
|
|
882
911
|
startX = e.clientX
|
|
@@ -976,7 +1005,7 @@ function horizontalLoop(app, items, config) {
|
|
|
976
1005
|
|
|
977
1006
|
// Horizontal — commit to drag
|
|
978
1007
|
hasDragged = true
|
|
979
|
-
|
|
1008
|
+
dragSurface.style.cursor = 'grabbing'
|
|
980
1009
|
trackElement.classList.add('looper-dragging')
|
|
981
1010
|
}
|
|
982
1011
|
|
|
@@ -1022,12 +1051,12 @@ function horizontalLoop(app, items, config) {
|
|
|
1022
1051
|
|
|
1023
1052
|
// Reset cursor and re-enable hover effects (only if we actually dragged)
|
|
1024
1053
|
if (hasDragged) {
|
|
1025
|
-
|
|
1054
|
+
dragSurface.style.cursor = 'grab'
|
|
1026
1055
|
trackElement.classList.remove('looper-dragging')
|
|
1027
1056
|
|
|
1028
1057
|
// Swallow the next click event so links don't navigate after a drag.
|
|
1029
1058
|
// The browser fires click after pointerup — capture it before it reaches any <a>.
|
|
1030
|
-
|
|
1059
|
+
dragSurface.addEventListener('click', swallowClick, { capture: true, once: true })
|
|
1031
1060
|
}
|
|
1032
1061
|
|
|
1033
1062
|
// If this was a click (not a drag):
|
|
@@ -1351,17 +1380,17 @@ function horizontalLoop(app, items, config) {
|
|
|
1351
1380
|
}
|
|
1352
1381
|
|
|
1353
1382
|
// Set up touch-action CSS for proper touch handling
|
|
1354
|
-
|
|
1383
|
+
dragSurface.style.touchAction = 'pan-y' // Allow vertical scroll, prevent horizontal
|
|
1355
1384
|
|
|
1356
1385
|
// Add pointer down listener
|
|
1357
|
-
|
|
1358
|
-
|
|
1386
|
+
dragSurface.style.cursor = 'grab'
|
|
1387
|
+
dragSurface.addEventListener('pointerdown', onPointerDown)
|
|
1359
1388
|
|
|
1360
1389
|
// Store cleanup function
|
|
1361
1390
|
dragState = {
|
|
1362
1391
|
cleanup: () => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1392
|
+
dragSurface.removeEventListener('pointerdown', onPointerDown)
|
|
1393
|
+
dragSurface.removeEventListener('click', swallowClick, { capture: true })
|
|
1365
1394
|
window.removeEventListener('pointermove', onPointerMove)
|
|
1366
1395
|
window.removeEventListener('pointerup', onPointerUp)
|
|
1367
1396
|
window.removeEventListener('pointercancel', onPointerUp)
|
|
@@ -1748,9 +1777,9 @@ function horizontalLoop(app, items, config) {
|
|
|
1748
1777
|
trackElement.style.willChange = ''
|
|
1749
1778
|
trackElement.style.transform = ''
|
|
1750
1779
|
|
|
1751
|
-
// Clear inline styles on
|
|
1752
|
-
|
|
1753
|
-
|
|
1780
|
+
// Clear inline styles on the drag surface
|
|
1781
|
+
dragSurface.style.touchAction = ''
|
|
1782
|
+
dragSurface.style.cursor = ''
|
|
1754
1783
|
|
|
1755
1784
|
// Clear item wrap transforms
|
|
1756
1785
|
items.forEach(item => {
|
|
@@ -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;
|
|
@@ -15,6 +15,10 @@ export default class FixedHeader {
|
|
|
15
15
|
* - Whether to unpin header on window resize
|
|
16
16
|
*/
|
|
17
17
|
unPinOnResize?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* - Whether --header-height drops to 0 when the header unpins. Set false for a header that never retracts.
|
|
20
|
+
*/
|
|
21
|
+
headerHeightTracksPin?: boolean;
|
|
18
22
|
/**
|
|
19
23
|
* - Scrolling element
|
|
20
24
|
*/
|
|
@@ -108,6 +112,25 @@ export default class FixedHeader {
|
|
|
108
112
|
pin(): void;
|
|
109
113
|
notSmall(): void;
|
|
110
114
|
small(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Update the --header-height CSS variable on :root.
|
|
117
|
+
*
|
|
118
|
+
* By default this is how much header is *visible* — the measured height when
|
|
119
|
+
* pinned, 0 when unpinned — so anything positioned under the bar follows it
|
|
120
|
+
* out of the way as it retracts.
|
|
121
|
+
*
|
|
122
|
+
* That is wrong for a header configured never to retract, whether through
|
|
123
|
+
* `preventUnpin` or by no-opping `onPin` / `onUnpin`. The bar stays put, but
|
|
124
|
+
* `_pinned` still flips on every change of scroll direction, so the variable
|
|
125
|
+
* drops to 0 and back while nothing moves. Any layout sized from it — a
|
|
126
|
+
* `padding-top` standing in for the fixed bar, say — then grows and shrinks
|
|
127
|
+
* the document under the reader. Scroll anchoring absorbs that mid-page, but
|
|
128
|
+
* not at the very bottom, where the scroll offset is clamped to the document
|
|
129
|
+
* and the page visibly jumps instead.
|
|
130
|
+
*
|
|
131
|
+
* `headerHeightTracksPin: false` publishes the measured height throughout.
|
|
132
|
+
*/
|
|
133
|
+
_updateHeaderHeight(): void;
|
|
111
134
|
notAltBg(): void;
|
|
112
135
|
altBg(): void;
|
|
113
136
|
shouldUnpin(toleranceExceeded: any): any;
|
|
@@ -195,6 +218,10 @@ export type FixedHeaderSectionOptions = {
|
|
|
195
218
|
* - Whether to unpin header on window resize
|
|
196
219
|
*/
|
|
197
220
|
unPinOnResize?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* - Whether --header-height drops to 0 when the header unpins. Set false for a header that never retracts.
|
|
223
|
+
*/
|
|
224
|
+
headerHeightTracksPin?: boolean;
|
|
198
225
|
/**
|
|
199
226
|
* - Scrolling element
|
|
200
227
|
*/
|
|
@@ -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;
|