rune-scroller 2.2.1 → 3.0.0
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 +208 -477
- package/package.json +6 -3
- package/dist/animations.css +0 -83
- package/dist/animations.d.ts +0 -18
- package/dist/animations.js +0 -38
- package/dist/dom-utils.d.ts +0 -30
- package/dist/dom-utils.js +0 -112
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -23
- package/dist/observer-utils.d.ts +0 -21
- package/dist/observer-utils.js +0 -31
- package/dist/runeScroller.d.ts +0 -9
- package/dist/runeScroller.js +0 -166
- package/dist/types.d.ts +0 -78
- package/dist/types.js +0 -48
- package/dist/useIntersection.svelte.d.ts +0 -11
- package/dist/useIntersection.svelte.js +0 -90
package/README.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
# ⚡ Rune Scroller
|
|
2
|
-
|
|
3
|
-
**📚 Complete API Reference** — Detailed documentation for all features and options.
|
|
4
|
-
|
|
5
|
-
---
|
|
1
|
+
# ⚡ Rune Scroller
|
|
6
2
|
|
|
7
3
|
<div align="center">
|
|
8
4
|
<img src="./logo.png" alt="Rune Scroller Logo" width="200" />
|
|
9
5
|
</div>
|
|
10
6
|
|
|
11
|
-
**Lightweight scroll animations
|
|
7
|
+
**Lightweight scroll animations. AOS replacement. Works everywhere.**
|
|
8
|
+
|
|
9
|
+
Built with native IntersectionObserver — zero JS on scroll, GPU-accelerated, ~5.6KB gzipped.
|
|
12
10
|
|
|
13
11
|
> 🚀 **Open Source** by [ludoloops](https://github.com/ludoloops) at [LeLab.dev](https://lelab.dev)
|
|
14
12
|
> 📜 Licensed under **MIT**
|
|
@@ -24,517 +22,263 @@
|
|
|
24
22
|
|
|
25
23
|
---
|
|
26
24
|
|
|
27
|
-
##
|
|
25
|
+
## 🚀 Quick Start
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
- **Zero dependencies** - Pure Svelte 5 + IntersectionObserver
|
|
31
|
-
- **14 animations** - Fade, Zoom, Flip, Slide, Bounce variants
|
|
32
|
-
- **Full TypeScript support** - Type definitions generated from JSDoc
|
|
33
|
-
- **SSR-ready** - SvelteKit compatible
|
|
34
|
-
- **GPU-accelerated** - Pure CSS transforms
|
|
35
|
-
- **Accessible** - Respects `prefers-reduced-motion`
|
|
36
|
-
- **v2.0.0 New** - `onVisible` callback, ResizeObserver support, animation validation, sentinel customization
|
|
37
|
-
- **✨ Latest** - `useIntersection` migrated to Svelte 5 `$effect` rune for better lifecycle management
|
|
38
|
-
- **🚀 Bundle optimized** - CSS with custom properties, production build minification
|
|
39
|
-
- **🚀 v2.2.0** - Cache CSS check to eliminate 99% of reflows
|
|
27
|
+
### Any framework — Svelte, React, Vue, Angular, Vanilla JS
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## 🚀 Performance
|
|
44
|
-
|
|
45
|
-
### Cache CSS Validation (v2.2.0)
|
|
46
|
-
|
|
47
|
-
**Problem:**
|
|
48
|
-
- `checkAndWarnIfCSSNotLoaded()` was called for EVERY element
|
|
49
|
-
- Each call did:
|
|
50
|
-
- `document.createElement('div')`
|
|
51
|
-
- `document.body.appendChild(test)`
|
|
52
|
-
- `getComputedStyle(test)` ⚠️ **Expensive!** Forces full page reflow
|
|
53
|
-
- `test.remove()`
|
|
54
|
-
- For 100 animated elements = **100 reflows + 100 DOM operations**
|
|
55
|
-
|
|
56
|
-
**Solution:**
|
|
57
|
-
```javascript
|
|
58
|
-
// Cache to check only once per page load
|
|
59
|
-
let cssCheckResult = null;
|
|
60
|
-
|
|
61
|
-
export function checkAndWarnIfCSSNotLoaded() {
|
|
62
|
-
if (cssCheckResult !== null) return cssCheckResult;
|
|
63
|
-
// ... expensive check ...
|
|
64
|
-
cssCheckResult = hasAnimation;
|
|
65
|
-
return hasAnimation;
|
|
66
|
-
}
|
|
29
|
+
```bash
|
|
30
|
+
npm install rune-scroller
|
|
67
31
|
```
|
|
68
32
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- Zero memory overhead (single boolean)
|
|
74
|
-
|
|
75
|
-
### Current Performance Metrics
|
|
76
|
-
|
|
77
|
-
| Metric | Value |
|
|
78
|
-
|---------|--------|
|
|
79
|
-
| **Bundle size** | 12.4KB (compressed), 40.3KB (unpacked) |
|
|
80
|
-
| **Initialization** | ~1-2ms per element |
|
|
81
|
-
| **Observer callback** | <0.5ms per frame |
|
|
82
|
-
| **CSS validation** | ~0.1ms total (v2.2.0, with cache) |
|
|
83
|
-
| **Memory per observer** | ~1.2KB |
|
|
84
|
-
| **Animation performance** | 60fps maintained |
|
|
85
|
-
| **Memory leaks** | 0 detected |
|
|
86
|
-
|
|
87
|
-
### Why Performance Matters
|
|
88
|
-
|
|
89
|
-
**Layout Thrashing:**
|
|
90
|
-
- Synchronous reflows block the main thread
|
|
91
|
-
- Each reflow can take 10-20ms
|
|
92
|
-
- For 100 elements = **1-2 seconds blocked**
|
|
93
|
-
- User sees stuttering/jank while scrolling
|
|
94
|
-
|
|
95
|
-
**Solution:**
|
|
96
|
-
- Cache = 1 reflow instead of N
|
|
97
|
-
- 99% improvement on pages with many animations
|
|
98
|
-
- Smoother scrolling, better UX
|
|
99
|
-
|
|
100
|
-
### Optimized Code Patterns
|
|
101
|
-
|
|
102
|
-
**IntersectionObserver:**
|
|
103
|
-
- Native API (no scroll listeners)
|
|
104
|
-
- Fast callback (<0.5ms per frame)
|
|
105
|
-
- No debounce needed (browser handles this efficiently)
|
|
106
|
-
|
|
107
|
-
**CSS Animations:**
|
|
108
|
-
- Transforms only (GPU-accelerated)
|
|
109
|
-
- No layout/repaint during animation
|
|
110
|
-
- `will-change` on visible elements only
|
|
111
|
-
|
|
112
|
-
**DOM Operations:**
|
|
113
|
-
- `insertAdjacentElement('beforebegin')` instead of `insertBefore`
|
|
114
|
-
- `offsetHeight` instead of `getBoundingClientRect()` (avoids transform issues)
|
|
115
|
-
- Complete cleanup on destroy
|
|
116
|
-
|
|
117
|
-
**Memory Management:**
|
|
118
|
-
- All observers disconnected
|
|
119
|
-
- Sentinel and wrapper removed
|
|
120
|
-
- State prevents double-disconnects
|
|
121
|
-
- 0 memory leaks detected (121/121 tests)
|
|
122
|
-
|
|
123
|
-
### Future Considerations
|
|
124
|
-
|
|
125
|
-
**1. `will-change` Timing**
|
|
126
|
-
- Currently: `.is-visible { will-change: transform, opacity; }`
|
|
127
|
-
- Trade-off: Stays active after animation (consumes GPU memory)
|
|
128
|
-
- Consideration: Use `transitionend` event to remove `will-change`
|
|
129
|
-
- Recommendation: Keep current (GPU memory is cheap)
|
|
130
|
-
|
|
131
|
-
**2. Threshold Tuning**
|
|
132
|
-
- Current: `threshold: 0` (triggers as soon as 1px is visible)
|
|
133
|
-
- Alternative: `threshold: 0.1` or `threshold: 0.25`
|
|
134
|
-
- Trade-off: Higher threshold = later trigger = smoother stagger
|
|
135
|
-
- Recommendation: Keep `threshold: 0` for immediate feedback
|
|
136
|
-
|
|
137
|
-
**3. requestIdleCallback**
|
|
138
|
-
- Potential: Defer non-critical setup to browser idle time
|
|
139
|
-
- Trade-off: Complex to implement, marginal benefit
|
|
140
|
-
- Recommendation: Not needed (current performance is excellent)
|
|
141
|
-
|
|
142
|
-
**4. Testing on Low-End Devices**
|
|
143
|
-
- Test on mobile phones, older browsers
|
|
144
|
-
- Use DevTools CPU throttling
|
|
145
|
-
- Consider Lighthouse/Puppeteer for automated testing
|
|
146
|
-
- Ensure 60fps maintained on real devices
|
|
147
|
-
|
|
148
|
-
### What NOT to Optimize
|
|
149
|
-
|
|
150
|
-
**Anti-patterns to avoid:**
|
|
151
|
-
|
|
152
|
-
1. ❌ **Premature optimization**
|
|
153
|
-
- Don't optimize without measurements
|
|
154
|
-
- Profile first, optimize later
|
|
155
|
-
- "Premature optimization is the root of all evil"
|
|
156
|
-
|
|
157
|
-
2. ❌ **Over-engineering**
|
|
158
|
-
- Complex solutions for small gains
|
|
159
|
-
- Keep it simple when possible
|
|
160
|
-
- Don't sacrifice readability for micro-optimizations
|
|
161
|
-
|
|
162
|
-
3. ❌ **Breaking performance for size**
|
|
163
|
-
- Bundle size matters (12.4KB is excellent)
|
|
164
|
-
- Don't add huge dependencies for minor improvements
|
|
165
|
-
|
|
166
|
-
4. ❌ **Optimizing unused paths**
|
|
167
|
-
- Focus on hot paths (element creation, scroll, intersection)
|
|
168
|
-
- Cold paths (initialization, destroy) less critical
|
|
169
|
-
|
|
170
|
-
5. ❌ **Sacrificing maintainability**
|
|
171
|
-
- Don't sacrifice code clarity for micro-optimizations
|
|
172
|
-
- Comments should explain WHY, not just WHAT
|
|
173
|
-
- Keep code simple and understandable
|
|
174
|
-
|
|
175
|
-
### Performance Testing
|
|
176
|
-
|
|
177
|
-
**Recommended approach:**
|
|
178
|
-
1. Create a benchmark with 100-1000 animated elements
|
|
179
|
-
2. Measure: initialization, first animation, scroll performance, cleanup
|
|
180
|
-
3. Profile with DevTools Performance tab
|
|
181
|
-
4. Test on real pages (not just benchmarks)
|
|
182
|
-
5. Verify 60fps is maintained during scroll
|
|
183
|
-
|
|
184
|
-
**Tools:**
|
|
185
|
-
- Chrome DevTools Performance tab
|
|
186
|
-
- Firefox Performance Profiler
|
|
187
|
-
- Web Inspector (Safari)
|
|
188
|
-
- Lighthouse (PageSpeed, accessibility, best practices)
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## 📦 Installation
|
|
33
|
+
```js
|
|
34
|
+
import AOS from "rune-scroller/aos";
|
|
35
|
+
AOS.init();
|
|
36
|
+
```
|
|
193
37
|
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
pnpm add rune-scroller
|
|
198
|
-
# or
|
|
199
|
-
yarn add rune-scroller
|
|
38
|
+
```html
|
|
39
|
+
<div data-aos="fade-up" data-aos-duration="800">Animated</div>
|
|
40
|
+
<div data-aos="zoom-in" data-aos-delay="200">Delayed zoom</div>
|
|
200
41
|
```
|
|
201
42
|
|
|
202
|
-
|
|
43
|
+
That's it. Same API as AOS. Works everywhere.
|
|
203
44
|
|
|
204
|
-
|
|
45
|
+
### Svelte (native action)
|
|
205
46
|
|
|
206
47
|
```svelte
|
|
207
48
|
<script>
|
|
208
|
-
import
|
|
49
|
+
import rs from 'rune-scroller';
|
|
209
50
|
</script>
|
|
210
51
|
|
|
211
|
-
|
|
212
|
-
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
213
|
-
<h2>Animated Heading</h2>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<!-- With custom duration -->
|
|
217
|
-
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
218
|
-
<div class="card">Smooth fade and slide</div>
|
|
219
|
-
</div>
|
|
220
|
-
|
|
221
|
-
<!-- Repeat on every scroll -->
|
|
222
|
-
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
223
|
-
<button>Bounces on every scroll</button>
|
|
224
|
-
</div>
|
|
52
|
+
<div use:rs={{ animation: 'fade-up' }}>Animates on scroll</div>
|
|
225
53
|
```
|
|
226
54
|
|
|
227
|
-
|
|
55
|
+
### React
|
|
228
56
|
|
|
229
|
-
|
|
57
|
+
````jsx
|
|
58
|
+
import { useEffect } from "react";
|
|
59
|
+
### React (not tested — should work)
|
|
230
60
|
|
|
231
|
-
|
|
61
|
+
```jsx
|
|
62
|
+
import { useEffect } from 'react';
|
|
63
|
+
import AOS from 'rune-scroller/aos';
|
|
232
64
|
|
|
233
|
-
|
|
65
|
+
function App() {
|
|
66
|
+
useEffect(() => { AOS.init(); }, []);
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<h1 data-aos="fade-down">Welcome</h1>
|
|
70
|
+
<p data-aos="fade-up" data-aos-delay="200">Subtitle</p>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
````
|
|
234
75
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
76
|
+
### Vue (not tested — should work)
|
|
77
|
+
|
|
78
|
+
```vue
|
|
79
|
+
<script setup>
|
|
80
|
+
import { onMounted } from "vue";
|
|
81
|
+
import AOS from "rune-scroller/aos";
|
|
82
|
+
onMounted(() => AOS.init());
|
|
240
83
|
</script>
|
|
241
84
|
|
|
242
|
-
|
|
85
|
+
<template>
|
|
86
|
+
<div data-aos="fade-up">Animated</div>
|
|
87
|
+
</template>
|
|
243
88
|
```
|
|
244
89
|
|
|
245
|
-
|
|
90
|
+
### Angular (not tested — should work)
|
|
246
91
|
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
92
|
+
```typescript
|
|
93
|
+
// app.component.ts
|
|
94
|
+
import { Component, OnInit } from "@angular/core";
|
|
95
|
+
import AOS from "rune-scroller/aos";
|
|
96
|
+
|
|
97
|
+
@Component({ selector: "app-root", templateUrl: "./app.component.html" })
|
|
98
|
+
export class AppComponent implements OnInit {
|
|
99
|
+
ngOnInit() {
|
|
100
|
+
AOS.init();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
252
103
|
```
|
|
253
104
|
|
|
254
|
-
|
|
105
|
+
```html
|
|
106
|
+
<!-- app.component.html -->
|
|
107
|
+
<div data-aos="fade-up">Animated</div>
|
|
108
|
+
```
|
|
255
109
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
110
|
+
### CDN (not tested — should work)
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<script type="module">
|
|
114
|
+
import AOS from "https://esm.sh/rune-scroller/aos";
|
|
115
|
+
AOS.init();
|
|
260
116
|
</script>
|
|
261
117
|
|
|
262
|
-
<div
|
|
263
|
-
Animated content
|
|
264
|
-
</div>
|
|
118
|
+
<div data-aos="fade-up">Works without any build step</div>
|
|
265
119
|
```
|
|
266
120
|
|
|
267
121
|
---
|
|
268
122
|
|
|
269
|
-
##
|
|
270
|
-
|
|
271
|
-
### Fade (5)
|
|
272
|
-
- `fade-in` - Simple opacity fade
|
|
273
|
-
- `fade-in-up` - Fade + move up 300px
|
|
274
|
-
- `fade-in-down` - Fade + move down 300px
|
|
275
|
-
- `fade-in-left` - Fade + move from right 300px
|
|
276
|
-
- `fade-in-right` - Fade + move from left 300px
|
|
277
|
-
|
|
278
|
-
### Zoom (5)
|
|
279
|
-
- `zoom-in` - Scale from 0.3 to 1
|
|
280
|
-
- `zoom-out` - Scale from 2 to 1
|
|
281
|
-
- `zoom-in-up` - Zoom (0.5→1) + move up 300px
|
|
282
|
-
- `zoom-in-left` - Zoom (0.5→1) + move from right 300px
|
|
283
|
-
- `zoom-in-right` - Zoom (0.5→1) + move from left 300px
|
|
123
|
+
## ✨ Features
|
|
284
124
|
|
|
285
|
-
|
|
286
|
-
- `
|
|
287
|
-
-
|
|
288
|
-
-
|
|
289
|
-
-
|
|
125
|
+
- **Framework agnostic** — Svelte, React, Vue, Angular, Vanilla JS, CDN
|
|
126
|
+
- **AOS drop-in** — Same `data-aos` attributes, same `init()` API
|
|
127
|
+
- **Zero dependencies** — Pure JS + native IntersectionObserver
|
|
128
|
+
- **~5.6KB gzipped** — Smaller than AOS (6.9KB)
|
|
129
|
+
- **37 animations** — Fade, Zoom, Flip, Slide, Bounce
|
|
130
|
+
- **Zero JS on scroll** — Browser handles detection natively
|
|
131
|
+
- **TypeScript support** — Full type definitions
|
|
132
|
+
- **SSR-ready** — SvelteKit, Next.js, Nuxt compatible
|
|
133
|
+
- **GPU-accelerated** — CSS transforms via `translate3d`
|
|
134
|
+
- **Accessible** — Respects `prefers-reduced-motion`
|
|
135
|
+
- **No wrapper divs** — Your layouts stay intact
|
|
290
136
|
|
|
291
137
|
---
|
|
292
138
|
|
|
293
|
-
|
|
139
|
+
### AOS vs rune-scroller
|
|
294
140
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
```
|
|
141
|
+
| | rune-scroller | AOS |
|
|
142
|
+
| ------------------------- | ---------------------------------------------- | ------------------------------------------ |
|
|
143
|
+
| **Bundle size (gzipped)** | **~5.6KB** JS+CSS | ~6.9KB JS+CSS |
|
|
144
|
+
| **Dependencies** | **0** | lodash.throttle, lodash.debounce |
|
|
145
|
+
| **Scroll detection** | **IntersectionObserver** (native, C++) | Scroll event + throttle (JS) |
|
|
146
|
+
| **Per-scroll cost** | **0** — browser handles it | Iterates ALL elements every 99ms |
|
|
147
|
+
| **Layout reads** | **1 per element** (init only) | `offsetParent` loop per element per scroll |
|
|
148
|
+
| **Resize handling** | **ResizeObserver** (native) | debounced scroll recalc |
|
|
149
|
+
| **100 animated elements** | **~0ms per scroll** | ~2-5ms per scroll (layout thrashing) |
|
|
150
|
+
| **Animations** | 30 | 28 |
|
|
151
|
+
| **Framework** | **Any** (Svelte, React, Vue, Angular, Vanilla) | Vanilla JS only |
|
|
307
152
|
|
|
308
|
-
|
|
153
|
+
The key difference: **AOS runs JavaScript on every scroll event** for every element. rune-scroller delegates detection to the browser's native IntersectionObserver — zero JS execution until an element actually enters the viewport.
|
|
309
154
|
|
|
310
|
-
|
|
311
|
-
- **`duration`** - How long the animation lasts in milliseconds (default: 800ms).
|
|
312
|
-
- **`repeat`** - If `true`, animation plays every time sentinel enters viewport. If `false`, plays only once.
|
|
313
|
-
- **`debug`** - If `true`, displays the sentinel element as a visible line below your element. Useful for seeing exactly when animations trigger. Default color is cyan (#00e0ff), customize with `sentinelColor`.
|
|
314
|
-
- **`offset`** - Offset of the sentinel in pixels. Positive values move sentinel down (delays animation), negative values move it up (triggers earlier). Useful for large elements where you want animation to trigger before the entire element is visible.
|
|
315
|
-
- **`onVisible`** *(v2.0.0+)* - Callback function triggered when the animation becomes visible. Receives the animated element as parameter. Useful for analytics, lazy loading, or triggering custom effects.
|
|
316
|
-
- **`sentinelColor`** *(v2.0.0+)* - Customize the debug sentinel color (e.g., '#ff6b6b' for red). Only visible when `debug: true`. Useful for distinguishing multiple sentinels on the same page.
|
|
317
|
-
- **`sentinelId`** *(v2.0.0+)* - Set a custom ID for the sentinel element. If not provided, an auto-ID is generated (`sentinel-1`, `sentinel-2`, etc.). Useful for identifying sentinels in DevTools and tracking which element owns which sentinel.
|
|
155
|
+
---
|
|
318
156
|
|
|
319
|
-
|
|
157
|
+
## 🎨 Available Animations (30)
|
|
320
158
|
|
|
321
|
-
|
|
322
|
-
<!-- Basic -->
|
|
323
|
-
<div use:runeScroller={{ animation: 'zoom-in' }}>
|
|
324
|
-
Content
|
|
325
|
-
</div>
|
|
159
|
+
### Fade (10)
|
|
326
160
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
</div>
|
|
161
|
+
- `fade` — Simple opacity fade
|
|
162
|
+
- `fade-up` / `fade-down` / `fade-left` / `fade-right` — Fade + translate
|
|
163
|
+
- `fade-up-right` / `fade-up-left` / `fade-down-right` / `fade-down-left` — Diagonal fades
|
|
331
164
|
|
|
332
|
-
|
|
333
|
-
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
334
|
-
Repeats every time you scroll
|
|
335
|
-
</div>
|
|
165
|
+
### Zoom (10)
|
|
336
166
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
</div>
|
|
167
|
+
- `zoom-in` / `zoom-out` — Scale in/out
|
|
168
|
+
- `zoom-in-up` / `zoom-in-down` / `zoom-in-left` / `zoom-in-right` — Zoom + translate
|
|
169
|
+
- `zoom-out-up` / `zoom-out-down` / `zoom-out-left` / `zoom-out-right` — Zoom out + translate
|
|
341
170
|
|
|
342
|
-
|
|
343
|
-
<div use:runeScroller={{
|
|
344
|
-
animation: 'fade-in-up',
|
|
345
|
-
duration: 1200,
|
|
346
|
-
repeat: true,
|
|
347
|
-
debug: true
|
|
348
|
-
}}>
|
|
349
|
-
Full featured example
|
|
350
|
-
</div>
|
|
171
|
+
### Slide (4)
|
|
351
172
|
|
|
352
|
-
|
|
353
|
-
<div use:runeScroller={{
|
|
354
|
-
animation: 'fade-in-up',
|
|
355
|
-
offset: -200 // Trigger 200px before element bottom
|
|
356
|
-
}}>
|
|
357
|
-
Large content that needs early triggering
|
|
358
|
-
</div>
|
|
173
|
+
- `slide-up` / `slide-down` / `slide-left` / `slide-right` — Slide from off-screen
|
|
359
174
|
|
|
360
|
-
|
|
361
|
-
<div use:runeScroller={{
|
|
362
|
-
animation: 'zoom-in',
|
|
363
|
-
offset: 300 // Trigger 300px after element bottom
|
|
364
|
-
}}>
|
|
365
|
-
Content with delayed animation
|
|
366
|
-
</div>
|
|
175
|
+
### Flip (4)
|
|
367
176
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
animation: 'fade-in-up',
|
|
371
|
-
onVisible: (el) => {
|
|
372
|
-
console.log('Animation visible!', el);
|
|
373
|
-
// Track analytics, load images, trigger API calls, etc.
|
|
374
|
-
window.gtag?.('event', 'animation_visible', { element: el.id });
|
|
375
|
-
}
|
|
376
|
-
}}>
|
|
377
|
-
Tracked animation
|
|
378
|
-
</div>
|
|
177
|
+
- `flip-left` / `flip-right` — 3D flip on Y-axis
|
|
178
|
+
- `flip-up` / `flip-down` — 3D flip on X-axis
|
|
379
179
|
|
|
380
|
-
|
|
381
|
-
<div use:runeScroller={{
|
|
382
|
-
animation: 'fade-in',
|
|
383
|
-
debug: true,
|
|
384
|
-
sentinelColor: '#ff6b6b' // Red instead of default cyan
|
|
385
|
-
}}>
|
|
386
|
-
Red debug sentinel
|
|
387
|
-
</div>
|
|
180
|
+
### Special (2)
|
|
388
181
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
animation: 'zoom-in',
|
|
392
|
-
sentinelId: 'hero-zoom',
|
|
393
|
-
debug: true
|
|
394
|
-
}}>
|
|
395
|
-
Identified sentinel (shows "hero-zoom" in debug mode)
|
|
396
|
-
</div>
|
|
182
|
+
- `slide-rotate` — Slide + rotate
|
|
183
|
+
- `bounce-in` — Bouncy spring entrance
|
|
397
184
|
|
|
398
|
-
|
|
399
|
-
<div use:runeScroller={{
|
|
400
|
-
animation: 'fade-in-up',
|
|
401
|
-
debug: true
|
|
402
|
-
// sentinelId omitted → auto generates "sentinel-1", "sentinel-2", etc
|
|
403
|
-
}}>
|
|
404
|
-
Auto-identified sentinel
|
|
405
|
-
</div>
|
|
406
|
-
```
|
|
185
|
+
### Customizable distance
|
|
407
186
|
|
|
408
|
-
|
|
187
|
+
All animations use the `--rs-distance` CSS variable (default: `100px`):
|
|
409
188
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
1. An invisible 1px sentinel element is created below your element
|
|
415
|
-
2. When the sentinel enters the viewport, animation triggers
|
|
416
|
-
3. This ensures precise timing regardless of element size
|
|
417
|
-
4. Uses native IntersectionObserver for performance
|
|
418
|
-
5. Pure CSS animations (GPU-accelerated)
|
|
419
|
-
6. *(v2.0.0)* Sentinel automatically repositions on element resize via ResizeObserver
|
|
420
|
-
|
|
421
|
-
**Why sentinels?**
|
|
422
|
-
- Accurate timing across all screen sizes
|
|
423
|
-
- No complex offset calculations
|
|
424
|
-
- Handles staggered animations naturally
|
|
425
|
-
- Sentinel stays fixed while element animates (no observer confusion with transforms)
|
|
426
|
-
|
|
427
|
-
**Automatic ResizeObserver** *(v2.0.0+)*
|
|
428
|
-
- Sentinel repositions automatically when element resizes
|
|
429
|
-
- Works with responsive layouts and dynamic content
|
|
430
|
-
- No configuration needed—it just works
|
|
189
|
+
```html
|
|
190
|
+
<div data-aos="fade-up" style="--rs-distance: 200px">Farther slide</div>
|
|
191
|
+
```
|
|
431
192
|
|
|
432
193
|
---
|
|
433
194
|
|
|
434
|
-
##
|
|
195
|
+
## ⚙️ Options
|
|
435
196
|
|
|
436
|
-
|
|
197
|
+
### AOS Mode (data attributes)
|
|
198
|
+
|
|
199
|
+
| Attribute | Example | Description |
|
|
200
|
+
| ------------------- | --------------- | -------------------------- |
|
|
201
|
+
| `data-aos` | `"fade-up"` | Animation name |
|
|
202
|
+
| `data-aos-duration` | `"800"` | Duration in ms |
|
|
203
|
+
| `data-aos-delay` | `"200"` | Delay in ms |
|
|
204
|
+
| `data-aos-easing` | `"ease-in-out"` | CSS timing function |
|
|
205
|
+
| `data-aos-offset` | `"120"` | Trigger offset in px |
|
|
206
|
+
| `data-aos-once` | `"true"` | Animate only once |
|
|
207
|
+
| `data-aos-mirror` | `"true"` | Animate on scroll away too |
|
|
208
|
+
|
|
209
|
+
### AOS init options
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
AOS.init({
|
|
213
|
+
offset: 120,
|
|
214
|
+
duration: 400,
|
|
215
|
+
delay: 0,
|
|
216
|
+
easing: "ease",
|
|
217
|
+
once: false,
|
|
218
|
+
mirror: false,
|
|
219
|
+
startEvent: "DOMContentLoaded",
|
|
220
|
+
});
|
|
221
|
+
```
|
|
437
222
|
|
|
438
|
-
|
|
439
|
-
<!-- src/routes/+layout.svelte -->
|
|
440
|
-
<script>
|
|
441
|
-
import runeScroller from 'rune-scroller';
|
|
442
|
-
let { children } = $props();
|
|
443
|
-
</script>
|
|
223
|
+
### Svelte Action options
|
|
444
224
|
|
|
445
|
-
|
|
225
|
+
```typescript
|
|
226
|
+
interface RuneScrollerOptions {
|
|
227
|
+
animation?: AnimationType; // default: 'fade-up'
|
|
228
|
+
duration?: number; // default: 400
|
|
229
|
+
delay?: number; // default: 0
|
|
230
|
+
easing?: string; // default: 'ease'
|
|
231
|
+
repeat?: boolean; // default: false
|
|
232
|
+
debug?: boolean;
|
|
233
|
+
offset?: number; // negative = earlier trigger
|
|
234
|
+
onVisible?: (el: HTMLElement) => void;
|
|
235
|
+
sentinelColor?: string;
|
|
236
|
+
sentinelId?: string;
|
|
237
|
+
}
|
|
446
238
|
```
|
|
447
239
|
|
|
448
|
-
|
|
240
|
+
---
|
|
449
241
|
|
|
450
|
-
|
|
451
|
-
<!-- src/routes/+page.svelte -->
|
|
452
|
-
<script>
|
|
453
|
-
import runeScroller from 'rune-scroller';
|
|
454
|
-
</script>
|
|
242
|
+
## 🎯 How It Works
|
|
455
243
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
```
|
|
244
|
+
1. Invisible 1px sentinel appended as child of the animated element
|
|
245
|
+
2. When sentinel enters viewport, animation triggers via IntersectionObserver
|
|
246
|
+
3. Pure CSS transitions (GPU-accelerated via `translate3d`)
|
|
247
|
+
4. ResizeObserver auto-repositions sentinel
|
|
461
248
|
|
|
462
|
-
|
|
249
|
+
**No wrapper divs** — the element itself becomes the positioning context. Your flex/grid layouts stay intact.
|
|
463
250
|
|
|
464
251
|
---
|
|
465
252
|
|
|
466
253
|
## ♿ Accessibility
|
|
467
254
|
|
|
468
|
-
Respects `prefers-reduced-motion
|
|
469
|
-
|
|
470
|
-
```css
|
|
471
|
-
/* In animations.css */
|
|
472
|
-
@media (prefers-reduced-motion: reduce) {
|
|
473
|
-
.scroll-animate {
|
|
474
|
-
animation: none !important;
|
|
475
|
-
opacity: 1 !important;
|
|
476
|
-
transform: none !important;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
Users who prefer reduced motion will see content without animations.
|
|
255
|
+
Respects `prefers-reduced-motion` — animations are disabled automatically.
|
|
482
256
|
|
|
483
257
|
---
|
|
484
258
|
|
|
485
259
|
## 📚 API Reference
|
|
486
|
-
### Public API
|
|
487
|
-
|
|
488
|
-
Rune Scroller exports a **single action-based API** (no components):
|
|
489
|
-
|
|
490
|
-
1. **`runeScroller`** (default) - Sentinel-based, simple, powerful
|
|
491
|
-
|
|
492
|
-
**Why actions instead of components?**
|
|
493
|
-
- Actions are lightweight directives
|
|
494
|
-
- No DOM wrapper overhead
|
|
495
|
-
- Better performance
|
|
496
|
-
- More flexible
|
|
497
|
-
|
|
498
|
-
### Main Export
|
|
499
260
|
|
|
500
261
|
```typescript
|
|
501
|
-
//
|
|
502
|
-
import
|
|
262
|
+
// Framework agnostic (AOS mode)
|
|
263
|
+
import AOS from "rune-scroller/aos";
|
|
264
|
+
AOS.init();
|
|
265
|
+
AOS.refresh();
|
|
266
|
+
AOS.refreshHard();
|
|
267
|
+
|
|
268
|
+
// Svelte action (default)
|
|
269
|
+
import rs from "rune-scroller";
|
|
503
270
|
|
|
504
271
|
// Named exports
|
|
505
272
|
import {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
273
|
+
runeScroller,
|
|
274
|
+
useIntersection,
|
|
275
|
+
useIntersectionOnce,
|
|
276
|
+
calculateRootMargin,
|
|
277
|
+
ANIMATION_TYPES,
|
|
278
|
+
} from "rune-scroller";
|
|
510
279
|
|
|
511
280
|
// Types
|
|
512
|
-
import type {
|
|
513
|
-
AnimationType,
|
|
514
|
-
RuneScrollerOptions,
|
|
515
|
-
IntersectionOptions,
|
|
516
|
-
UseIntersectionReturn
|
|
517
|
-
} from 'rune-scroller';
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### TypeScript Types
|
|
521
|
-
|
|
522
|
-
```typescript
|
|
523
|
-
type AnimationType =
|
|
524
|
-
| 'fade-in' | 'fade-in-up' | 'fade-in-down' | 'fade-in-left' | 'fade-in-right'
|
|
525
|
-
| 'zoom-in' | 'zoom-out' | 'zoom-in-up' | 'zoom-in-left' | 'zoom-in-right'
|
|
526
|
-
| 'flip' | 'flip-x' | 'slide-rotate' | 'bounce-in';
|
|
527
|
-
|
|
528
|
-
interface RuneScrollerOptions {
|
|
529
|
-
animation?: AnimationType;
|
|
530
|
-
duration?: number;
|
|
531
|
-
repeat?: boolean;
|
|
532
|
-
debug?: boolean;
|
|
533
|
-
offset?: number;
|
|
534
|
-
onVisible?: (element: HTMLElement) => void; // v2.0.0+
|
|
535
|
-
sentinelColor?: string; // v2.0.0+
|
|
536
|
-
sentinelId?: string; // v2.0.0+
|
|
537
|
-
}
|
|
281
|
+
import type { AnimationType, RuneScrollerOptions } from "rune-scroller";
|
|
538
282
|
```
|
|
539
283
|
|
|
540
284
|
---
|
|
@@ -545,48 +289,49 @@ interface RuneScrollerOptions {
|
|
|
545
289
|
|
|
546
290
|
```svelte
|
|
547
291
|
<script>
|
|
548
|
-
import
|
|
549
|
-
|
|
550
|
-
const items = [
|
|
551
|
-
{ title: 'Feature 1', description: 'Description 1' },
|
|
552
|
-
{ title: 'Feature 2', description: 'Description 2' },
|
|
553
|
-
{ title: 'Feature 3', description: 'Description 3' }
|
|
554
|
-
];
|
|
292
|
+
import rs from 'rune-scroller';
|
|
293
|
+
const items = ['Item 1', 'Item 2', 'Item 3'];
|
|
555
294
|
</script>
|
|
556
295
|
|
|
557
|
-
|
|
558
|
-
{
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
</div>
|
|
563
|
-
{/each}
|
|
564
|
-
</div>
|
|
296
|
+
{#each items as item, i}
|
|
297
|
+
<div use:rs={{ animation: 'fade-up', duration: 800, delay: i * 100 }}>
|
|
298
|
+
{item}
|
|
299
|
+
</div>
|
|
300
|
+
{/each}
|
|
565
301
|
```
|
|
566
302
|
|
|
567
303
|
### Hero Section
|
|
568
304
|
|
|
569
|
-
```
|
|
570
|
-
<
|
|
571
|
-
|
|
572
|
-
</
|
|
305
|
+
```html
|
|
306
|
+
<h1 data-aos="fade-down" data-aos-duration="1000">Welcome</h1>
|
|
307
|
+
<p data-aos="fade-up" data-aos-duration="1200">Subtitle</p>
|
|
308
|
+
<button data-aos="zoom-in" data-aos-duration="800">Get Started</button>
|
|
309
|
+
```
|
|
573
310
|
|
|
574
|
-
|
|
575
|
-
<p>Engaging content</p>
|
|
576
|
-
</div>
|
|
311
|
+
---
|
|
577
312
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
313
|
+
## 🔄 Replacing AOS
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm uninstall aos
|
|
317
|
+
npm install rune-scroller
|
|
581
318
|
```
|
|
582
319
|
|
|
320
|
+
```diff
|
|
321
|
+
- import AOS from 'aos';
|
|
322
|
+
- import 'aos/dist/aos.css';
|
|
323
|
+
+ import AOS from 'rune-scroller/aos';
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Everything else stays the same. Same attributes, same options.
|
|
327
|
+
|
|
583
328
|
---
|
|
584
329
|
|
|
585
330
|
## 🔗 Links
|
|
586
331
|
|
|
587
|
-
- **npm
|
|
332
|
+
- **npm**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
|
|
588
333
|
- **GitHub**: [lelabdev/rune-scroller](https://github.com/lelabdev/rune-scroller)
|
|
589
|
-
- **Changelog**: [CHANGELOG.md](
|
|
334
|
+
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
|
|
590
335
|
|
|
591
336
|
---
|
|
592
337
|
|
|
@@ -596,18 +341,4 @@ MIT © [ludoloops](https://github.com/ludoloops)
|
|
|
596
341
|
|
|
597
342
|
---
|
|
598
343
|
|
|
599
|
-
## 🤝 Contributing
|
|
600
|
-
|
|
601
|
-
Contributions welcome! Please open an issue or PR on GitHub.
|
|
602
|
-
|
|
603
|
-
```bash
|
|
604
|
-
# Development
|
|
605
|
-
bun install
|
|
606
|
-
bun run dev
|
|
607
|
-
bun test
|
|
608
|
-
bun run build
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
---
|
|
612
|
-
|
|
613
344
|
Made with ❤️ by [LeLab.dev](https://lelab.dev)
|