rune-scroller 2.2.2 → 3.0.1
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 +220 -133
- 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
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<img src="./logo.png" alt="Rune Scroller Logo" width="200" />
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
**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.
|
|
8
10
|
|
|
9
11
|
> 🚀 **Open Source** by [ludoloops](https://github.com/ludoloops) at [LeLab.dev](https://lelab.dev)
|
|
10
12
|
> 📜 Licensed under **MIT**
|
|
@@ -20,191 +22,263 @@
|
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
- **5.7KB gzipped** (JS+CSS) - Minimal overhead
|
|
26
|
-
- **Zero dependencies** - Pure Svelte 5 + IntersectionObserver
|
|
27
|
-
- **14 animations** - Fade, Zoom, Flip, Slide, Bounce
|
|
28
|
-
- **TypeScript support** - Full type definitions
|
|
29
|
-
- **SSR-ready** - SvelteKit compatible
|
|
30
|
-
- **GPU-accelerated** - Pure CSS transforms
|
|
31
|
-
- **Accessible** - Respects `prefers-reduced-motion`
|
|
32
|
-
|
|
33
|
-
---
|
|
25
|
+
## 🚀 Quick Start
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
### Any framework — Svelte, React, Vue, Angular, Vanilla JS
|
|
36
28
|
|
|
37
29
|
```bash
|
|
38
30
|
npm install rune-scroller
|
|
39
31
|
```
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
```js
|
|
34
|
+
import AOS from "rune-scroller/aos";
|
|
35
|
+
AOS.init();
|
|
36
|
+
```
|
|
42
37
|
|
|
43
|
-
|
|
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>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it. Same API as AOS. Works everywhere.
|
|
44
|
+
|
|
45
|
+
### Svelte (native action)
|
|
44
46
|
|
|
45
47
|
```svelte
|
|
46
48
|
<script>
|
|
47
|
-
import
|
|
49
|
+
import rs from 'rune-scroller';
|
|
48
50
|
</script>
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<h2>Animated Heading</h2>
|
|
53
|
-
</div>
|
|
52
|
+
<div use:rs={{ animation: 'fade-up' }}>Animates on scroll</div>
|
|
53
|
+
```
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
57
|
-
<div class="card">Smooth fade and slide</div>
|
|
58
|
-
</div>
|
|
55
|
+
### React
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
</div>
|
|
64
|
-
```
|
|
57
|
+
````jsx
|
|
58
|
+
import { useEffect } from "react";
|
|
59
|
+
### React (not tested — should work)
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
```jsx
|
|
62
|
+
import { useEffect } from 'react';
|
|
63
|
+
import AOS from 'rune-scroller/aos';
|
|
67
64
|
|
|
68
|
-
|
|
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
|
+
````
|
|
69
75
|
|
|
70
|
-
###
|
|
76
|
+
### Vue (not tested — should work)
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
```vue
|
|
79
|
+
<script setup>
|
|
80
|
+
import { onMounted } from "vue";
|
|
81
|
+
import AOS from "rune-scroller/aos";
|
|
82
|
+
onMounted(() => AOS.init());
|
|
83
|
+
</script>
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
<template>
|
|
86
|
+
<div data-aos="fade-up">Animated</div>
|
|
87
|
+
</template>
|
|
88
|
+
```
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
- `zoom-out` - Scale from 2 to 1
|
|
82
|
-
- `zoom-in-up` - Zoom (0.5→1) + move up 300px
|
|
83
|
-
- `zoom-in-left` - Zoom (0.5→1) + move from right 300px
|
|
84
|
-
- `zoom-in-right` - Zoom (0.5→1) + move from left 300px
|
|
90
|
+
### Angular (not tested — should work)
|
|
85
91
|
|
|
86
|
-
|
|
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
|
+
}
|
|
103
|
+
```
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
```html
|
|
106
|
+
<!-- app.component.html -->
|
|
107
|
+
<div data-aos="fade-up">Animated</div>
|
|
108
|
+
```
|
|
92
109
|
|
|
93
|
-
|
|
110
|
+
### CDN (not tested — should work)
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
```html
|
|
113
|
+
<script type="module">
|
|
114
|
+
import AOS from "https://esm.sh/rune-scroller/aos";
|
|
115
|
+
AOS.init();
|
|
116
|
+
</script>
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
interface RuneScrollerOptions {
|
|
99
|
-
animation?: AnimationType // Animation name (default: 'fade-in')
|
|
100
|
-
duration?: number // Duration in ms (default: 800)
|
|
101
|
-
repeat?: boolean // Repeat on scroll (default: false)
|
|
102
|
-
debug?: boolean // Show sentinel as visible line (default: false)
|
|
103
|
-
offset?: number // Sentinel offset in px (default: 0, negative = earlier)
|
|
104
|
-
onVisible?: (element: HTMLElement) => void // Callback when visible
|
|
105
|
-
sentinelColor?: string // Debug sentinel color (e.g. '#ff6b6b')
|
|
106
|
-
sentinelId?: string // Custom sentinel ID
|
|
107
|
-
}
|
|
118
|
+
<div data-aos="fade-up">Works without any build step</div>
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
---
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
<!-- Basic -->
|
|
114
|
-
<div use:runeScroller={{ animation: 'zoom-in' }}>Content</div>
|
|
123
|
+
## ✨ Features
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
118
136
|
|
|
119
|
-
|
|
120
|
-
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>Repeats</div>
|
|
137
|
+
---
|
|
121
138
|
|
|
122
|
-
|
|
123
|
-
<div use:runeScroller={{ animation: 'fade-in', debug: true }}>Debug</div>
|
|
139
|
+
### AOS vs rune-scroller
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 |
|
|
129
152
|
|
|
130
|
-
|
|
131
|
-
<div use:runeScroller={{
|
|
132
|
-
animation: 'fade-in-up',
|
|
133
|
-
onVisible: (el) => {
|
|
134
|
-
window.gtag?.('event', 'section_viewed', { id: el.id });
|
|
135
|
-
}
|
|
136
|
-
}}>
|
|
137
|
-
Tracked section
|
|
138
|
-
</div>
|
|
139
|
-
```
|
|
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.
|
|
140
154
|
|
|
141
155
|
---
|
|
142
156
|
|
|
143
|
-
##
|
|
157
|
+
## 🎨 Available Animations (30)
|
|
144
158
|
|
|
145
|
-
|
|
159
|
+
### Fade (10)
|
|
146
160
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
4. Pure CSS animations (GPU-accelerated)
|
|
151
|
-
5. ResizeObserver auto-repositions sentinel
|
|
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
|
|
152
164
|
|
|
153
|
-
|
|
165
|
+
### Zoom (10)
|
|
154
166
|
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
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
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
### Slide (4)
|
|
160
172
|
|
|
161
|
-
|
|
173
|
+
- `slide-up` / `slide-down` / `slide-left` / `slide-right` — Slide from off-screen
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
### Flip (4)
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<script>
|
|
168
|
-
import runeScroller from 'rune-scroller';
|
|
169
|
-
let { children } = $props();
|
|
170
|
-
</script>
|
|
177
|
+
- `flip-left` / `flip-right` — 3D flip on Y-axis
|
|
178
|
+
- `flip-up` / `flip-down` — 3D flip on X-axis
|
|
171
179
|
|
|
172
|
-
|
|
180
|
+
### Special (2)
|
|
181
|
+
|
|
182
|
+
- `slide-rotate` — Slide + rotate
|
|
183
|
+
- `bounce-in` — Bouncy spring entrance
|
|
184
|
+
|
|
185
|
+
### Customizable distance
|
|
186
|
+
|
|
187
|
+
All animations use the `--rs-distance` CSS variable (default: `100px`):
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<div data-aos="fade-up" style="--rs-distance: 200px">Farther slide</div>
|
|
173
191
|
```
|
|
174
192
|
|
|
175
193
|
---
|
|
176
194
|
|
|
177
|
-
##
|
|
195
|
+
## ⚙️ Options
|
|
178
196
|
|
|
179
|
-
|
|
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
|
+
```
|
|
180
222
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
### Svelte Action options
|
|
224
|
+
|
|
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;
|
|
188
237
|
}
|
|
189
238
|
```
|
|
190
239
|
|
|
191
240
|
---
|
|
192
241
|
|
|
242
|
+
## 🎯 How It Works
|
|
243
|
+
|
|
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
|
|
248
|
+
|
|
249
|
+
**No wrapper divs** — the element itself becomes the positioning context. Your flex/grid layouts stay intact.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## ♿ Accessibility
|
|
254
|
+
|
|
255
|
+
Respects `prefers-reduced-motion` — animations are disabled automatically.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
193
259
|
## 📚 API Reference
|
|
194
260
|
|
|
195
261
|
```typescript
|
|
196
|
-
//
|
|
197
|
-
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";
|
|
198
270
|
|
|
199
271
|
// Named exports
|
|
200
272
|
import {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
273
|
+
runeScroller,
|
|
274
|
+
useIntersection,
|
|
275
|
+
useIntersectionOnce,
|
|
276
|
+
calculateRootMargin,
|
|
277
|
+
ANIMATION_TYPES,
|
|
278
|
+
} from "rune-scroller";
|
|
205
279
|
|
|
206
280
|
// Types
|
|
207
|
-
import type { AnimationType, RuneScrollerOptions } from "rune-scroller"
|
|
281
|
+
import type { AnimationType, RuneScrollerOptions } from "rune-scroller";
|
|
208
282
|
```
|
|
209
283
|
|
|
210
284
|
---
|
|
@@ -215,16 +289,12 @@ import type { AnimationType, RuneScrollerOptions } from "rune-scroller"
|
|
|
215
289
|
|
|
216
290
|
```svelte
|
|
217
291
|
<script>
|
|
218
|
-
import
|
|
292
|
+
import rs from 'rune-scroller';
|
|
219
293
|
const items = ['Item 1', 'Item 2', 'Item 3'];
|
|
220
294
|
</script>
|
|
221
295
|
|
|
222
296
|
{#each items as item, i}
|
|
223
|
-
<div use:
|
|
224
|
-
animation: 'fade-in-up',
|
|
225
|
-
duration: 800,
|
|
226
|
-
style: `--delay: ${i * 100}ms`
|
|
227
|
-
}}>
|
|
297
|
+
<div use:rs={{ animation: 'fade-up', duration: 800, delay: i * 100 }}>
|
|
228
298
|
{item}
|
|
229
299
|
</div>
|
|
230
300
|
{/each}
|
|
@@ -232,19 +302,36 @@ import type { AnimationType, RuneScrollerOptions } from "rune-scroller"
|
|
|
232
302
|
|
|
233
303
|
### Hero Section
|
|
234
304
|
|
|
235
|
-
```
|
|
236
|
-
<h1
|
|
237
|
-
<p
|
|
238
|
-
<button
|
|
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>
|
|
239
309
|
```
|
|
240
310
|
|
|
241
311
|
---
|
|
242
312
|
|
|
313
|
+
## 🔄 Replacing AOS
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm uninstall aos
|
|
317
|
+
npm install rune-scroller
|
|
318
|
+
```
|
|
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
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
243
330
|
## 🔗 Links
|
|
244
331
|
|
|
245
332
|
- **npm**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
|
|
246
333
|
- **GitHub**: [lelabdev/rune-scroller](https://github.com/lelabdev/rune-scroller)
|
|
247
|
-
- **Changelog**: [CHANGELOG.md](
|
|
334
|
+
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
|
|
248
335
|
|
|
249
336
|
---
|
|
250
337
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-scroller",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Lightweight
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Lightweight scroll animations for Svelte 5. Drop-in AOS replacement. 30 animations, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"svelte": "./dist/index.js",
|
|
30
30
|
"default": "./dist/index.js"
|
|
31
31
|
},
|
|
32
|
+
"./aos": {
|
|
33
|
+
"default": "./dist/aos.js"
|
|
34
|
+
},
|
|
32
35
|
"./animations.css": "./dist/animations.css"
|
|
33
36
|
},
|
|
34
37
|
"files": [
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
"@eslint/compat": "^1.4.0",
|
|
60
63
|
"@eslint/js": "^9.36.0",
|
|
61
64
|
"@sveltejs/kit": "^2.43.2",
|
|
62
|
-
"@sveltejs/package": "^2.5.
|
|
65
|
+
"@sveltejs/package": "^2.5.7",
|
|
63
66
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
|
64
67
|
"@types/node": "^22",
|
|
65
68
|
"eslint": "^9.36.0",
|
package/dist/animations.css
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reusable scroll animation styles - Optimized with CSS custom properties
|
|
3
|
-
* Reduces bundle size while maintaining all 14 animations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/* Base animation container with optimized transitions */
|
|
7
|
-
.scroll-animate,
|
|
8
|
-
.animated-element {
|
|
9
|
-
opacity: 0;
|
|
10
|
-
transition: opacity var(--duration, 2500ms) linear var(--delay, 0ms),
|
|
11
|
-
transform var(--duration, 2500ms) cubic-bezier(0.34, 1.56, 0.64, 1) var(--delay, 0ms);
|
|
12
|
-
transform: perspective(1000px) translate(var(--tx, 0), var(--ty, 0)) scale(var(--scale, 1)) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) rotate(var(--rotate, 0deg));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.scroll-animate.is-visible,
|
|
16
|
-
.animated-element.is-visible {
|
|
17
|
-
opacity: 1 !important;
|
|
18
|
-
will-change: transform, opacity;
|
|
19
|
-
transform: perspective(1000px) translate(var(--tx, 0), var(--ty, 0)) scale(var(--scale, 1)) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) rotate(var(--rotate, 0deg));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* Initial state - transform values before animation */
|
|
23
|
-
[data-animation='fade-in'] { --tx: 0; --ty: 0; }
|
|
24
|
-
[data-animation='fade-in-up'] { --ty: var(--translate-distance, 300px); }
|
|
25
|
-
[data-animation='fade-in-down'] { --ty: calc(-1 * var(--translate-distance, 300px)); }
|
|
26
|
-
[data-animation='fade-in-left'] { --tx: calc(-1 * var(--translate-distance, 300px)); }
|
|
27
|
-
[data-animation='fade-in-right'] { --tx: var(--translate-distance, 300px); }
|
|
28
|
-
|
|
29
|
-
[data-animation='zoom-in'] { --scale: 0.3; }
|
|
30
|
-
[data-animation='zoom-out'] { --scale: 2; }
|
|
31
|
-
[data-animation='zoom-in-up'] { --scale: 0.5; --ty: var(--translate-distance, 300px); }
|
|
32
|
-
[data-animation='zoom-in-left'] { --scale: 0.5; --tx: calc(-1 * var(--translate-distance, 300px)); }
|
|
33
|
-
[data-animation='zoom-in-right'] { --scale: 0.5; --tx: var(--translate-distance, 300px); }
|
|
34
|
-
|
|
35
|
-
[data-animation='flip'] { --ry: 90deg; }
|
|
36
|
-
[data-animation='flip-x'] { --rx: 90deg; }
|
|
37
|
-
|
|
38
|
-
[data-animation='slide-rotate'] { --tx: calc(-1 * var(--translate-distance, 300px)); --rotate: -45deg; }
|
|
39
|
-
[data-animation='bounce-in'] { --scale: 0; }
|
|
40
|
-
|
|
41
|
-
/* Visible state - reset transform values to final position */
|
|
42
|
-
[data-animation='fade-in-up'].is-visible { --ty: 0; }
|
|
43
|
-
[data-animation='fade-in-down'].is-visible { --ty: 0; }
|
|
44
|
-
[data-animation='fade-in-left'].is-visible { --tx: 0; }
|
|
45
|
-
[data-animation='fade-in-right'].is-visible { --tx: 0; }
|
|
46
|
-
|
|
47
|
-
[data-animation='zoom-in'].is-visible { --scale: 1; }
|
|
48
|
-
[data-animation='zoom-out'].is-visible { --scale: 1; }
|
|
49
|
-
[data-animation='zoom-in-up'].is-visible { --scale: 1; --ty: 0; }
|
|
50
|
-
[data-animation='zoom-in-left'].is-visible { --scale: 1; --tx: 0; }
|
|
51
|
-
[data-animation='zoom-in-right'].is-visible { --scale: 1; --tx: 0; }
|
|
52
|
-
|
|
53
|
-
[data-animation='flip'].is-visible { --ry: 0deg; }
|
|
54
|
-
[data-animation='flip-x'].is-visible { --rx: 0deg; }
|
|
55
|
-
|
|
56
|
-
[data-animation='slide-rotate'].is-visible { --tx: 0; --rotate: 0deg; }
|
|
57
|
-
|
|
58
|
-
@keyframes bounce {
|
|
59
|
-
0% { transform: scale(0); }
|
|
60
|
-
50% { transform: scale(1.1); }
|
|
61
|
-
100% { transform: scale(1); }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* Bounce animation - special case with keyframes */
|
|
65
|
-
[data-animation='bounce-in'].is-visible {
|
|
66
|
-
animation: bounce var(--duration, 1500ms) cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
67
|
-
animation-delay: var(--delay, 0ms);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* Accessibility: Respect user's motion preferences */
|
|
71
|
-
@media (prefers-reduced-motion: reduce) {
|
|
72
|
-
.scroll-animate,
|
|
73
|
-
.animated-element {
|
|
74
|
-
transition: none;
|
|
75
|
-
animation: none !important;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.scroll-animate.is-visible,
|
|
79
|
-
.animated-element.is-visible {
|
|
80
|
-
opacity: 1;
|
|
81
|
-
transform: none !important;
|
|
82
|
-
}
|
|
83
|
-
}
|
package/dist/animations.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calculate rootMargin for IntersectionObserver from offset or custom rootMargin
|
|
3
|
-
*
|
|
4
|
-
* @param {number} [offset] - Viewport offset (0-100). 0 = bottom of viewport touches top of element, 100 = top of viewport touches top of element
|
|
5
|
-
* @param {string} [rootMargin] - Custom rootMargin string (takes precedence over offset)
|
|
6
|
-
* @returns {string} rootMargin string for IntersectionObserver
|
|
7
|
-
*/
|
|
8
|
-
export function calculateRootMargin(offset?: number, rootMargin?: string): string;
|
|
9
|
-
/**
|
|
10
|
-
* Animation utilities
|
|
11
|
-
* Type definitions have been moved to types.js for single source of truth
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* All available animation types in the library
|
|
15
|
-
* Useful for programmatic access and validation
|
|
16
|
-
* @type {readonly string[]}
|
|
17
|
-
*/
|
|
18
|
-
export const ANIMATION_TYPES: readonly string[];
|
package/dist/animations.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Animation utilities
|
|
3
|
-
* Type definitions have been moved to types.js for single source of truth
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* All available animation types in the library
|
|
8
|
-
* Useful for programmatic access and validation
|
|
9
|
-
* @type {readonly string[]}
|
|
10
|
-
*/
|
|
11
|
-
export const ANIMATION_TYPES = [
|
|
12
|
-
'fade-in',
|
|
13
|
-
'fade-in-up',
|
|
14
|
-
'fade-in-down',
|
|
15
|
-
'fade-in-left',
|
|
16
|
-
'fade-in-right',
|
|
17
|
-
'zoom-in',
|
|
18
|
-
'zoom-out',
|
|
19
|
-
'zoom-in-up',
|
|
20
|
-
'zoom-in-left',
|
|
21
|
-
'zoom-in-right',
|
|
22
|
-
'flip',
|
|
23
|
-
'flip-x',
|
|
24
|
-
'slide-rotate',
|
|
25
|
-
'bounce-in'
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Calculate rootMargin for IntersectionObserver from offset or custom rootMargin
|
|
30
|
-
*
|
|
31
|
-
* @param {number} [offset] - Viewport offset (0-100). 0 = bottom of viewport touches top of element, 100 = top of viewport touches top of element
|
|
32
|
-
* @param {string} [rootMargin] - Custom rootMargin string (takes precedence over offset)
|
|
33
|
-
* @returns {string} rootMargin string for IntersectionObserver
|
|
34
|
-
*/
|
|
35
|
-
export function calculateRootMargin(offset, rootMargin) {
|
|
36
|
-
return rootMargin ??
|
|
37
|
-
(offset !== undefined ? `-${100 - offset}% 0px -${offset}% 0px` : '-10% 0px -10% 0px');
|
|
38
|
-
}
|
package/dist/dom-utils.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {HTMLElement} element
|
|
3
|
-
* @param {number} [duration]
|
|
4
|
-
* @param {number} [delay=0]
|
|
5
|
-
*/
|
|
6
|
-
export function setCSSVariables(element: HTMLElement, duration?: number, delay?: number): void;
|
|
7
|
-
/**
|
|
8
|
-
* @param {HTMLElement} element
|
|
9
|
-
* @param {import('./types.js').AnimationType} animation
|
|
10
|
-
*/
|
|
11
|
-
export function setupAnimationElement(element: HTMLElement, animation: import("./types.js").AnimationType): void;
|
|
12
|
-
/**
|
|
13
|
-
* @param {HTMLElement} element
|
|
14
|
-
* @param {boolean} [debug=false]
|
|
15
|
-
* @param {number} [offset=0]
|
|
16
|
-
* @param {string} [sentinelColor='#00e0ff']
|
|
17
|
-
* @param {string} [debugLabel]
|
|
18
|
-
* @param {string} [sentinelId]
|
|
19
|
-
* @returns {{ element: HTMLElement, id: string }}
|
|
20
|
-
*/
|
|
21
|
-
export function createSentinel(element: HTMLElement, debug?: boolean, offset?: number, sentinelColor?: string, debugLabel?: string, sentinelId?: string): {
|
|
22
|
-
element: HTMLElement;
|
|
23
|
-
id: string;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Check if CSS animations are loaded and warn if not (dev only)
|
|
27
|
-
* Uses cache to avoid expensive getComputedStyle() on every element creation
|
|
28
|
-
* @returns {boolean} True if CSS appears to be loaded
|
|
29
|
-
*/
|
|
30
|
-
export function checkAndWarnIfCSSNotLoaded(): boolean;
|
package/dist/dom-utils.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global counter for auto-generating sentinel IDs
|
|
3
|
-
* @type {number}
|
|
4
|
-
*/
|
|
5
|
-
let sentinelCounter = 0;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Cache to check CSS only once per page load
|
|
9
|
-
* Avoids expensive getComputedStyle() calls
|
|
10
|
-
* @type {boolean | null}
|
|
11
|
-
*/
|
|
12
|
-
let cssCheckResult = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {HTMLElement} element
|
|
16
|
-
* @param {number} [duration]
|
|
17
|
-
* @param {number} [delay=0]
|
|
18
|
-
*/
|
|
19
|
-
export function setCSSVariables(element, duration, delay = 0) {
|
|
20
|
-
if (duration !== undefined) {
|
|
21
|
-
element.style.setProperty('--duration', `${duration}ms`);
|
|
22
|
-
}
|
|
23
|
-
element.style.setProperty('--delay', `${delay}ms`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {HTMLElement} element
|
|
28
|
-
* @param {import('./types.js').AnimationType} animation
|
|
29
|
-
*/
|
|
30
|
-
export function setupAnimationElement(element, animation) {
|
|
31
|
-
element.classList.add('scroll-animate');
|
|
32
|
-
element.setAttribute('data-animation', animation);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {HTMLElement} element
|
|
37
|
-
* @param {boolean} [debug=false]
|
|
38
|
-
* @param {number} [offset=0]
|
|
39
|
-
* @param {string} [sentinelColor='#00e0ff']
|
|
40
|
-
* @param {string} [debugLabel]
|
|
41
|
-
* @param {string} [sentinelId]
|
|
42
|
-
* @returns {{ element: HTMLElement, id: string }}
|
|
43
|
-
*/
|
|
44
|
-
export function createSentinel(element, debug = false, offset = 0, sentinelColor = '#00e0ff', debugLabel = '', sentinelId) {
|
|
45
|
-
const sentinel = document.createElement('div');
|
|
46
|
-
// Use offsetHeight instead of getBoundingClientRect for accurate dimensions
|
|
47
|
-
// getBoundingClientRect returns transformed dimensions (affected by scale, etc)
|
|
48
|
-
// offsetHeight returns actual element height independent of CSS transforms
|
|
49
|
-
const elementHeight = element.offsetHeight;
|
|
50
|
-
const sentinelTop = elementHeight + offset;
|
|
51
|
-
|
|
52
|
-
// Generate auto-ID if not provided
|
|
53
|
-
if (!sentinelId) {
|
|
54
|
-
sentinelCounter++;
|
|
55
|
-
sentinelId = `sentinel-${sentinelCounter}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Always set to data-sentinel-id attribute
|
|
59
|
-
sentinel.setAttribute('data-sentinel-id', sentinelId);
|
|
60
|
-
|
|
61
|
-
if (debug) {
|
|
62
|
-
sentinel.style.cssText =
|
|
63
|
-
`position:absolute;top:${sentinelTop}px;left:0;width:100%;height:3px;background:${sentinelColor};margin:0;padding:2px 4px;box-sizing:border-box;z-index:999;pointer-events:none;display:flex;align-items:center;font-size:10px;color:#000;font-weight:bold;white-space:nowrap;overflow:hidden;text-overflow:ellipsis`;
|
|
64
|
-
sentinel.setAttribute('data-sentinel-debug', 'true');
|
|
65
|
-
// Show ID in debug mode (or debugLabel if provided)
|
|
66
|
-
if (debugLabel) {
|
|
67
|
-
sentinel.textContent = debugLabel;
|
|
68
|
-
} else {
|
|
69
|
-
sentinel.textContent = sentinelId;
|
|
70
|
-
}
|
|
71
|
-
} else {
|
|
72
|
-
sentinel.style.cssText =
|
|
73
|
-
`position:absolute;top:${sentinelTop}px;left:0;width:100%;height:1px;visibility:hidden;margin:0;padding:0;box-sizing:border-box;pointer-events:none`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return { element: sentinel, id: sentinelId };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check if CSS animations are loaded and warn if not (dev only)
|
|
81
|
-
* Uses cache to avoid expensive getComputedStyle() on every element creation
|
|
82
|
-
* @returns {boolean} True if CSS appears to be loaded
|
|
83
|
-
*/
|
|
84
|
-
export function checkAndWarnIfCSSNotLoaded() {
|
|
85
|
-
if (typeof document === 'undefined') return false;
|
|
86
|
-
if (process.env.NODE_ENV === 'production') return true;
|
|
87
|
-
|
|
88
|
-
// Return cached result if already checked (avoids expensive reflows)
|
|
89
|
-
if (cssCheckResult !== null) return cssCheckResult;
|
|
90
|
-
|
|
91
|
-
// Try to detect if animations.css is loaded by checking for animation classes
|
|
92
|
-
const test = document.createElement('div');
|
|
93
|
-
test.className = 'scroll-animate is-visible';
|
|
94
|
-
test.style.position = 'absolute';
|
|
95
|
-
test.style.opacity = '0';
|
|
96
|
-
document.body.appendChild(test);
|
|
97
|
-
const computed = getComputedStyle(test);
|
|
98
|
-
const hasAnimation = computed.animation !== 'none' && computed.animation !== '';
|
|
99
|
-
test.remove();
|
|
100
|
-
|
|
101
|
-
if (!hasAnimation) {
|
|
102
|
-
console.warn(
|
|
103
|
-
'[rune-scroller] CSS animations not found. Make sure to import the animations:\n' +
|
|
104
|
-
' import "rune-scroller/animations.css";\n' +
|
|
105
|
-
'Documentation: https://github.com/lelabdev/rune-scroller#installation'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Cache the result for future calls
|
|
110
|
-
cssCheckResult = hasAnimation;
|
|
111
|
-
return hasAnimation;
|
|
112
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rune Scroller - Lightweight scroll animations for Svelte 5
|
|
3
|
-
*
|
|
4
|
-
* Main entry point exporting all public APIs
|
|
5
|
-
*
|
|
6
|
-
* @module rune-scroller
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Import CSS animations automatically
|
|
10
|
-
import './animations.css';
|
|
11
|
-
|
|
12
|
-
// Main action (default export - recommended)
|
|
13
|
-
import { runeScroller } from './runeScroller.js';
|
|
14
|
-
export default runeScroller;
|
|
15
|
-
export { runeScroller };
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Composables
|
|
20
|
-
export { useIntersection, useIntersectionOnce } from './useIntersection.svelte.js';
|
|
21
|
-
|
|
22
|
-
// Utilities
|
|
23
|
-
export { calculateRootMargin } from './animations.js';
|
package/dist/observer-utils.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared IntersectionObserver utility functions
|
|
3
|
-
* Reduces code duplication between action implementations
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @param {HTMLElement} target
|
|
7
|
-
* @param {IntersectionObserverCallback} callback
|
|
8
|
-
* @param {IntersectionObserverInit} options
|
|
9
|
-
* @returns {{ observer: IntersectionObserver, isConnected: boolean }}
|
|
10
|
-
*/
|
|
11
|
-
export function createManagedObserver(target: HTMLElement, callback: IntersectionObserverCallback, options: IntersectionObserverInit): {
|
|
12
|
-
observer: IntersectionObserver;
|
|
13
|
-
isConnected: boolean;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* @param {IntersectionObserver} observer
|
|
17
|
-
* @param {{ isConnected: boolean }} state
|
|
18
|
-
*/
|
|
19
|
-
export function disconnectObserver(observer: IntersectionObserver, state: {
|
|
20
|
-
isConnected: boolean;
|
|
21
|
-
}): void;
|
package/dist/observer-utils.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared IntersectionObserver utility functions
|
|
3
|
-
* Reduces code duplication between action implementations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {HTMLElement} target
|
|
8
|
-
* @param {IntersectionObserverCallback} callback
|
|
9
|
-
* @param {IntersectionObserverInit} options
|
|
10
|
-
* @returns {{ observer: IntersectionObserver, isConnected: boolean }}
|
|
11
|
-
*/
|
|
12
|
-
export function createManagedObserver(target, callback, options) {
|
|
13
|
-
const observer = new IntersectionObserver(callback, options);
|
|
14
|
-
observer.observe(target);
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
observer,
|
|
18
|
-
isConnected: true
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {IntersectionObserver} observer
|
|
24
|
-
* @param {{ isConnected: boolean }} state
|
|
25
|
-
*/
|
|
26
|
-
export function disconnectObserver(observer, state) {
|
|
27
|
-
if (state.isConnected && observer) {
|
|
28
|
-
observer.disconnect();
|
|
29
|
-
state.isConnected = false;
|
|
30
|
-
}
|
|
31
|
-
}
|
package/dist/runeScroller.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {HTMLElement} element
|
|
3
|
-
* @param {import('./types.js').RuneScrollerOptions} [options]
|
|
4
|
-
* @returns {{ update: (newOptions?: import('./types.js').RuneScrollerOptions) => void, destroy: () => void }}
|
|
5
|
-
*/
|
|
6
|
-
export function runeScroller(element: HTMLElement, options?: import("./types.js").RuneScrollerOptions): {
|
|
7
|
-
update: (newOptions?: import("./types.js").RuneScrollerOptions) => void;
|
|
8
|
-
destroy: () => void;
|
|
9
|
-
};
|
package/dist/runeScroller.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { setCSSVariables, setupAnimationElement, createSentinel, checkAndWarnIfCSSNotLoaded } from './dom-utils.js';
|
|
2
|
-
import { createManagedObserver, disconnectObserver } from './observer-utils.js';
|
|
3
|
-
import { ANIMATION_TYPES } from './animations.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @param {HTMLElement} element
|
|
7
|
-
* @param {import('./types.js').RuneScrollerOptions} [options]
|
|
8
|
-
* @returns {{ update: (newOptions?: import('./types.js').RuneScrollerOptions) => void, destroy: () => void }}
|
|
9
|
-
*/
|
|
10
|
-
export function runeScroller(element, options) {
|
|
11
|
-
// SSR Guard: Return no-op action when running on server
|
|
12
|
-
if (typeof window === 'undefined') {
|
|
13
|
-
return {
|
|
14
|
-
update: () => {},
|
|
15
|
-
destroy: () => {}
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Warn if CSS is not loaded (first time only)
|
|
20
|
-
if (typeof document !== 'undefined') {
|
|
21
|
-
checkAndWarnIfCSSNotLoaded();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Validate animation type
|
|
25
|
-
let animation = options?.animation ?? 'fade-in';
|
|
26
|
-
if (animation && !ANIMATION_TYPES.includes(animation)) {
|
|
27
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
28
|
-
console.warn(
|
|
29
|
-
`[rune-scroller] Invalid animation "${animation}". Using "fade-in" instead. ` +
|
|
30
|
-
`Valid options: ${ANIMATION_TYPES.join(', ')}`
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
animation = 'fade-in';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Initialize opacity: 0 BEFORE adding .scroll-animate class
|
|
37
|
-
// This ensures the transition applies correctly when .is-visible is added later
|
|
38
|
-
element.style.opacity = '0';
|
|
39
|
-
|
|
40
|
-
// Setup animation classes and CSS variables
|
|
41
|
-
if (animation) {
|
|
42
|
-
setupAnimationElement(element, animation);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Set CSS variables for duration
|
|
46
|
-
if (options?.duration !== undefined) {
|
|
47
|
-
setCSSVariables(element, options.duration);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Force reflow to ensure transitions are ready
|
|
51
|
-
void element.offsetHeight;
|
|
52
|
-
|
|
53
|
-
// Create a wrapper div around the element to position the sentinel
|
|
54
|
-
const wrapper = document.createElement('div');
|
|
55
|
-
wrapper.style.cssText = 'position:relative;display:block;width:100%;margin:0;padding:0;box-sizing:border-box';
|
|
56
|
-
element.insertAdjacentElement('beforebegin', wrapper);
|
|
57
|
-
wrapper.appendChild(element);
|
|
58
|
-
|
|
59
|
-
// Create the invisible sentinel (or visible if debug=true)
|
|
60
|
-
// Positioned absolutely relative to the wrapper
|
|
61
|
-
const sentinelResult = createSentinel(
|
|
62
|
-
element,
|
|
63
|
-
options?.debug,
|
|
64
|
-
options?.offset,
|
|
65
|
-
options?.sentinelColor,
|
|
66
|
-
options?.debugLabel,
|
|
67
|
-
options?.sentinelId
|
|
68
|
-
);
|
|
69
|
-
const sentinel = sentinelResult.element;
|
|
70
|
-
const sentinelId = sentinelResult.id;
|
|
71
|
-
|
|
72
|
-
// Add sentinel ID to element (either provided or auto-generated)
|
|
73
|
-
element.setAttribute('data-sentinel-id', sentinelId);
|
|
74
|
-
|
|
75
|
-
wrapper.appendChild(sentinel);
|
|
76
|
-
|
|
77
|
-
// Observe the sentinel with cleanup tracking
|
|
78
|
-
const state = { isConnected: true };
|
|
79
|
-
let currentSentinel = sentinel;
|
|
80
|
-
let resizeObserver;
|
|
81
|
-
let intersectionObserver;
|
|
82
|
-
|
|
83
|
-
const { observer } = createManagedObserver(
|
|
84
|
-
sentinel,
|
|
85
|
-
(entries) => {
|
|
86
|
-
const isIntersecting = entries[0].isIntersecting;
|
|
87
|
-
if (isIntersecting) {
|
|
88
|
-
// Add the is-visible class to trigger animation
|
|
89
|
-
element.classList.add('is-visible');
|
|
90
|
-
// Call onVisible callback if provided
|
|
91
|
-
options?.onVisible?.(element);
|
|
92
|
-
// Disconnect if not in repeat mode
|
|
93
|
-
if (!options?.repeat) {
|
|
94
|
-
disconnectObserver(intersectionObserver, state);
|
|
95
|
-
}
|
|
96
|
-
} else if (options?.repeat) {
|
|
97
|
-
// In repeat mode, remove the class when the sentinel exits
|
|
98
|
-
element.classList.remove('is-visible');
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
{ threshold: 0 }
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
intersectionObserver = observer;
|
|
105
|
-
|
|
106
|
-
// Function to recreate sentinel when element is resized
|
|
107
|
-
const recreateSentinel = () => {
|
|
108
|
-
const newSentinelResult = createSentinel(
|
|
109
|
-
element,
|
|
110
|
-
options?.debug,
|
|
111
|
-
options?.offset,
|
|
112
|
-
options?.sentinelColor,
|
|
113
|
-
options?.debugLabel,
|
|
114
|
-
sentinelId
|
|
115
|
-
);
|
|
116
|
-
const newSentinel = newSentinelResult.element;
|
|
117
|
-
currentSentinel.replaceWith(newSentinel);
|
|
118
|
-
currentSentinel = newSentinel;
|
|
119
|
-
// Update observer to watch the new sentinel
|
|
120
|
-
intersectionObserver.disconnect();
|
|
121
|
-
intersectionObserver.observe(newSentinel);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Setup ResizeObserver to handle element resizing
|
|
125
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
126
|
-
resizeObserver = new ResizeObserver(() => {
|
|
127
|
-
recreateSentinel();
|
|
128
|
-
});
|
|
129
|
-
resizeObserver.observe(element);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
update(newOptions) {
|
|
134
|
-
if (newOptions?.animation) {
|
|
135
|
-
element.setAttribute('data-animation', newOptions.animation);
|
|
136
|
-
}
|
|
137
|
-
if (newOptions?.duration) {
|
|
138
|
-
setCSSVariables(element, newOptions.duration);
|
|
139
|
-
}
|
|
140
|
-
// Update repeat option
|
|
141
|
-
if (newOptions?.repeat !== undefined && newOptions.repeat !== options?.repeat) {
|
|
142
|
-
options = { ...options, repeat: newOptions.repeat };
|
|
143
|
-
}
|
|
144
|
-
// Update offset and debug if changed
|
|
145
|
-
if ((newOptions?.offset !== undefined && newOptions.offset !== options?.offset) ||
|
|
146
|
-
(newOptions?.debug !== undefined && newOptions.debug !== options?.debug)) {
|
|
147
|
-
options = { ...options, ...newOptions };
|
|
148
|
-
recreateSentinel();
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
destroy() {
|
|
152
|
-
disconnectObserver(intersectionObserver, state);
|
|
153
|
-
// Cleanup ResizeObserver
|
|
154
|
-
if (resizeObserver) {
|
|
155
|
-
resizeObserver.disconnect();
|
|
156
|
-
}
|
|
157
|
-
currentSentinel.remove();
|
|
158
|
-
// Unwrap element (move it out of wrapper)
|
|
159
|
-
const parent = wrapper.parentElement;
|
|
160
|
-
if (parent) {
|
|
161
|
-
wrapper.insertAdjacentElement('beforebegin', element);
|
|
162
|
-
}
|
|
163
|
-
wrapper.remove();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Animation type names available in Rune Scroller
|
|
3
|
-
*/
|
|
4
|
-
export type AnimationType = "fade-in" | "fade-in-up" | "fade-in-down" | "fade-in-left" | "fade-in-right" | "zoom-in" | "zoom-out" | "zoom-in-up" | "zoom-in-left" | "zoom-in-right" | "flip" | "flip-x" | "slide-rotate" | "bounce-in";
|
|
5
|
-
/**
|
|
6
|
-
* Options for the runeScroller action
|
|
7
|
-
* Sentinel-based scroll animation triggering
|
|
8
|
-
*/
|
|
9
|
-
export type RuneScrollerOptions = {
|
|
10
|
-
/**
|
|
11
|
-
* - Animation type to apply
|
|
12
|
-
*/
|
|
13
|
-
animation?: AnimationType | undefined;
|
|
14
|
-
/**
|
|
15
|
-
* - Animation duration in milliseconds
|
|
16
|
-
*/
|
|
17
|
-
duration?: number | undefined;
|
|
18
|
-
/**
|
|
19
|
-
* - Repeat animation on every scroll
|
|
20
|
-
*/
|
|
21
|
-
repeat?: boolean | undefined;
|
|
22
|
-
/**
|
|
23
|
-
* - Show sentinel as visible line for debugging
|
|
24
|
-
*/
|
|
25
|
-
debug?: boolean | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* - Sentinel color for debug mode (hex or CSS color)
|
|
28
|
-
*/
|
|
29
|
-
sentinelColor?: string | undefined;
|
|
30
|
-
/**
|
|
31
|
-
* - Unique identifier for sentinel (auto-generated if not provided)
|
|
32
|
-
*/
|
|
33
|
-
sentinelId?: string | undefined;
|
|
34
|
-
/**
|
|
35
|
-
* - Debug label to show on sentinel (e.g., animation name)
|
|
36
|
-
*/
|
|
37
|
-
debugLabel?: string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
* - Offset of sentinel in pixels (negative = above element)
|
|
40
|
-
*/
|
|
41
|
-
offset?: number | undefined;
|
|
42
|
-
/**
|
|
43
|
-
* - Callback fired when animation becomes visible
|
|
44
|
-
*/
|
|
45
|
-
onVisible?: ((element: HTMLElement) => void) | undefined;
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Configuration options for IntersectionObserver
|
|
49
|
-
* Used by useIntersection and useIntersectionOnce composables
|
|
50
|
-
*/
|
|
51
|
-
export type IntersectionOptions = {
|
|
52
|
-
/**
|
|
53
|
-
* - IntersectionObserver threshold
|
|
54
|
-
*/
|
|
55
|
-
threshold?: number | number[] | undefined;
|
|
56
|
-
/**
|
|
57
|
-
* - Custom margin around root element
|
|
58
|
-
*/
|
|
59
|
-
rootMargin?: string | undefined;
|
|
60
|
-
/**
|
|
61
|
-
* - Root element for intersection observation
|
|
62
|
-
*/
|
|
63
|
-
root?: Element | null | undefined;
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Return type for useIntersection and useIntersectionOnce composables
|
|
67
|
-
* Provides reactive element reference and visibility state
|
|
68
|
-
*/
|
|
69
|
-
export type UseIntersectionReturn = {
|
|
70
|
-
/**
|
|
71
|
-
* - Reference to the DOM element being observed
|
|
72
|
-
*/
|
|
73
|
-
element: HTMLElement | null;
|
|
74
|
-
/**
|
|
75
|
-
* - Whether the element is currently visible in viewport
|
|
76
|
-
*/
|
|
77
|
-
isVisible: boolean;
|
|
78
|
-
};
|
package/dist/types.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized type definitions for Rune Scroller library
|
|
3
|
-
* All types are defined here for consistency and ease of maintenance
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Animation type names available in Rune Scroller
|
|
8
|
-
* @typedef {'fade-in' | 'fade-in-up' | 'fade-in-down' | 'fade-in-left' | 'fade-in-right' | 'zoom-in' | 'zoom-out' | 'zoom-in-up' | 'zoom-in-left' | 'zoom-in-right' | 'flip' | 'flip-x' | 'slide-rotate' | 'bounce-in'} AnimationType
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Options for the runeScroller action
|
|
13
|
-
* Sentinel-based scroll animation triggering
|
|
14
|
-
*
|
|
15
|
-
* @typedef {Object} RuneScrollerOptions
|
|
16
|
-
* @property {AnimationType} [animation='fade-in'] - Animation type to apply
|
|
17
|
-
* @property {number} [duration=2500] - Animation duration in milliseconds
|
|
18
|
-
* @property {boolean} [repeat=false] - Repeat animation on every scroll
|
|
19
|
-
* @property {boolean} [debug=false] - Show sentinel as visible line for debugging
|
|
20
|
-
* @property {string} [sentinelColor='#00e0ff'] - Sentinel color for debug mode (hex or CSS color)
|
|
21
|
-
* @property {string} [sentinelId] - Unique identifier for sentinel (auto-generated if not provided)
|
|
22
|
-
* @property {string} [debugLabel] - Debug label to show on sentinel (e.g., animation name)
|
|
23
|
-
* @property {number} [offset=0] - Offset of sentinel in pixels (negative = above element)
|
|
24
|
-
* @property {(element: HTMLElement) => void} [onVisible] - Callback fired when animation becomes visible
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Configuration options for IntersectionObserver
|
|
31
|
-
* Used by useIntersection and useIntersectionOnce composables
|
|
32
|
-
*
|
|
33
|
-
* @typedef {Object} IntersectionOptions
|
|
34
|
-
* @property {number | number[]} [threshold] - IntersectionObserver threshold
|
|
35
|
-
* @property {string} [rootMargin] - Custom margin around root element
|
|
36
|
-
* @property {Element | null} [root] - Root element for intersection observation
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Return type for useIntersection and useIntersectionOnce composables
|
|
41
|
-
* Provides reactive element reference and visibility state
|
|
42
|
-
*
|
|
43
|
-
* @typedef {Object} UseIntersectionReturn
|
|
44
|
-
* @property {HTMLElement | null} element - Reference to the DOM element being observed
|
|
45
|
-
* @property {boolean} isVisible - Whether the element is currently visible in viewport
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
3
|
-
* @param {(isVisible: boolean) => void} [onVisible]
|
|
4
|
-
* @returns {import('./types.js').UseIntersectionReturn}
|
|
5
|
-
*/
|
|
6
|
-
export function useIntersection(options?: import("./types.js").IntersectionOptions, onVisible?: (isVisible: boolean) => void): import("./types.js").UseIntersectionReturn;
|
|
7
|
-
/**
|
|
8
|
-
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
9
|
-
* @returns {import('./types.js').UseIntersectionReturn}
|
|
10
|
-
*/
|
|
11
|
-
export function useIntersectionOnce(options?: import("./types.js").IntersectionOptions): import("./types.js").UseIntersectionReturn;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composable for handling IntersectionObserver logic
|
|
3
|
-
* Reduces duplication between animation components
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
8
|
-
* @param {((entry: IntersectionObserverEntry, isVisible: boolean) => void) | undefined} onIntersect
|
|
9
|
-
* @param {boolean} [once=false]
|
|
10
|
-
* @returns {import('./types.js').UseIntersectionReturn}
|
|
11
|
-
*/
|
|
12
|
-
function createIntersectionObserver(options = {}, onIntersect = undefined, once = false) {
|
|
13
|
-
const { threshold = 0.5, rootMargin = '-10% 0px -10% 0px', root = null } = options;
|
|
14
|
-
|
|
15
|
-
let element = $state(null);
|
|
16
|
-
let isVisible = $state(false);
|
|
17
|
-
let hasTriggeredOnce = false;
|
|
18
|
-
/** @type {IntersectionObserver | null} */
|
|
19
|
-
let observer = null;
|
|
20
|
-
|
|
21
|
-
$effect(() => {
|
|
22
|
-
if (!element) return;
|
|
23
|
-
|
|
24
|
-
observer = new IntersectionObserver(
|
|
25
|
-
(entries) => {
|
|
26
|
-
entries.forEach((entry) => {
|
|
27
|
-
// For once-only behavior, check if already triggered
|
|
28
|
-
if (once && hasTriggeredOnce) return;
|
|
29
|
-
|
|
30
|
-
isVisible = entry.isIntersecting;
|
|
31
|
-
if (onIntersect) {
|
|
32
|
-
onIntersect(entry, entry.isIntersecting);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Unobserve after first trigger if once=true
|
|
36
|
-
if (once && entry.isIntersecting) {
|
|
37
|
-
hasTriggeredOnce = true;
|
|
38
|
-
observer?.unobserve(entry.target);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
threshold,
|
|
44
|
-
rootMargin,
|
|
45
|
-
root
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
observer.observe(element);
|
|
50
|
-
|
|
51
|
-
return () => {
|
|
52
|
-
observer?.disconnect();
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
get element() {
|
|
58
|
-
return element;
|
|
59
|
-
},
|
|
60
|
-
set element(value) {
|
|
61
|
-
element = value;
|
|
62
|
-
},
|
|
63
|
-
get isVisible() {
|
|
64
|
-
return isVisible;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
71
|
-
* @param {(isVisible: boolean) => void} [onVisible]
|
|
72
|
-
* @returns {import('./types.js').UseIntersectionReturn}
|
|
73
|
-
*/
|
|
74
|
-
export function useIntersection(options = {}, onVisible) {
|
|
75
|
-
return createIntersectionObserver(
|
|
76
|
-
options,
|
|
77
|
-
(_entry, isVisible) => {
|
|
78
|
-
onVisible?.(isVisible);
|
|
79
|
-
},
|
|
80
|
-
false
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
86
|
-
* @returns {import('./types.js').UseIntersectionReturn}
|
|
87
|
-
*/
|
|
88
|
-
export function useIntersectionOnce(options = {}) {
|
|
89
|
-
return createIntersectionObserver(options, () => {}, true);
|
|
90
|
-
}
|