react-scroll-media 1.0.0 → 1.0.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/LICENSE +1 -1
- package/README.md +484 -256
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,315 +1,471 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Zero scroll-jacking. Pure sticky positioning. 60fps performance.
|
|
3
|
+
# 🎬 React Scroll Media
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## ✨ Features
|
|
9
|
-
|
|
10
|
-
- **🚀 Native Performance**:
|
|
11
|
-
- Uses `requestAnimationFrame` for buttery smooth 60fps rendering.
|
|
12
|
-
- **No Scroll Jacking**: We never hijack the scrollbar. It works with native scrolling.
|
|
13
|
-
- **CSS Sticky**: Uses relatively positioned containers with sticky inner content.
|
|
14
|
-
- **🖼️ Flexible Loading**:
|
|
15
|
-
- **Manual**: Pass an array of image URLs.
|
|
16
|
-
- **Pattern**: Generate sequences like `/img_{index}.jpg`.
|
|
17
|
-
- **Manifest**: Load sequences from a JSON manifest.
|
|
18
|
-
- **🧠 Smart Memory Management**:
|
|
19
|
-
- **Lazy Mode**: Keeps only ±3 frames in memory for huge sequences (800+ frames).
|
|
20
|
-
- **Eager Mode**: Preloads everything for maximum smoothness on smaller sequences.
|
|
21
|
-
- **Decoding**: Uses `img.decode()` to prevent main-thread jank during painting.
|
|
22
|
-
- **🛠️ Developer Experience**:
|
|
23
|
-
- **Debug Overlay**: Visualize progress and frame index in real-time.
|
|
24
|
-
- **Hooks**: Exported `useScrollSequence` for custom UI implementations.
|
|
25
|
-
- **TypeScript**: First-class type definitions.
|
|
26
|
-
- **SSR Safe**: Works perfectly with Next.js / Remix / Gatsby.
|
|
27
|
-
- **A11y**: Built-in support for `prefers-reduced-motion` and ARIA attributes.
|
|
28
|
-
- **Robust**: Error boundaries and callbacks for image load failures.
|
|
29
|
-
|
|
30
|
-
## 🤔 When to use this vs Video?
|
|
5
|
+
**Production-ready, cinematic scroll sequences for React.**
|
|
31
6
|
|
|
32
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/react-scroll-media)
|
|
8
|
+
[](https://www.npmjs.com/package/react-scroll-media)
|
|
9
|
+
[](https://bundlephobia.com/package/react-scroll-media)
|
|
10
|
+
[](https://github.com/yourusername/react-scroll-media/blob/main/LICENSE)
|
|
33
11
|
|
|
34
|
-
|
|
12
|
+
*Zero scroll-jacking • Pure sticky positioning • 60fps performance*
|
|
35
13
|
|
|
36
|
-
|
|
14
|
+
[Installation](#-installation) • [Usage](#-usage) • [API](#%EF%B8%8F-configuration) • [Examples](#-usage)
|
|
37
15
|
|
|
38
|
-
|
|
16
|
+
</div>
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Lossless / Exact Frames (CRISP)
|
|
18
|
+
---
|
|
43
19
|
|
|
44
|
-
|
|
20
|
+
## 🌟 Overview
|
|
45
21
|
|
|
46
|
-
|
|
22
|
+
`react-scroll-media` is a lightweight library for creating Apple-style "scrollytelling" image sequences. It maps scroll progress to image frames deterministically, using standard CSS sticky positioning for a native, jank-free feel.
|
|
47
23
|
|
|
48
|
-
|
|
24
|
+
<br />
|
|
49
25
|
|
|
50
|
-
|
|
26
|
+
<div align="center">
|
|
27
|
+
<img src="https://github.com/iam-saiteja/react-scroll-media/blob/master/demo.gif?raw=true" alt="React Scroll Media Demo" width="600" />
|
|
28
|
+
<p><em><strong>Above:</strong> A 60fps scroll-driven sequence. The animation frame is tied 1:1 to the scroll position, allowing for instant scrubbing and pausing at any angle.</em></p>
|
|
29
|
+
</div>
|
|
51
30
|
|
|
52
|
-
|
|
31
|
+
<br />
|
|
53
32
|
|
|
54
|
-
|
|
33
|
+
## ✨ Features
|
|
55
34
|
|
|
56
|
-
**
|
|
35
|
+
### 🚀 **Native Performance**
|
|
36
|
+
- Uses `requestAnimationFrame` for buttery smooth 60fps rendering
|
|
37
|
+
- **No Scroll Jacking** — We never hijack the scrollbar. It works with native scrolling
|
|
38
|
+
- **CSS Sticky** — Uses relatively positioned containers with sticky inner content
|
|
57
39
|
|
|
58
|
-
|
|
40
|
+
### 🖼️ **Flexible Loading**
|
|
41
|
+
- **Manual** — Pass an array of image URLs
|
|
42
|
+
- **Pattern** — Generate sequences like `/img_{index}.jpg`
|
|
43
|
+
- **Manifest** — Load sequences from a JSON manifest
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
### 🧠 **Smart Memory Management**
|
|
46
|
+
- **Lazy Mode** — Keeps only ±3 frames in memory for huge sequences (800+ frames)
|
|
47
|
+
- **Eager Mode** — Preloads everything for maximum smoothness on smaller sequences
|
|
48
|
+
- **Decoding** — Uses `img.decode()` to prevent main-thread jank during painting
|
|
61
49
|
|
|
62
|
-
**
|
|
50
|
+
### 🛠️ **Developer Experience**
|
|
51
|
+
- **Debug Overlay** — Visualize progress and frame index in real-time
|
|
52
|
+
- **Hooks** — Exported `useScrollSequence` for custom UI implementations
|
|
53
|
+
- **TypeScript** — First-class type definitions
|
|
54
|
+
- **SSR Safe** — Works perfectly with Next.js / Remix / Gatsby
|
|
55
|
+
- **A11y** — Built-in support for `prefers-reduced-motion` and ARIA attributes
|
|
56
|
+
- **Robust** — Error boundaries and callbacks for image load failures
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
<br />
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
---
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
## 🤔 When to Use This vs Video?
|
|
63
|
+
|
|
64
|
+
<table>
|
|
65
|
+
<thead>
|
|
66
|
+
<tr>
|
|
67
|
+
<th>Feature</th>
|
|
68
|
+
<th>Video (<code><video></code>)</th>
|
|
69
|
+
<th>Scroll Sequence (<code>react-scroll-media</code>)</th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
<tr>
|
|
74
|
+
<td><strong>Quality</strong></td>
|
|
75
|
+
<td>Compressed (artifacts)</td>
|
|
76
|
+
<td>✨ Lossless / Exact Frames (CRISP)</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td><strong>Transparency</strong></td>
|
|
80
|
+
<td>Difficult (needs webm/hevc)</td>
|
|
81
|
+
<td>✨ Native PNG/WebP Transparency (Easy)</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<tr>
|
|
84
|
+
<td><strong>Scrubbing</strong></td>
|
|
85
|
+
<td>Janky (keyframe dependency)</td>
|
|
86
|
+
<td>✨ 1:1 Instant Scrubbing</td>
|
|
87
|
+
</tr>
|
|
88
|
+
<tr>
|
|
89
|
+
<td><strong>Mobile</strong></td>
|
|
90
|
+
<td>Auto-play often blocked</td>
|
|
91
|
+
<td>✨ Works everywhere</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<tr>
|
|
94
|
+
<td><strong>File Size</strong></td>
|
|
95
|
+
<td>✨ Small</td>
|
|
96
|
+
<td>Large (requires optimization/lazy loading)</td>
|
|
97
|
+
</tr>
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
|
|
101
|
+
**💡 Use Scroll Sequence** when you need perfect interaction, transparency, or crystal-clear product visuals (like Apple).
|
|
102
|
+
|
|
103
|
+
**💡 Use Video** for long, non-interactive backgrounds.
|
|
104
|
+
|
|
105
|
+
<br />
|
|
69
106
|
|
|
70
107
|
---
|
|
71
108
|
|
|
72
109
|
## 📦 Installation
|
|
73
110
|
|
|
74
111
|
```bash
|
|
75
|
-
npm install react-scroll-media
|
|
112
|
+
npm install react-scroll-media
|
|
76
113
|
```
|
|
77
114
|
|
|
115
|
+
**or**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
yarn add react-scroll-media
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
<br />
|
|
122
|
+
|
|
78
123
|
---
|
|
79
124
|
|
|
80
125
|
## 🚀 Usage
|
|
81
126
|
|
|
82
|
-
### Basic Example
|
|
127
|
+
### 🎯 Basic Example
|
|
83
128
|
|
|
84
129
|
The simplest way to use it is with the `ScrollSequence` component.
|
|
85
130
|
|
|
86
131
|
```tsx
|
|
87
|
-
import { ScrollSequence } from 'react-scroll-media';
|
|
132
|
+
import { ScrollSequence } from 'react-scroll-media';
|
|
133
|
+
|
|
134
|
+
const frames = [
|
|
135
|
+
'/images/frame_01.jpg',
|
|
136
|
+
'/images/frame_02.jpg',
|
|
137
|
+
// ...
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
export default function MyPage() {
|
|
141
|
+
return (
|
|
142
|
+
<div style={{ height: '200vh' }}>
|
|
143
|
+
<h1>Scroll Down</h1>
|
|
144
|
+
|
|
145
|
+
<ScrollSequence
|
|
146
|
+
source={{ type: 'manual', frames }}
|
|
147
|
+
scrollLength="300vh" // Determines how long the sequence plays
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<h1>Continue Scrolling</h1>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
88
154
|
```
|
|
89
155
|
|
|
156
|
+
<br />
|
|
157
|
+
|
|
90
158
|
### ✨ Scrollytelling & Composition
|
|
91
159
|
|
|
92
160
|
You can nest components inside `ScrollSequence`. They will be placed in the sticky container and can react to the timeline.
|
|
93
161
|
|
|
94
|
-
|
|
162
|
+
<br />
|
|
163
|
+
|
|
164
|
+
#### 📝 Animated Text (`ScrollText`)
|
|
95
165
|
|
|
96
166
|
Animate opacity and position based on scroll progress (0 to 1). Supports enter and exit phases.
|
|
97
167
|
|
|
98
168
|
```tsx
|
|
99
|
-
import { ScrollSequence, ScrollText } from 'react-scroll-media'
|
|
169
|
+
import { ScrollSequence, ScrollText } from 'react-scroll-media';
|
|
170
|
+
|
|
171
|
+
<ScrollSequence source={...} scrollLength="400vh">
|
|
172
|
+
|
|
173
|
+
{/* Fade In (0.1-0.2) -> Hold -> Fade Out (0.8-0.9) */}
|
|
174
|
+
<ScrollText
|
|
175
|
+
start={0.1}
|
|
176
|
+
end={0.2}
|
|
177
|
+
exitStart={0.8}
|
|
178
|
+
exitEnd={0.9}
|
|
179
|
+
translateY={50}
|
|
180
|
+
className="my-text-overlay"
|
|
181
|
+
>
|
|
182
|
+
Cinematic Experience
|
|
183
|
+
</ScrollText>
|
|
184
|
+
|
|
185
|
+
</ScrollSequence>
|
|
100
186
|
```
|
|
101
187
|
|
|
102
|
-
|
|
188
|
+
<br />
|
|
189
|
+
|
|
190
|
+
#### 💬 Word Reveal (`ScrollWordReveal`)
|
|
103
191
|
|
|
104
192
|
Reveals text word-by-word as you scroll.
|
|
105
193
|
|
|
106
194
|
```tsx
|
|
107
|
-
import { ScrollWordReveal } from 'react-scroll-media'
|
|
195
|
+
import { ScrollWordReveal } from 'react-scroll-media';
|
|
196
|
+
|
|
197
|
+
<ScrollWordReveal
|
|
198
|
+
text="Experience the smooth cinematic scroll."
|
|
199
|
+
start={0.4}
|
|
200
|
+
end={0.6}
|
|
201
|
+
style={{ fontSize: '2rem', color: 'white' }}
|
|
202
|
+
/>
|
|
108
203
|
```
|
|
109
204
|
|
|
110
|
-
|
|
205
|
+
<br />
|
|
206
|
+
|
|
207
|
+
### 🔧 Advanced: Custom Hooks
|
|
111
208
|
|
|
112
209
|
For full control over the specialized UI, use the headless hooks.
|
|
113
210
|
|
|
211
|
+
<br />
|
|
212
|
+
|
|
114
213
|
#### `useScrollSequence`
|
|
115
214
|
|
|
116
215
|
Manages the canvas image controller.
|
|
117
216
|
|
|
118
217
|
```tsx
|
|
119
|
-
import { useScrollSequence } from 'react-scroll-media';
|
|
218
|
+
import { useScrollSequence } from 'react-scroll-media';
|
|
219
|
+
|
|
220
|
+
const CustomScroller = () => {
|
|
221
|
+
// ... setup refs
|
|
222
|
+
const { containerRef, canvasRef, isLoaded } = useScrollSequence({ ... });
|
|
223
|
+
// ... render custom structure
|
|
224
|
+
};
|
|
120
225
|
```
|
|
121
226
|
|
|
227
|
+
<br />
|
|
228
|
+
|
|
122
229
|
#### `useScrollTimeline`
|
|
123
230
|
|
|
124
231
|
Subscribe to the scroll timeline in any component.
|
|
125
232
|
|
|
126
233
|
```tsx
|
|
127
|
-
import { useScrollTimeline } from 'react-scroll-media';
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## ⚙️ Configuration
|
|
133
|
-
|
|
134
|
-
### `ScrollSequence` Props
|
|
135
|
-
|
|
136
|
-
Prop
|
|
137
|
-
|
|
138
|
-
Type
|
|
139
|
-
|
|
140
|
-
Default
|
|
141
|
-
|
|
142
|
-
Description
|
|
143
|
-
|
|
144
|
-
`source`
|
|
145
|
-
|
|
146
|
-
`SequenceSource`
|
|
147
|
-
|
|
148
|
-
**Required**
|
|
149
|
-
|
|
150
|
-
Defines where images come from.
|
|
151
|
-
|
|
152
|
-
`scrollLength`
|
|
153
|
-
|
|
154
|
-
`string`
|
|
155
|
-
|
|
156
|
-
`"300vh"`
|
|
157
|
-
|
|
158
|
-
Height of the container (animation duration).
|
|
159
|
-
|
|
160
|
-
`memoryStrategy`
|
|
161
|
-
|
|
162
|
-
`"eager" | "lazy"`
|
|
163
|
-
|
|
164
|
-
`"eager"`
|
|
165
|
-
|
|
166
|
-
Optimization strategy.
|
|
167
|
-
|
|
168
|
-
`lazyBuffer`
|
|
169
|
-
|
|
170
|
-
`number`
|
|
171
|
-
|
|
172
|
-
`10`
|
|
173
|
-
|
|
174
|
-
Number of frames to keep loaded in lazy mode.
|
|
175
|
-
|
|
176
|
-
`fallback`
|
|
177
|
-
|
|
178
|
-
`ReactNode`
|
|
179
|
-
|
|
180
|
-
`null`
|
|
181
|
-
|
|
182
|
-
Loading state component.
|
|
183
|
-
|
|
184
|
-
`accessibilityLabel`
|
|
185
|
-
|
|
186
|
-
`string`
|
|
187
|
-
|
|
188
|
-
`"Scroll sequence"`
|
|
189
|
-
|
|
190
|
-
ARIA label for the canvas. Example: `"360 degree view of the product"`.
|
|
191
|
-
|
|
192
|
-
`debug`
|
|
193
|
-
|
|
194
|
-
`boolean`
|
|
195
|
-
|
|
196
|
-
`false`
|
|
197
|
-
|
|
198
|
-
Shows debug overlay.
|
|
199
|
-
|
|
200
|
-
`onError`
|
|
201
|
-
|
|
202
|
-
`(error: Error) => void`
|
|
203
|
-
|
|
204
|
-
`undefined`
|
|
205
|
-
|
|
206
|
-
Callback fired when an image fails to load or initialization errors occur.
|
|
207
|
-
|
|
208
|
-
## 📊 Performance & compatibility
|
|
209
|
-
|
|
210
|
-
### Bundle Size
|
|
211
|
-
|
|
212
|
-
- **Minified**: ~22.0 kB
|
|
213
|
-
- **Gzipped**: ~6.08 kB
|
|
214
|
-
- Zero dependencies (uses native Canvas API, no heavyweight libraries).
|
|
215
|
-
|
|
216
|
-
### Browser Support
|
|
217
|
-
|
|
218
|
-
Browser
|
|
219
|
-
|
|
220
|
-
Status
|
|
221
|
-
|
|
222
|
-
Note
|
|
223
|
-
|
|
224
|
-
Chrome
|
|
234
|
+
import { useScrollTimeline } from 'react-scroll-media';
|
|
225
235
|
|
|
226
|
-
|
|
236
|
+
const MyComponent = () => {
|
|
237
|
+
const { subscribe } = useScrollTimeline();
|
|
227
238
|
|
|
228
|
-
|
|
239
|
+
// Subscribe to progress (0-1)
|
|
240
|
+
useEffect(() => subscribe((progress) => {
|
|
241
|
+
console.log('Progress:', progress);
|
|
242
|
+
}), [subscribe]);
|
|
229
243
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Full support
|
|
235
|
-
|
|
236
|
-
Safari
|
|
237
|
-
|
|
238
|
-
✅
|
|
239
|
-
|
|
240
|
-
Full support (Desktop & Mobile)
|
|
241
|
-
|
|
242
|
-
Edge
|
|
243
|
-
|
|
244
|
-
✅
|
|
245
|
-
|
|
246
|
-
Full support
|
|
247
|
-
|
|
248
|
-
IE11
|
|
249
|
-
|
|
250
|
-
❌
|
|
251
|
-
|
|
252
|
-
Not supported (Missing ES6/Canvas features)
|
|
253
|
-
|
|
254
|
-
### Accessibility (A11y)
|
|
255
|
-
|
|
256
|
-
- **Keyboard Navigation**: Users can scrub through the sequence using standard keyboard controls (Arrow Keys, Spacebar, Page Up/Down) because it relies on native scrolling.
|
|
257
|
-
- **Screen Readers**: Add `accessibilityLabel` to `ScrollSequence` to provide a description for the canvas. Canvas has `role="img"`.
|
|
258
|
-
- **Reduced Motion**: Automatically detects `prefers-reduced-motion: reduce`. If enabled, `ScrollSequence` will disable the scroll animation and display the `fallback` content (if provided) or simply freeze the first frame to prevent motion sickness.
|
|
259
|
-
|
|
260
|
-
### Memory Usage (Benchmarks)
|
|
261
|
-
|
|
262
|
-
Tested on 1080p frames.
|
|
263
|
-
|
|
264
|
-
Frames
|
|
265
|
-
|
|
266
|
-
Strategy
|
|
267
|
-
|
|
268
|
-
Memory
|
|
269
|
-
|
|
270
|
-
Recommendation
|
|
271
|
-
|
|
272
|
-
100
|
|
273
|
-
|
|
274
|
-
`eager`
|
|
275
|
-
|
|
276
|
-
~50MB
|
|
277
|
-
|
|
278
|
-
Instant seeking, smooth.
|
|
279
|
-
|
|
280
|
-
500
|
|
281
|
-
|
|
282
|
-
`eager`
|
|
244
|
+
return <div>...</div>;
|
|
245
|
+
};
|
|
246
|
+
```
|
|
283
247
|
|
|
284
|
-
|
|
248
|
+
<br />
|
|
285
249
|
|
|
286
|
-
|
|
250
|
+
---
|
|
287
251
|
|
|
288
|
-
|
|
252
|
+
## ⚙️ Configuration
|
|
289
253
|
|
|
290
|
-
`
|
|
254
|
+
### `ScrollSequence` Props
|
|
291
255
|
|
|
292
|
-
|
|
256
|
+
<table>
|
|
257
|
+
<thead>
|
|
258
|
+
<tr>
|
|
259
|
+
<th>Prop</th>
|
|
260
|
+
<th>Type</th>
|
|
261
|
+
<th>Default</th>
|
|
262
|
+
<th>Description</th>
|
|
263
|
+
</tr>
|
|
264
|
+
</thead>
|
|
265
|
+
<tbody>
|
|
266
|
+
<tr>
|
|
267
|
+
<td><code>source</code></td>
|
|
268
|
+
<td><code>SequenceSource</code></td>
|
|
269
|
+
<td><strong>Required</strong></td>
|
|
270
|
+
<td>Defines where images come from.</td>
|
|
271
|
+
</tr>
|
|
272
|
+
<tr>
|
|
273
|
+
<td><code>scrollLength</code></td>
|
|
274
|
+
<td><code>string</code></td>
|
|
275
|
+
<td><code>"300vh"</code></td>
|
|
276
|
+
<td>Height of the container (animation duration).</td>
|
|
277
|
+
</tr>
|
|
278
|
+
<tr>
|
|
279
|
+
<td><code>memoryStrategy</code></td>
|
|
280
|
+
<td><code>"eager" | "lazy"</code></td>
|
|
281
|
+
<td><code>"eager"</code></td>
|
|
282
|
+
<td>Optimization strategy.</td>
|
|
283
|
+
</tr>
|
|
284
|
+
<tr>
|
|
285
|
+
<td><code>lazyBuffer</code></td>
|
|
286
|
+
<td><code>number</code></td>
|
|
287
|
+
<td><code>10</code></td>
|
|
288
|
+
<td>Number of frames to keep loaded in lazy mode.</td>
|
|
289
|
+
</tr>
|
|
290
|
+
<tr>
|
|
291
|
+
<td><code>fallback</code></td>
|
|
292
|
+
<td><code>ReactNode</code></td>
|
|
293
|
+
<td><code>null</code></td>
|
|
294
|
+
<td>Loading state component.</td>
|
|
295
|
+
</tr>
|
|
296
|
+
<tr>
|
|
297
|
+
<td><code>accessibilityLabel</code></td>
|
|
298
|
+
<td><code>string</code></td>
|
|
299
|
+
<td><code>"Scroll sequence"</code></td>
|
|
300
|
+
<td>ARIA label for the canvas. Example: <code>"360 degree view of the product"</code>.</td>
|
|
301
|
+
</tr>
|
|
302
|
+
<tr>
|
|
303
|
+
<td><code>debug</code></td>
|
|
304
|
+
<td><code>boolean</code></td>
|
|
305
|
+
<td><code>false</code></td>
|
|
306
|
+
<td>Shows debug overlay.</td>
|
|
307
|
+
</tr>
|
|
308
|
+
<tr>
|
|
309
|
+
<td><code>onError</code></td>
|
|
310
|
+
<td><code>(error: Error) => void</code></td>
|
|
311
|
+
<td><code>undefined</code></td>
|
|
312
|
+
<td>Callback fired when an image fails to load or initialization errors occur.</td>
|
|
313
|
+
</tr>
|
|
314
|
+
</tbody>
|
|
315
|
+
</table>
|
|
316
|
+
|
|
317
|
+
<br />
|
|
293
318
|
|
|
294
|
-
|
|
319
|
+
---
|
|
295
320
|
|
|
296
|
-
|
|
321
|
+
## 📊 Performance & Compatibility
|
|
322
|
+
|
|
323
|
+
### 📦 Bundle Size
|
|
324
|
+
|
|
325
|
+
| Metric | Size |
|
|
326
|
+
|--------|------|
|
|
327
|
+
| **Minified** | ~22.0 kB |
|
|
328
|
+
| **Gzipped** | ~6.08 kB |
|
|
329
|
+
|
|
330
|
+
✨ **Zero dependencies** — Uses native Canvas API, no heavyweight libraries.
|
|
331
|
+
|
|
332
|
+
<br />
|
|
333
|
+
|
|
334
|
+
### 🌐 Browser Support
|
|
335
|
+
|
|
336
|
+
<table>
|
|
337
|
+
<thead>
|
|
338
|
+
<tr>
|
|
339
|
+
<th>Browser</th>
|
|
340
|
+
<th>Status</th>
|
|
341
|
+
<th>Note</th>
|
|
342
|
+
</tr>
|
|
343
|
+
</thead>
|
|
344
|
+
<tbody>
|
|
345
|
+
<tr>
|
|
346
|
+
<td>Chrome</td>
|
|
347
|
+
<td>✅</td>
|
|
348
|
+
<td>Full support (OffscreenCanvas enabled)</td>
|
|
349
|
+
</tr>
|
|
350
|
+
<tr>
|
|
351
|
+
<td>Firefox</td>
|
|
352
|
+
<td>✅</td>
|
|
353
|
+
<td>Full support</td>
|
|
354
|
+
</tr>
|
|
355
|
+
<tr>
|
|
356
|
+
<td>Safari</td>
|
|
357
|
+
<td>✅</td>
|
|
358
|
+
<td>Full support (Desktop & Mobile)</td>
|
|
359
|
+
</tr>
|
|
360
|
+
<tr>
|
|
361
|
+
<td>Edge</td>
|
|
362
|
+
<td>✅</td>
|
|
363
|
+
<td>Full support</td>
|
|
364
|
+
</tr>
|
|
365
|
+
<tr>
|
|
366
|
+
<td>IE11</td>
|
|
367
|
+
<td>❌</td>
|
|
368
|
+
<td>Not supported (Missing ES6/Canvas features)</td>
|
|
369
|
+
</tr>
|
|
370
|
+
</tbody>
|
|
371
|
+
</table>
|
|
372
|
+
|
|
373
|
+
<br />
|
|
374
|
+
|
|
375
|
+
### ♿ Accessibility (A11y)
|
|
376
|
+
|
|
377
|
+
- **🎹 Keyboard Navigation** — Users can scrub through the sequence using standard keyboard controls (Arrow Keys, Spacebar, Page Up/Down) because it relies on native scrolling.
|
|
378
|
+
|
|
379
|
+
- **🔊 Screen Readers** — Add `accessibilityLabel` to `ScrollSequence` to provide a description for the canvas. Canvas has `role="img"`.
|
|
380
|
+
|
|
381
|
+
- **🎭 Reduced Motion** — Automatically detects `prefers-reduced-motion: reduce`. If enabled, `ScrollSequence` will disable the scroll animation and display the `fallback` content (if provided) or simply freeze the first frame to prevent motion sickness.
|
|
382
|
+
|
|
383
|
+
<br />
|
|
384
|
+
|
|
385
|
+
### 💾 Memory Usage (Benchmarks)
|
|
386
|
+
|
|
387
|
+
*Tested on 1080p frames.*
|
|
388
|
+
|
|
389
|
+
<table>
|
|
390
|
+
<thead>
|
|
391
|
+
<tr>
|
|
392
|
+
<th>Frames</th>
|
|
393
|
+
<th>Strategy</th>
|
|
394
|
+
<th>Memory</th>
|
|
395
|
+
<th>Recommendation</th>
|
|
396
|
+
</tr>
|
|
397
|
+
</thead>
|
|
398
|
+
<tbody>
|
|
399
|
+
<tr>
|
|
400
|
+
<td>100</td>
|
|
401
|
+
<td><code>eager</code></td>
|
|
402
|
+
<td>30MB</td>
|
|
403
|
+
<td>Instant seeking, smooth.</td>
|
|
404
|
+
</tr>
|
|
405
|
+
<tr>
|
|
406
|
+
<td>500</td>
|
|
407
|
+
<td><code>eager</code></td>
|
|
408
|
+
<td>46MB</td>
|
|
409
|
+
<td>High RAM usage.</td>
|
|
410
|
+
</tr>
|
|
411
|
+
<tr>
|
|
412
|
+
<td>1000</td>
|
|
413
|
+
<td><code>eager</code></td>
|
|
414
|
+
<td>57MB</td>
|
|
415
|
+
<td>Very high RAM usage.</td>
|
|
416
|
+
</tr>
|
|
417
|
+
<tr>
|
|
418
|
+
<td>100</td>
|
|
419
|
+
<td><code>lazy</code></td>
|
|
420
|
+
<td>25MB</td>
|
|
421
|
+
<td>Low memory usage.</td>
|
|
422
|
+
</tr>
|
|
423
|
+
<tr>
|
|
424
|
+
<td>500</td>
|
|
425
|
+
<td><code>lazy</code></td>
|
|
426
|
+
<td>30MB</td>
|
|
427
|
+
<td>Low memory usage.</td>
|
|
428
|
+
</tr>
|
|
429
|
+
<tr>
|
|
430
|
+
<td>1000</td>
|
|
431
|
+
<td><code>lazy</code></td>
|
|
432
|
+
<td>45MB</td>
|
|
433
|
+
<td><strong>⭐ Recommended</strong>. Kept flat constant.</td>
|
|
434
|
+
</tr>
|
|
435
|
+
</tbody>
|
|
436
|
+
</table>
|
|
437
|
+
|
|
438
|
+
<br />
|
|
439
|
+
|
|
440
|
+
### 🛡️ Error Handling & Fallbacks
|
|
297
441
|
|
|
298
442
|
Network errors are handled gracefully. You can provide a fallback UI that displays while images are loading or if they fail.
|
|
299
443
|
|
|
300
444
|
```tsx
|
|
301
|
-
<ScrollSequence
|
|
445
|
+
<ScrollSequence
|
|
446
|
+
source={{ type: 'manifest', url: '/bad_url.json' }}
|
|
447
|
+
fallback={<div className="error">Failed to load sequence</div>}
|
|
448
|
+
onError={(e) => console.error("Sequence error:", e)}
|
|
449
|
+
/>
|
|
302
450
|
```
|
|
303
451
|
|
|
304
|
-
|
|
452
|
+
<br />
|
|
453
|
+
|
|
454
|
+
### 🚨 Error Boundaries
|
|
305
455
|
|
|
306
456
|
For robust production apps, wrap `ScrollSequence` in an Error Boundary to catch unexpected crashes:
|
|
307
457
|
|
|
308
458
|
```tsx
|
|
309
|
-
|
|
310
|
-
|
|
459
|
+
class ErrorBoundary extends React.Component<
|
|
460
|
+
{ fallback: React.ReactNode, children: React.ReactNode },
|
|
461
|
+
{ hasError: boolean }
|
|
462
|
+
> {
|
|
311
463
|
state = { hasError: false };
|
|
312
|
-
|
|
464
|
+
|
|
465
|
+
static getDerivedStateFromError() {
|
|
466
|
+
return { hasError: true };
|
|
467
|
+
}
|
|
468
|
+
|
|
313
469
|
render() {
|
|
314
470
|
if (this.state.hasError) return this.props.fallback;
|
|
315
471
|
return this.props.children;
|
|
@@ -321,65 +477,117 @@ class ErrorBoundary extends React.Component<{ fallback: React.ReactNode, childre
|
|
|
321
477
|
<ScrollSequence source={...} />
|
|
322
478
|
</ErrorBoundary>
|
|
323
479
|
```
|
|
324
|
-
```
|
|
325
480
|
|
|
326
|
-
|
|
481
|
+
<br />
|
|
482
|
+
|
|
483
|
+
### 🔄 Multi-Instance & Nested Scroll
|
|
327
484
|
|
|
328
485
|
`react-scroll-media` automatically handles multiple instances on the same page. Each instance:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
486
|
+
|
|
487
|
+
1. Registers with a shared `RAF` loop (singleton) for optimal performance.
|
|
488
|
+
2. Calculates its own progress independently.
|
|
489
|
+
3. Should have a unique `scrollLength` or container setup.
|
|
490
|
+
|
|
491
|
+
<br />
|
|
332
492
|
|
|
333
493
|
---
|
|
334
494
|
|
|
335
495
|
## 🏗️ Architecture
|
|
336
496
|
|
|
337
|
-
### `SequenceSource` Options
|
|
497
|
+
### 📂 `SequenceSource` Options
|
|
338
498
|
|
|
339
|
-
|
|
499
|
+
<br />
|
|
500
|
+
|
|
501
|
+
#### **1. Manual Mode** (Pass array directly)
|
|
340
502
|
|
|
341
503
|
```ts
|
|
342
|
-
{
|
|
504
|
+
{
|
|
505
|
+
type: 'manual',
|
|
506
|
+
frames: ['/img/1.jpg', '/img/2.jpg']
|
|
507
|
+
}
|
|
343
508
|
```
|
|
344
509
|
|
|
345
|
-
|
|
510
|
+
<br />
|
|
511
|
+
|
|
512
|
+
#### **2. Pattern Mode** (Generate URLs)
|
|
346
513
|
|
|
347
514
|
```ts
|
|
348
|
-
{
|
|
515
|
+
{
|
|
516
|
+
type: 'pattern',
|
|
517
|
+
url: '/assets/sequence_{index}.jpg', // {index} is replaced
|
|
518
|
+
start: 1, // Start index
|
|
519
|
+
end: 100, // End index
|
|
520
|
+
pad: 4 // Zero padding (e.g. 1 -> 0001)
|
|
521
|
+
}
|
|
349
522
|
```
|
|
350
523
|
|
|
351
|
-
|
|
524
|
+
<br />
|
|
525
|
+
|
|
526
|
+
#### **3. Manifest Mode** (Fetch JSON)
|
|
352
527
|
|
|
353
528
|
```ts
|
|
354
|
-
{
|
|
529
|
+
{
|
|
530
|
+
type: 'manifest',
|
|
531
|
+
url: '/sequence.json'
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// JSON format: { "frames": ["url1", "url2"] } OR pattern config
|
|
355
535
|
```
|
|
356
536
|
|
|
357
|
-
>
|
|
537
|
+
> **💡 Note**: Manifests are cached in memory by URL. To force a refresh, append a query param (e.g. `?v=2`).
|
|
358
538
|
|
|
359
|
-
|
|
539
|
+
<br />
|
|
360
540
|
|
|
361
|
-
|
|
541
|
+
---
|
|
362
542
|
|
|
363
|
-
|
|
543
|
+
## 🎨 How it Works (The "Sticky" Technique)
|
|
364
544
|
|
|
365
545
|
Unlike libraries that use `position: fixed` or JS-based scroll locking (which breaks refreshing and feels unnatural), we use **CSS Sticky Positioning**.
|
|
366
546
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
547
|
+
<br />
|
|
548
|
+
|
|
549
|
+
<div align="center">
|
|
550
|
+
<img src="https://github.com/iam-saiteja/react-scroll-media/blob/master/demo-213.gif?raw=true" alt="React Scroll Media Technical Demo" width="600" />
|
|
551
|
+
<p><em><strong>Technical Demo:</strong> This visualization shows the direct correlation between the scrollbar position and the rendered frame. The component calculates the exact frame index based on the percentage of the container scrolled, ensuring perfect synchronization without "scroll jacking".</em></p>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<br />
|
|
555
|
+
|
|
556
|
+
### 🔧 Technical Breakdown
|
|
557
|
+
|
|
558
|
+
1. **Container (`relative`)** — This element has the height you specify (e.g., `300vh`). It occupies space in the document flow.
|
|
559
|
+
|
|
560
|
+
2. **Sticky Wrapper (`sticky`)** — Inside the container, we place a `div` that is `100vh` tall and `sticky` at `top: 0`.
|
|
561
|
+
|
|
562
|
+
3. **Canvas** — The `<canvas>` sits inside the sticky wrapper.
|
|
563
|
+
|
|
564
|
+
4. **Math** — As you scroll the container, the sticky wrapper stays pinned to the viewport. We calculate:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
progress = -containerRect.top / (containerHeight - viewportHeight)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
This gives a precise **0.0 to 1.0** value tied to the pixel position of the scrollbar. This value is then mapped to the corresponding frame index:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
frameIndex = Math.floor(progress * (totalFrames - 1))
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
This approach ensures:
|
|
577
|
+
* **Zero Jitter**: The canvas position is handled by the browser's compositor thread (CSS Sticky).
|
|
578
|
+
* **Native Feel**: Momentum scrolling works perfectly on touchpads and mobile.
|
|
579
|
+
* **Exact Sync**: The frame updates are synchronized with the scroll position in a `requestAnimationFrame` loop.
|
|
580
|
+
|
|
581
|
+
<br />
|
|
582
|
+
|
|
583
|
+
### 💡 Memory Strategy
|
|
377
584
|
|
|
378
|
-
|
|
585
|
+
- **"eager" (Default)** — Best for sequences < 200 frames. Preloads all images into `HTMLImageElement` instances. Instant seeking, smooth playback. High memory usage.
|
|
379
586
|
|
|
380
|
-
-
|
|
381
|
-
-
|
|
382
|
-
|
|
587
|
+
- **"lazy"** — Best for long sequences (500+ frames). Only keeps the current frame and its neighbors in memory. Saves RAM, prevents crashes.
|
|
588
|
+
- Buffer size defaults to ±10 frames but can be customized via `lazyBuffer`.
|
|
589
|
+
|
|
590
|
+
<br />
|
|
383
591
|
|
|
384
592
|
---
|
|
385
593
|
|
|
@@ -388,17 +596,37 @@ Unlike libraries that use `position: fixed` or JS-based scroll locking (which br
|
|
|
388
596
|
Enable the debug overlay to inspect your sequence in production:
|
|
389
597
|
|
|
390
598
|
```tsx
|
|
391
|
-
<ScrollSequence
|
|
599
|
+
<ScrollSequence
|
|
600
|
+
source={...}
|
|
601
|
+
debug={true}
|
|
602
|
+
/>
|
|
392
603
|
```
|
|
393
604
|
|
|
605
|
+
<br />
|
|
606
|
+
|
|
394
607
|
**Output:**
|
|
395
608
|
|
|
396
609
|
```
|
|
397
|
-
Progress: 0.
|
|
610
|
+
Progress: 0.45
|
|
611
|
+
Frame: 45 / 100
|
|
398
612
|
```
|
|
399
613
|
|
|
400
|
-
This overlay is updated directly via DOM manipulation (bypassing React renders) for zero overhead
|
|
614
|
+
This overlay is updated directly via DOM manipulation (bypassing React renders) for **zero overhead**.
|
|
615
|
+
|
|
616
|
+
<br />
|
|
401
617
|
|
|
402
618
|
---
|
|
403
619
|
|
|
404
|
-
|
|
620
|
+
<div align="center">
|
|
621
|
+
|
|
622
|
+
## 📄 License
|
|
623
|
+
|
|
624
|
+
**MIT** © 2026 **Thanniru Sai Teja**
|
|
625
|
+
|
|
626
|
+
<br />
|
|
627
|
+
|
|
628
|
+
Made with ❤️ for the React community
|
|
629
|
+
|
|
630
|
+
[⬆ Back to Top](#-react-scroll-media)
|
|
631
|
+
|
|
632
|
+
</div>
|