@linear_non/stellar-libs 1.3.1 → 1.4.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/README.md +145 -37
- package/package.json +1 -1
- package/src/Slider/index.js +375 -0
- package/src/Smooth/index.js +9 -8
- package/src/SpritePlayer/index.js +9 -4
- package/src/index.js +2 -1
package/README.md
CHANGED
|
@@ -1,72 +1,180 @@
|
|
|
1
1
|
# stellar-libs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> `@linear_non/stellar-libs` — Reusable UI behavior modules for Non-Linear Studio projects.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A collection of lightweight, self-contained frontend UI behavior modules built to work seamlessly with [`@linear_non/stellar-kit`](https://www.npmjs.com/package/@linear_non/stellar-kit). Each library provides specific functionality and can be dropped into modular websites with minimal configuration.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Sticky**: Basic sticky logic for DOM elements
|
|
9
|
-
- **Smooth**: Smooth scroll instance using Virtual Scroll
|
|
10
|
-
- **SplitOnScroll**: Text splitting + reveal synced to scroll
|
|
11
|
-
- **Noise**: Configurable canvas noise
|
|
12
|
-
- **SpritePlayer**: Canvas image sequence player
|
|
7
|
+
## Available Libraries
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
| Library | Description |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **Sticky** | Sticky scroll behavior with GSAP ScrollTrigger |
|
|
12
|
+
| **Smooth** | Smooth scroll system using VirtualScroll |
|
|
13
|
+
| **SplitOnScroll** | Text splitting with scroll-triggered reveals |
|
|
14
|
+
| **Noise** | Canvas-based animated noise effect |
|
|
15
|
+
| **SpritePlayer** | Image sequence player on canvas |
|
|
16
|
+
| **CursorTracker** | Custom cursor that follows mouse |
|
|
17
|
+
| **Marquee** | Infinite horizontal marquee scroller |
|
|
18
|
+
| **Slider** | Drag slider with optional infinite loop and progress |
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
15
21
|
|
|
16
22
|
```bash
|
|
17
23
|
npm install @linear_non/stellar-libs
|
|
24
|
+
npm install @linear_non/stellar-kit
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
## Local Development
|
|
21
28
|
|
|
22
29
|
```bash
|
|
23
|
-
npm install
|
|
30
|
+
npm install
|
|
31
|
+
npm run dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Launches a Vite dev server with a demo index linking to individual playgrounds for each library.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Sticky
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { Sticky } from "@linear_non/stellar-libs"
|
|
44
|
+
|
|
45
|
+
const sticky = new Sticky({
|
|
46
|
+
el: document.querySelector(".sticky-container"),
|
|
47
|
+
sticky: document.querySelector(".sticky-element"),
|
|
48
|
+
isSmooth: false,
|
|
49
|
+
onUpdateCallback: ({ progress }) => console.log(progress),
|
|
50
|
+
})
|
|
24
51
|
```
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
### Smooth
|
|
27
54
|
|
|
55
|
+
```js
|
|
56
|
+
import { Smooth } from "@linear_non/stellar-libs"
|
|
57
|
+
|
|
58
|
+
const smooth = new Smooth({ scroll: kitStore.scroll })
|
|
28
59
|
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
│ ├── SplitOnScroll/ # Scroll-triggered text reveals
|
|
33
|
-
│ ├── Smooth/ # Smooth scrolling system
|
|
34
|
-
│ └── ScrollBar/ # Custom scrollbar component
|
|
35
|
-
│ └── Noise/ # Canvas noise
|
|
36
|
-
│ └── SpritePlayer/ # Image sprite player
|
|
37
|
-
├── dev/ # Dev playgrounds for each lib
|
|
38
|
-
├── index.js # Entry point for exports
|
|
39
|
-
└── vite.config.js # Vite config to run individual demos
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<section data-smooth data-speed="0.8">Parallax content</section>
|
|
40
63
|
```
|
|
41
64
|
|
|
42
|
-
|
|
65
|
+
### SplitOnScroll
|
|
43
66
|
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
```js
|
|
68
|
+
import { SplitOnScroll } from "@linear_non/stellar-libs"
|
|
69
|
+
|
|
70
|
+
const split = new SplitOnScroll({
|
|
71
|
+
el: document.querySelector(".trigger"),
|
|
72
|
+
splitText: document.querySelectorAll("h1"),
|
|
73
|
+
isReady: ({ splits, groups }) => {
|
|
74
|
+
gsap.from(splits[0].chars, { y: 40, opacity: 0, stagger: 0.03 })
|
|
75
|
+
},
|
|
76
|
+
once: true,
|
|
77
|
+
})
|
|
47
78
|
```
|
|
48
79
|
|
|
49
|
-
>
|
|
80
|
+
> The constructor key is `splitText` (not `splitTargets`). The `isReady` callback receives `{ splits, groups }`.
|
|
50
81
|
|
|
51
|
-
|
|
82
|
+
### Noise
|
|
52
83
|
|
|
53
84
|
```js
|
|
54
|
-
import {
|
|
85
|
+
import { Noise } from "@linear_non/stellar-libs"
|
|
86
|
+
|
|
87
|
+
const noise = new Noise({
|
|
88
|
+
target: ".noise-canvas",
|
|
89
|
+
density: 0.75,
|
|
90
|
+
color: 0xf3f2feff,
|
|
91
|
+
opacity: 0.25,
|
|
92
|
+
fps: 24,
|
|
93
|
+
})
|
|
94
|
+
```
|
|
55
95
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
### SpritePlayer
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { SpritePlayer } from "@linear_non/stellar-libs"
|
|
100
|
+
|
|
101
|
+
const player = new SpritePlayer({
|
|
102
|
+
canvas: document.querySelector("canvas"),
|
|
103
|
+
container: document.querySelector(".player-container"),
|
|
104
|
+
desktop_path: "/sprites/desktop/",
|
|
105
|
+
mobile_path: "/sprites/mobile/",
|
|
106
|
+
fileName: "frame_{index}",
|
|
107
|
+
total: 60,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
await player.init()
|
|
111
|
+
player.setProgress(0.5)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### CursorTracker
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { CursorTracker } from "@linear_non/stellar-libs"
|
|
118
|
+
|
|
119
|
+
const tracker = new CursorTracker({
|
|
120
|
+
container: document.querySelector(".hover-area"),
|
|
121
|
+
el: document.querySelector(".cursor"),
|
|
122
|
+
ease: 0.12,
|
|
123
|
+
isCentered: true,
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> `CursorTracker` subscribes to `APP_TICK` internally — do not add an external tick subscription.
|
|
128
|
+
|
|
129
|
+
### Marquee
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { Marquee } from "@linear_non/stellar-libs"
|
|
133
|
+
|
|
134
|
+
const marquee = new Marquee({
|
|
135
|
+
container: document.querySelector(".marquee"),
|
|
136
|
+
el: document.querySelector(".marquee__track"),
|
|
137
|
+
speed: 50,
|
|
138
|
+
gap: "1.6rem",
|
|
59
139
|
})
|
|
60
140
|
```
|
|
61
141
|
|
|
62
|
-
|
|
142
|
+
### Slider
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { Slider } from "@linear_non/stellar-libs"
|
|
146
|
+
|
|
147
|
+
const slider = new Slider({
|
|
148
|
+
container: document.querySelector(".slider"),
|
|
149
|
+
el: document.querySelector(".slider__track"),
|
|
150
|
+
speed: 0.1,
|
|
151
|
+
snap: true,
|
|
152
|
+
infinite: false,
|
|
153
|
+
prevEl: document.querySelector(".prev"),
|
|
154
|
+
nextEl: document.querySelector(".next"),
|
|
155
|
+
progressEl: document.querySelector(".progress-fill"),
|
|
156
|
+
onSlideChange: ({ index, total }) => console.log(index, total),
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<div class="slider">
|
|
162
|
+
<div class="slider__track">
|
|
163
|
+
<div class="slide">01</div>
|
|
164
|
+
<div class="slide">02</div>
|
|
165
|
+
<div class="slide">03</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
63
169
|
|
|
64
170
|
---
|
|
65
171
|
|
|
66
|
-
|
|
172
|
+
## Lifecycle
|
|
173
|
+
|
|
174
|
+
All libraries follow a consistent class-based lifecycle: `init() → on() → tick() → resize() → off() → destroy()`
|
|
67
175
|
|
|
68
|
-
|
|
176
|
+
Always call `destroy()` on page navigation to clean up event listeners, animations, and timers.
|
|
69
177
|
|
|
70
178
|
---
|
|
71
179
|
|
|
72
|
-
Made with
|
|
180
|
+
Made with love by [Non-Linear Studio](https://non-linear.studio)
|
package/package.json
CHANGED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { lerp, bounds, sniffer } from "@linear_non/stellar-kit/utils"
|
|
2
|
+
import { gsap } from "@linear_non/stellar-kit/gsap"
|
|
3
|
+
import { emitter, EVENTS } from "@linear_non/stellar-kit/events"
|
|
4
|
+
|
|
5
|
+
export default class Slider {
|
|
6
|
+
constructor({
|
|
7
|
+
container,
|
|
8
|
+
el,
|
|
9
|
+
speed = 0.1,
|
|
10
|
+
snap = true,
|
|
11
|
+
infinite = false,
|
|
12
|
+
mobileOnly = false,
|
|
13
|
+
prevEl = null,
|
|
14
|
+
nextEl = null,
|
|
15
|
+
progressEl = null,
|
|
16
|
+
onSlideChange = null,
|
|
17
|
+
}) {
|
|
18
|
+
this.container = container
|
|
19
|
+
this.el = el
|
|
20
|
+
this.speed = speed
|
|
21
|
+
this.snap = infinite ? true : snap // infinite always snaps
|
|
22
|
+
this.infinite = infinite
|
|
23
|
+
this.mobileOnly = mobileOnly
|
|
24
|
+
this.prevEl = prevEl
|
|
25
|
+
this.nextEl = nextEl
|
|
26
|
+
this.progressEl = progressEl
|
|
27
|
+
this.onSlideChange = typeof onSlideChange === "function" ? onSlideChange : null
|
|
28
|
+
|
|
29
|
+
this.isActive = false
|
|
30
|
+
this.isDown = false
|
|
31
|
+
this.isDragging = false
|
|
32
|
+
|
|
33
|
+
this.x = { start: 0, state: 0 }
|
|
34
|
+
this.current = 0
|
|
35
|
+
this.target = 0
|
|
36
|
+
this.index = 0 // 0-based real index (infinite: derived from position)
|
|
37
|
+
this.total = 0
|
|
38
|
+
this.slideWidth = 0
|
|
39
|
+
this.setW = 0 // total width of one full set of real slides (infinite mode)
|
|
40
|
+
this.limit = 0
|
|
41
|
+
|
|
42
|
+
this.dpr = Math.max(1, Math.round(window.devicePixelRatio || 1))
|
|
43
|
+
this._snap2px = v => Math.round(v * this.dpr) / this.dpr
|
|
44
|
+
|
|
45
|
+
this.init()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Setup ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
_shouldActivate() {
|
|
51
|
+
return !this.mobileOnly || sniffer.isDevice
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setup() {
|
|
55
|
+
this.isActive = this._shouldActivate()
|
|
56
|
+
if (!this.isActive) return
|
|
57
|
+
|
|
58
|
+
if (this.infinite) {
|
|
59
|
+
this._setupInfinite()
|
|
60
|
+
} else {
|
|
61
|
+
this._measure()
|
|
62
|
+
this.index = gsap.utils.clamp(0, this.total - 1, this.index)
|
|
63
|
+
this.target = this.index * this.slideWidth
|
|
64
|
+
this.current = this.target
|
|
65
|
+
this._draw(this.current)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_measure() {
|
|
70
|
+
// Only measure non-clone slides for width/total
|
|
71
|
+
const rawSlides = Array.from(this.el.children).filter(s => !s.dataset.clone)
|
|
72
|
+
this.total = rawSlides.length
|
|
73
|
+
if (!this.total) return
|
|
74
|
+
|
|
75
|
+
const gapPx = parseFloat(getComputedStyle(this.el).gap) || 0
|
|
76
|
+
this.slideWidth = rawSlides[0].getBoundingClientRect().width + gapPx
|
|
77
|
+
this.setW = this.slideWidth * this.total
|
|
78
|
+
const viewWidth = bounds(this.container).width
|
|
79
|
+
this.limit = Math.max(0, this.setW - viewWidth)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_setupInfinite() {
|
|
83
|
+
// Remove any stale clones from previous setup
|
|
84
|
+
Array.from(this.el.querySelectorAll("[data-clone]")).forEach(c => c.remove())
|
|
85
|
+
|
|
86
|
+
const rawSlides = Array.from(this.el.children)
|
|
87
|
+
if (rawSlides.length < 2) return
|
|
88
|
+
|
|
89
|
+
// Clone the ENTIRE set before and after (tripling) so the user can never
|
|
90
|
+
// reach a real edge regardless of drag speed or prev/next calls.
|
|
91
|
+
const cloneBefore = rawSlides.map(s => {
|
|
92
|
+
const c = s.cloneNode(true)
|
|
93
|
+
c.setAttribute("aria-hidden", "true")
|
|
94
|
+
c.dataset.clone = "before"
|
|
95
|
+
return c
|
|
96
|
+
})
|
|
97
|
+
const cloneAfter = rawSlides.map(s => {
|
|
98
|
+
const c = s.cloneNode(true)
|
|
99
|
+
c.setAttribute("aria-hidden", "true")
|
|
100
|
+
c.dataset.clone = "after"
|
|
101
|
+
return c
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Prepend "before" in correct visual order, append "after"
|
|
105
|
+
;[...cloneBefore].reverse().forEach(c => this.el.prepend(c))
|
|
106
|
+
cloneAfter.forEach(c => this.el.appendChild(c))
|
|
107
|
+
|
|
108
|
+
this._measure()
|
|
109
|
+
|
|
110
|
+
// x convention: translateX(-x). Start at setW so real slide 0 is visible.
|
|
111
|
+
// Track layout: [before clones 0..setW) [real slides setW..2setW) [after clones 2setW..3setW)
|
|
112
|
+
this.index = 0
|
|
113
|
+
this.current = this.setW
|
|
114
|
+
this.target = this.setW
|
|
115
|
+
this._draw(this.current)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Infinite wrap (runs every tick) ──────────────────────────────────────
|
|
119
|
+
// Silently teleport when position drifts outside the real-set zone.
|
|
120
|
+
// Because we cloned full sets on both sides this jump is always invisible.
|
|
121
|
+
|
|
122
|
+
_wrapInfinite() {
|
|
123
|
+
if (!this.setW) return
|
|
124
|
+
let shift = 0
|
|
125
|
+
if (this.current < 0) {
|
|
126
|
+
shift = Math.ceil(-this.current / this.setW) * this.setW
|
|
127
|
+
} else if (this.current >= 2 * this.setW) {
|
|
128
|
+
shift = -Math.floor((this.current - this.setW) / this.setW) * this.setW
|
|
129
|
+
}
|
|
130
|
+
if (shift !== 0) {
|
|
131
|
+
this.current += shift
|
|
132
|
+
this.target += shift
|
|
133
|
+
// Keep the drag reference in sync so the next onPointerMove doesn't
|
|
134
|
+
// compute a target relative to the pre-wrap baseline, which would cause
|
|
135
|
+
// the slider to snap back to the wrong position.
|
|
136
|
+
if (this.isDown) this.x.state += shift
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
_draw(x) {
|
|
143
|
+
this.el.style.transform = `translate3d(${-this._snap2px(x)}px, 0, 0)`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_updateProgress() {
|
|
147
|
+
if (!this.progressEl || this.infinite) return
|
|
148
|
+
const progress = this.limit > 0 ? this.current / this.limit : 0
|
|
149
|
+
this.progressEl.style.transformOrigin = "left center"
|
|
150
|
+
this.progressEl.style.transform = `scaleX(${gsap.utils.clamp(0, 1, progress)})`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Tick ─────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
tick = () => {
|
|
156
|
+
if (!this.isActive) return
|
|
157
|
+
|
|
158
|
+
const prev = this.current
|
|
159
|
+
this.current = lerp(this.current, this.target, this.speed)
|
|
160
|
+
|
|
161
|
+
// Snap to exact value when close enough to avoid sub-pixel drift
|
|
162
|
+
if (Math.abs(this.current - this.target) < 0.01) {
|
|
163
|
+
this.current = this.target
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Infinite: wrap on every tick (not just when settled) so a fast drag
|
|
167
|
+
// that overshoots the clone zone is corrected immediately.
|
|
168
|
+
if (this.infinite) this._wrapInfinite()
|
|
169
|
+
|
|
170
|
+
if (this.current !== prev) {
|
|
171
|
+
this._draw(this.current)
|
|
172
|
+
this._updateProgress()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Snap ─────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
_snapToNearest() {
|
|
179
|
+
if (!this.slideWidth) return
|
|
180
|
+
|
|
181
|
+
if (this.infinite) {
|
|
182
|
+
// Round to nearest slide in ABSOLUTE (un-normalised) space.
|
|
183
|
+
// Do NOT clamp target back into [0, 2·setW) here — that would invert the
|
|
184
|
+
// lerp direction if the user released before current crossed the wrap
|
|
185
|
+
// boundary, causing a visible "all-slides-jump-backward" reset.
|
|
186
|
+
// Instead, keep target in whatever cycle it is; _wrapInfinite will shift
|
|
187
|
+
// both current and target together tick-by-tick as current naturally
|
|
188
|
+
// crosses each setW boundary — always seamless, always forward.
|
|
189
|
+
this.target = Math.round(this.target / this.slideWidth) * this.slideWidth
|
|
190
|
+
// Derive real 0-based index via modulo on normalised position
|
|
191
|
+
const normPos = ((this.target % this.setW) + this.setW) % this.setW
|
|
192
|
+
this.index = Math.round(normPos / this.slideWidth) % this.total
|
|
193
|
+
} else {
|
|
194
|
+
this.index = gsap.utils.clamp(0, this.total - 1, Math.round(this.target / this.slideWidth))
|
|
195
|
+
this.target = gsap.utils.clamp(0, this.limit, this.index * this.slideWidth)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this._fireChange()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_fireChange() {
|
|
202
|
+
if (!this.onSlideChange) return
|
|
203
|
+
this.onSlideChange({ index: this.index, total: this.total })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Public navigation ────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
next = () => {
|
|
209
|
+
if (!this.isActive) return
|
|
210
|
+
if (this.infinite) {
|
|
211
|
+
this.target += this.slideWidth
|
|
212
|
+
this.index = (this.index + 1) % this.total
|
|
213
|
+
} else {
|
|
214
|
+
this.index = gsap.utils.clamp(0, this.total - 1, this.index + 1)
|
|
215
|
+
this.target = gsap.utils.clamp(0, this.limit, this.index * this.slideWidth)
|
|
216
|
+
}
|
|
217
|
+
this._fireChange()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
prev = () => {
|
|
221
|
+
if (!this.isActive) return
|
|
222
|
+
if (this.infinite) {
|
|
223
|
+
this.target -= this.slideWidth
|
|
224
|
+
this.index = (this.index - 1 + this.total) % this.total
|
|
225
|
+
} else {
|
|
226
|
+
this.index = gsap.utils.clamp(0, this.total - 1, this.index - 1)
|
|
227
|
+
this.target = gsap.utils.clamp(0, this.limit, this.index * this.slideWidth)
|
|
228
|
+
}
|
|
229
|
+
this._fireChange()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
goTo(i) {
|
|
233
|
+
if (!this.isActive) return
|
|
234
|
+
const idx = gsap.utils.clamp(0, this.total - 1, i)
|
|
235
|
+
if (this.infinite) {
|
|
236
|
+
// Find the shortest delta from current position to target idx
|
|
237
|
+
const currentIdx = this.index
|
|
238
|
+
let delta = idx - currentIdx
|
|
239
|
+
if (Math.abs(delta) > this.total / 2) {
|
|
240
|
+
delta = delta > 0 ? delta - this.total : delta + this.total
|
|
241
|
+
}
|
|
242
|
+
this.target += delta * this.slideWidth
|
|
243
|
+
} else {
|
|
244
|
+
this.target = gsap.utils.clamp(0, this.limit, idx * this.slideWidth)
|
|
245
|
+
}
|
|
246
|
+
this.index = idx
|
|
247
|
+
this._fireChange()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Pointer events ───────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
onPointerDown = e => {
|
|
253
|
+
if (!this.isActive) return
|
|
254
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
255
|
+
this.isDown = true
|
|
256
|
+
this.x.start = clientX
|
|
257
|
+
this.x.state = this.target
|
|
258
|
+
this.container.classList.add("-dragging")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onPointerMove = e => {
|
|
262
|
+
if (!this.isActive || !this.isDown) return
|
|
263
|
+
this.isDragging = true
|
|
264
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
265
|
+
const delta = this.x.start - clientX
|
|
266
|
+
let newTarget = this.x.state + delta
|
|
267
|
+
|
|
268
|
+
if (!this.infinite) {
|
|
269
|
+
newTarget = gsap.utils.clamp(0, this.limit, newTarget)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.target = newTarget
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
onPointerUp = () => {
|
|
276
|
+
if (!this.isActive) return
|
|
277
|
+
this.isDown = false
|
|
278
|
+
this.container.classList.remove("-dragging")
|
|
279
|
+
|
|
280
|
+
if (this.isDragging && this.snap) {
|
|
281
|
+
this._snapToNearest()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.isDragging = false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
on() {
|
|
290
|
+
if (!this.isActive) return
|
|
291
|
+
|
|
292
|
+
this.container.addEventListener("mousedown", this.onPointerDown)
|
|
293
|
+
this.container.addEventListener("touchstart", this.onPointerDown, { passive: true })
|
|
294
|
+
window.addEventListener("mousemove", this.onPointerMove)
|
|
295
|
+
window.addEventListener("touchmove", this.onPointerMove, { passive: true })
|
|
296
|
+
window.addEventListener("mouseup", this.onPointerUp)
|
|
297
|
+
window.addEventListener("touchend", this.onPointerUp)
|
|
298
|
+
|
|
299
|
+
if (this.prevEl) this.prevEl.addEventListener("click", this.prev)
|
|
300
|
+
if (this.nextEl) this.nextEl.addEventListener("click", this.next)
|
|
301
|
+
|
|
302
|
+
emitter.on(EVENTS.APP_TICK, this.tick)
|
|
303
|
+
emitter.on(EVENTS.APP_RESIZE, this.resize)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
off() {
|
|
307
|
+
this.container?.removeEventListener("mousedown", this.onPointerDown)
|
|
308
|
+
this.container?.removeEventListener("touchstart", this.onPointerDown)
|
|
309
|
+
window.removeEventListener("mousemove", this.onPointerMove)
|
|
310
|
+
window.removeEventListener("touchmove", this.onPointerMove)
|
|
311
|
+
window.removeEventListener("mouseup", this.onPointerUp)
|
|
312
|
+
window.removeEventListener("touchend", this.onPointerUp)
|
|
313
|
+
|
|
314
|
+
if (this.prevEl) this.prevEl.removeEventListener("click", this.prev)
|
|
315
|
+
if (this.nextEl) this.nextEl.removeEventListener("click", this.next)
|
|
316
|
+
|
|
317
|
+
emitter.off(EVENTS.APP_TICK, this.tick)
|
|
318
|
+
emitter.off(EVENTS.APP_RESIZE, this.resize)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
resize = () => {
|
|
322
|
+
const wasActive = this.isActive
|
|
323
|
+
this.isActive = this._shouldActivate()
|
|
324
|
+
|
|
325
|
+
// Was active, now inactive (e.g. mobileOnly + resized to desktop)
|
|
326
|
+
if (wasActive && !this.isActive) {
|
|
327
|
+
this._reset()
|
|
328
|
+
this.off()
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Was inactive, now active (e.g. mobileOnly + resized to mobile)
|
|
333
|
+
if (!wasActive && this.isActive) {
|
|
334
|
+
this.setup()
|
|
335
|
+
this.on()
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!this.isActive) return
|
|
340
|
+
|
|
341
|
+
if (this.infinite) {
|
|
342
|
+
// Rebuild clones and remeasure, keeping current real index visible
|
|
343
|
+
const savedIndex = this.index
|
|
344
|
+
this._setupInfinite()
|
|
345
|
+
this.index = savedIndex
|
|
346
|
+
this.current = this.target = this.setW + savedIndex * this.slideWidth
|
|
347
|
+
this._draw(this.current)
|
|
348
|
+
} else {
|
|
349
|
+
this._measure()
|
|
350
|
+
this.target = gsap.utils.clamp(0, this.limit, this.index * this.slideWidth)
|
|
351
|
+
this.current = this.target
|
|
352
|
+
this._draw(this.current)
|
|
353
|
+
this._updateProgress()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_reset() {
|
|
358
|
+
Array.from(this.el.querySelectorAll("[data-clone]")).forEach(c => c.remove())
|
|
359
|
+
this.el.style.transform = ""
|
|
360
|
+
this.current = this.target = 0
|
|
361
|
+
this.index = 0
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
destroy() {
|
|
365
|
+
this.off()
|
|
366
|
+
this._reset()
|
|
367
|
+
this.container = null
|
|
368
|
+
this.el = null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
init() {
|
|
372
|
+
this.setup()
|
|
373
|
+
this.on()
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/Smooth/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Scrollbar from "../ScrollBar"
|
|
2
|
+
import { gsap } from "@linear_non/stellar-kit/gsap"
|
|
2
3
|
import { kitStore } from "@linear_non/stellar-kit"
|
|
3
4
|
import { emitter, EVENTS } from "@linear_non/stellar-kit/events"
|
|
4
5
|
import { qs, qsa, bounds, Debug } from "@linear_non/stellar-kit/utils"
|
|
@@ -24,7 +25,7 @@ export default class Smooth {
|
|
|
24
25
|
let cursor = 0
|
|
25
26
|
|
|
26
27
|
this.elems.forEach(el => {
|
|
27
|
-
|
|
28
|
+
gsap.set(el, { y: 0 })
|
|
28
29
|
const raw = el.dataset.speed != null ? parseFloat(el.dataset.speed) : 1
|
|
29
30
|
const speed = Number.isFinite(raw) ? raw : 1
|
|
30
31
|
const { height, offset } = this.getVars(el, speed)
|
|
@@ -39,6 +40,7 @@ export default class Smooth {
|
|
|
39
40
|
|
|
40
41
|
this.sections.push({
|
|
41
42
|
el,
|
|
43
|
+
ySet: gsap.quickSetter(el, "y", "px"),
|
|
42
44
|
parent,
|
|
43
45
|
top,
|
|
44
46
|
bottom,
|
|
@@ -66,9 +68,9 @@ export default class Smooth {
|
|
|
66
68
|
this.sections.forEach(section => {
|
|
67
69
|
const { isVisible, transform } = this.isVisible(section)
|
|
68
70
|
if (isVisible || isResizing || !section.out) {
|
|
69
|
-
const
|
|
71
|
+
const y = this.getY(transform)
|
|
70
72
|
if (y !== section._roundedY) {
|
|
71
|
-
section.
|
|
73
|
+
section.ySet(y)
|
|
72
74
|
section._roundedY = y
|
|
73
75
|
}
|
|
74
76
|
section.out = !isVisible
|
|
@@ -90,10 +92,9 @@ export default class Smooth {
|
|
|
90
92
|
return { isVisible, transform }
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return { y, css: `translate3d(0, ${y}px, 0)` }
|
|
95
|
+
getY(transform) {
|
|
96
|
+
const y = -transform
|
|
97
|
+
return Math.round(y * this.dpr) / this.dpr
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
getVars(el, speed) {
|
|
@@ -137,7 +138,7 @@ export default class Smooth {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
clean() {
|
|
140
|
-
if (this.sections) this.sections.forEach(s => (s.el
|
|
141
|
+
if (this.sections) this.sections.forEach(s => gsap.set(s.el, { y: 0, clearProps: "transform" }))
|
|
141
142
|
this.elems = this.sections = null
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -124,7 +124,7 @@ export default class SpritePlayer {
|
|
|
124
124
|
this.dom.alphaImages = alphaImages
|
|
125
125
|
|
|
126
126
|
if (this.isAlpha && this.dom.alphaImages.length) {
|
|
127
|
-
this.dom.alphaImages = this.dom.alphaImages.map(img => this.toAlphaFromLuma(img))
|
|
127
|
+
this.dom.alphaImages = await Promise.all(this.dom.alphaImages.map(img => this.toAlphaFromLuma(img)))
|
|
128
128
|
}
|
|
129
129
|
if (this.isAlpha && this.dom.alphaImages.length !== this.dom.images.length) {
|
|
130
130
|
this.isAlpha = false
|
|
@@ -154,8 +154,8 @@ export default class SpritePlayer {
|
|
|
154
154
|
Object.assign(this.canvas, { width: rect.width, height: rect.height, rect, area, offset })
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
// grayscale → alpha (returns
|
|
158
|
-
toAlphaFromLuma(img) {
|
|
157
|
+
// grayscale → alpha (returns an ImageBitmap; temp canvas backing store freed immediately)
|
|
158
|
+
async toAlphaFromLuma(img) {
|
|
159
159
|
const w = img.naturalWidth || img.width
|
|
160
160
|
const h = img.naturalHeight || img.height
|
|
161
161
|
const c = document.createElement("canvas")
|
|
@@ -173,7 +173,11 @@ export default class SpritePlayer {
|
|
|
173
173
|
a[i + 3] = lum
|
|
174
174
|
}
|
|
175
175
|
x.putImageData(d, 0, 0)
|
|
176
|
-
|
|
176
|
+
const bitmap = await createImageBitmap(c)
|
|
177
|
+
// Release backing store — bitmap is now the only reference needed
|
|
178
|
+
c.width = 0
|
|
179
|
+
c.height = 0
|
|
180
|
+
return bitmap
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
drawImageCover(ctx, img, area, offset) {
|
|
@@ -254,6 +258,7 @@ export default class SpritePlayer {
|
|
|
254
258
|
if (!this.persistent) {
|
|
255
259
|
this._loaders.forEach(l => l.destroy?.())
|
|
256
260
|
}
|
|
261
|
+
this.dom.alphaImages.forEach(bmp => bmp?.close?.())
|
|
257
262
|
this.dom.images = []
|
|
258
263
|
this.dom.alphaImages = []
|
|
259
264
|
this._loaders = []
|
package/src/index.js
CHANGED
|
@@ -5,5 +5,6 @@ import Noise from "./Noise"
|
|
|
5
5
|
import SpritePlayer from "./SpritePlayer"
|
|
6
6
|
import CursorTracker from "./CursorTracker"
|
|
7
7
|
import Marquee from "./Marquee"
|
|
8
|
+
import Slider from "./Slider"
|
|
8
9
|
|
|
9
|
-
export { Sticky, Smooth, SplitOnScroll, Noise, SpritePlayer, CursorTracker, Marquee }
|
|
10
|
+
export { Sticky, Smooth, SplitOnScroll, Noise, SpritePlayer, CursorTracker, Marquee, Slider }
|