rune-scroller 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -309
- package/dist/animate.svelte.d.ts +1 -9
- package/dist/animate.svelte.js +34 -14
- package/dist/dom-utils.svelte.d.ts +22 -0
- package/dist/dom-utils.svelte.js +46 -0
- package/dist/index.d.ts +4 -6
- package/dist/index.js +5 -4
- package/dist/runeScroller.svelte.d.ts +11 -24
- package/dist/runeScroller.svelte.js +23 -20
- package/dist/types.d.ts +44 -0
- package/dist/types.js +5 -0
- package/dist/useIntersection.svelte.d.ts +1 -13
- package/dist/useIntersection.svelte.js +4 -0
- package/package.json +1 -1
- package/dist/Rs.svelte.d.ts +0 -16
- /package/dist/{Rs.svelte → runeScroller.svelte} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# ⚡ Rune Scroller
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="./logo.png" alt="Rune Scroller Logo" width="200" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
3
7
|
**Native Scroll Animations for Svelte 5** — Built with **Svelte 5 Runes** and **IntersectionObserver API**. No external dependencies, pure performance.
|
|
4
8
|
|
|
5
9
|
> 🚀 **Open Source Project** by **[ludoloops](https://github.com/ludoloops)** at **[LeLab.dev](https://lelab.dev)**
|
|
@@ -57,9 +61,7 @@ For a typical SvelteKit app:
|
|
|
57
61
|
```
|
|
58
62
|
rune-scroller-lib/
|
|
59
63
|
├── src/lib/
|
|
60
|
-
│ ├──
|
|
61
|
-
│ ├── BaseAnimated.svelte # Base animation implementation
|
|
62
|
-
│ ├── runeScroller.svelte.ts # Sentinel-based animation action (recommended)
|
|
64
|
+
│ ├── runeScroller.svelte.ts # Main animation action (sentinel-based)
|
|
63
65
|
│ ├── useIntersection.svelte.ts # IntersectionObserver composables
|
|
64
66
|
│ ├── animate.svelte.ts # Animation action for direct DOM control
|
|
65
67
|
│ ├── animations.ts # Animation configuration & validation
|
|
@@ -92,115 +94,54 @@ pnpm add rune-scroller
|
|
|
92
94
|
yarn add rune-scroller
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
### Basic Usage with `
|
|
97
|
+
### Basic Usage with `runeScroller` Action
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
Use the `runeScroller` action with the `use:` directive for sentinel-based animation triggering:
|
|
98
100
|
|
|
99
101
|
```svelte
|
|
100
102
|
<script>
|
|
101
|
-
import
|
|
102
|
-
import 'rune-scroller/animations.css';
|
|
103
|
+
import runeScroller from 'rune-scroller';
|
|
103
104
|
</script>
|
|
104
105
|
|
|
105
|
-
<!--
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
</div>
|
|
111
|
-
</Rs>
|
|
112
|
-
|
|
113
|
-
<!-- Repeat animation every time element enters viewport -->
|
|
114
|
-
<Rs animation="zoom-in" repeat>
|
|
115
|
-
<div class="card">
|
|
116
|
-
<h2>Repeating Animation</h2>
|
|
117
|
-
<p>This triggers each time you scroll past it</p>
|
|
118
|
-
</div>
|
|
119
|
-
</Rs>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### In SvelteKit/Local Development
|
|
106
|
+
<!-- Simple fade in animation -->
|
|
107
|
+
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
108
|
+
<h2>Animated Heading</h2>
|
|
109
|
+
<p>Animates when scrolled into view</p>
|
|
110
|
+
</div>
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</script>
|
|
112
|
+
<!-- With custom duration -->
|
|
113
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
114
|
+
<div class="card">Smooth fade and slide</div>
|
|
115
|
+
</div>
|
|
129
116
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</Rs>
|
|
117
|
+
<!-- Repeat animation on every scroll -->
|
|
118
|
+
<div use:runeScroller={{ animation: 'bounce-in', duration: 800, repeat: true }}>
|
|
119
|
+
<button>Bounces on every scroll</button>
|
|
120
|
+
</div>
|
|
135
121
|
```
|
|
136
122
|
|
|
137
|
-
###
|
|
123
|
+
### How It Works
|
|
138
124
|
|
|
139
|
-
The `
|
|
125
|
+
The `runeScroller` action uses an invisible **sentinel element** for precise animation triggering:
|
|
140
126
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<!-- Customize timing -->
|
|
149
|
-
<Rs animation="zoom-in" duration={1200} delay={300}>
|
|
150
|
-
Content
|
|
151
|
-
</Rs>
|
|
152
|
-
|
|
153
|
-
<!-- Use with any HTML attribute -->
|
|
154
|
-
<Rs animation="bounce-in" class="my-class" data-test="value">
|
|
155
|
-
Content
|
|
156
|
-
</Rs>
|
|
157
|
-
```
|
|
127
|
+
1. **Sentinel Creation** - An invisible sentinel is created and positioned **absolutely** at your element's bottom
|
|
128
|
+
2. **Parent Context** - Parent element automatically gets `position: relative` if needed (no visual impact)
|
|
129
|
+
3. **Viewport Detection** - When the sentinel enters the viewport, it triggers the animation
|
|
130
|
+
4. **Fixed Positioning** - The sentinel stays fixed while your element animates, ensuring accurate timing
|
|
131
|
+
5. **Invisible by Default** - The sentinel is 1px tall and invisible (`visibility:hidden`)
|
|
132
|
+
6. **Debug Mode** - Add `debug: true` to see the sentinel as a visible cyan line (useful for development)
|
|
158
133
|
|
|
159
134
|
---
|
|
160
135
|
|
|
161
|
-
## 🎯 Sentinel-Based Animation Triggering
|
|
162
|
-
|
|
163
|
-
For more precise control over animation timing, use the `runeScroller` action. This approach uses an invisible **sentinel element** positioned below your content to trigger animations at exactly the right moment.
|
|
136
|
+
## 🎯 Sentinel-Based Animation Triggering
|
|
164
137
|
|
|
165
138
|
### Why Sentinels?
|
|
166
139
|
|
|
167
|
-
- **Accurate Timing** -
|
|
140
|
+
- **Accurate Timing** - Triggers at the perfect moment for smooth animations
|
|
168
141
|
- **Consistent Behavior** - Same timing across all screen sizes and content heights
|
|
169
142
|
- **Simple API** - No complex offset calculations needed
|
|
170
143
|
- **Performance** - Minimal overhead, pure CSS animations
|
|
171
|
-
|
|
172
|
-
### How Sentinels Work
|
|
173
|
-
|
|
174
|
-
1. An invisible 20px sentinel element is automatically placed **below** your animated element
|
|
175
|
-
2. When the sentinel enters the viewport, it triggers the animation
|
|
176
|
-
3. This ensures content animates in perfectly as it becomes visible
|
|
177
|
-
|
|
178
|
-
```svelte
|
|
179
|
-
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
|
|
180
|
-
<!-- Your content here -->
|
|
181
|
-
<!-- Invisible sentinel is automatically placed below -->
|
|
182
|
-
</div>
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Basic Usage
|
|
186
|
-
|
|
187
|
-
```svelte
|
|
188
|
-
<script>
|
|
189
|
-
import { runeScroller } from 'rune-scroller';
|
|
190
|
-
import 'rune-scroller/animations.css';
|
|
191
|
-
</script>
|
|
192
|
-
|
|
193
|
-
<!-- Simple fade in with sentinel triggering -->
|
|
194
|
-
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
195
|
-
<h2>Animated Heading</h2>
|
|
196
|
-
<p>Animates when sentinel enters viewport</p>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
<!-- With duration control -->
|
|
200
|
-
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
201
|
-
<div class="card">Smooth animation</div>
|
|
202
|
-
</div>
|
|
203
|
-
```
|
|
144
|
+
- **Reliable** - Automatic sentinel placement handles edge cases
|
|
204
145
|
|
|
205
146
|
### Sentinel-Based Examples
|
|
206
147
|
|
|
@@ -208,7 +149,7 @@ For more precise control over animation timing, use the `runeScroller` action. T
|
|
|
208
149
|
|
|
209
150
|
```svelte
|
|
210
151
|
<script>
|
|
211
|
-
import
|
|
152
|
+
import runeScroller from 'rune-scroller';
|
|
212
153
|
</script>
|
|
213
154
|
|
|
214
155
|
<div class="grid">
|
|
@@ -247,95 +188,6 @@ interface RuneScrollerOptions {
|
|
|
247
188
|
}
|
|
248
189
|
```
|
|
249
190
|
|
|
250
|
-
### Comparing: `Rs` Component vs `runeScroller` Action
|
|
251
|
-
|
|
252
|
-
| Feature | `Rs` Component | `runeScroller` Action |
|
|
253
|
-
|---------|---|---|
|
|
254
|
-
| **Usage** | `<Rs>` wrapper | `use:` directive |
|
|
255
|
-
| **Triggering** | IntersectionObserver on element | IntersectionObserver on sentinel |
|
|
256
|
-
| **Timing Control** | offset, threshold props | Automatic sentinel placement |
|
|
257
|
-
| **Repeat Support** | Yes (via `repeat` prop) | Yes (via `repeat` option) |
|
|
258
|
-
| **Best For** | Complex layouts, component isolation | Direct DOM control, simple/mixed elements |
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## ⚙️ Component Props
|
|
263
|
-
|
|
264
|
-
### Rs Component
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
interface RsProps {
|
|
268
|
-
animation?: string; // Animation type (default: 'fade-in')
|
|
269
|
-
threshold?: number; // Visibility threshold (default: 0.5)
|
|
270
|
-
offset?: number; // Trigger offset 0-100% (optional, uses default if not set)
|
|
271
|
-
rootMargin?: string; // Observer margin (overrides offset if set)
|
|
272
|
-
duration?: number; // Duration in ms (default: 800)
|
|
273
|
-
delay?: number; // Delay in ms (default: 0)
|
|
274
|
-
repeat?: boolean; // Repeat animation on every scroll (default: false)
|
|
275
|
-
children: Snippet; // Content to animate
|
|
276
|
-
[key: string]: any; // Accepts any HTML attributes (e.g., data-testid, class, etc.)
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
#### `offset` Prop (Optional)
|
|
281
|
-
|
|
282
|
-
Controls when the animation triggers as the element scrolls into view. If not specified, uses default behavior (`-10% 0px -10% 0px`).
|
|
283
|
-
|
|
284
|
-
- `offset={0}` — Triggers when element touches bottom of screen
|
|
285
|
-
- `offset={50}` — Triggers at middle of screen
|
|
286
|
-
- `offset={100}` — Triggers when element reaches top of screen
|
|
287
|
-
- **Not set** — Uses default behavior (triggers in middle ~80% band)
|
|
288
|
-
|
|
289
|
-
**Examples:**
|
|
290
|
-
|
|
291
|
-
```svelte
|
|
292
|
-
<!-- Early trigger (bottom of screen) -->
|
|
293
|
-
<Rs animation="fade-in-up" offset={0}>
|
|
294
|
-
<div class="card">Animates early</div>
|
|
295
|
-
</Rs>
|
|
296
|
-
|
|
297
|
-
<!-- Late trigger (top of screen) -->
|
|
298
|
-
<Rs animation="fade-in-up" offset={100}>
|
|
299
|
-
<div class="card">Animates late</div>
|
|
300
|
-
</Rs>
|
|
301
|
-
|
|
302
|
-
<!-- Custom timing -->
|
|
303
|
-
<Rs animation="fade-in-up" offset={75}>
|
|
304
|
-
<div class="card">Animates at 75%</div>
|
|
305
|
-
</Rs>
|
|
306
|
-
|
|
307
|
-
<!-- Repeat animation on every scroll -->
|
|
308
|
-
<Rs animation="fade-in-up" offset={50} repeat>
|
|
309
|
-
<div class="card">Animates every time</div>
|
|
310
|
-
</Rs>
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
**Full example with all props:**
|
|
314
|
-
|
|
315
|
-
```svelte
|
|
316
|
-
<Rs
|
|
317
|
-
animation="fade-in-up"
|
|
318
|
-
duration={1200}
|
|
319
|
-
delay={300}
|
|
320
|
-
threshold={0.8}
|
|
321
|
-
offset={25}
|
|
322
|
-
repeat={false}
|
|
323
|
-
data-testid="custom-animation"
|
|
324
|
-
>
|
|
325
|
-
<div class="card">
|
|
326
|
-
<h2>Custom Timing</h2>
|
|
327
|
-
<p>Duration: 1200ms, Delay: 300ms, Threshold: 80%, Offset: 25%</p>
|
|
328
|
-
</div>
|
|
329
|
-
</Rs>
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
#### Repeat Behavior
|
|
333
|
-
|
|
334
|
-
Use the `repeat` prop to control animation behavior:
|
|
335
|
-
|
|
336
|
-
- `repeat={false}` (default) - Animation plays **once** when element enters viewport
|
|
337
|
-
- `repeat={true}` - Animation **repeats** every time element enters viewport
|
|
338
|
-
|
|
339
191
|
---
|
|
340
192
|
|
|
341
193
|
## 🎨 All Animations with Examples
|
|
@@ -347,12 +199,14 @@ Use the `repeat` prop to control animation behavior:
|
|
|
347
199
|
Simple opacity fade from transparent to visible.
|
|
348
200
|
|
|
349
201
|
```svelte
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
</
|
|
202
|
+
<script>
|
|
203
|
+
import runeScroller from 'rune-scroller';
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
207
|
+
<h2>Fade In</h2>
|
|
208
|
+
<p>Simple fade entrance</p>
|
|
209
|
+
</div>
|
|
356
210
|
```
|
|
357
211
|
|
|
358
212
|
#### `fade-in-up`
|
|
@@ -360,12 +214,10 @@ Simple opacity fade from transparent to visible.
|
|
|
360
214
|
Fades in while moving up 100px.
|
|
361
215
|
|
|
362
216
|
```svelte
|
|
363
|
-
<
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
</div>
|
|
368
|
-
</Rs>
|
|
217
|
+
<div use:runeScroller={{ animation: 'fade-in-up' }}>
|
|
218
|
+
<h2>Fade In Up</h2>
|
|
219
|
+
<p>Rises from below</p>
|
|
220
|
+
</div>
|
|
369
221
|
```
|
|
370
222
|
|
|
371
223
|
#### `fade-in-down`
|
|
@@ -373,12 +225,10 @@ Fades in while moving up 100px.
|
|
|
373
225
|
Fades in while moving down 100px.
|
|
374
226
|
|
|
375
227
|
```svelte
|
|
376
|
-
<
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
</div>
|
|
381
|
-
</Rs>
|
|
228
|
+
<div use:runeScroller={{ animation: 'fade-in-down' }}>
|
|
229
|
+
<h2>Fade In Down</h2>
|
|
230
|
+
<p>Descends from above</p>
|
|
231
|
+
</div>
|
|
382
232
|
```
|
|
383
233
|
|
|
384
234
|
#### `fade-in-left`
|
|
@@ -386,12 +236,10 @@ Fades in while moving down 100px.
|
|
|
386
236
|
Fades in while moving left 100px.
|
|
387
237
|
|
|
388
238
|
```svelte
|
|
389
|
-
<
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
</div>
|
|
394
|
-
</Rs>
|
|
239
|
+
<div use:runeScroller={{ animation: 'fade-in-left' }}>
|
|
240
|
+
<h2>Fade In Left</h2>
|
|
241
|
+
<p>Comes from the right</p>
|
|
242
|
+
</div>
|
|
395
243
|
```
|
|
396
244
|
|
|
397
245
|
#### `fade-in-right`
|
|
@@ -399,12 +247,10 @@ Fades in while moving left 100px.
|
|
|
399
247
|
Fades in while moving right 100px.
|
|
400
248
|
|
|
401
249
|
```svelte
|
|
402
|
-
<
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
</div>
|
|
407
|
-
</Rs>
|
|
250
|
+
<div use:runeScroller={{ animation: 'fade-in-right' }}>
|
|
251
|
+
<h2>Fade In Right</h2>
|
|
252
|
+
<p>Comes from the left</p>
|
|
253
|
+
</div>
|
|
408
254
|
```
|
|
409
255
|
|
|
410
256
|
---
|
|
@@ -416,12 +262,10 @@ Fades in while moving right 100px.
|
|
|
416
262
|
Scales from 50% to 100% while fading in.
|
|
417
263
|
|
|
418
264
|
```svelte
|
|
419
|
-
<
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
</div>
|
|
424
|
-
</Rs>
|
|
265
|
+
<div use:runeScroller={{ animation: 'zoom-in' }}>
|
|
266
|
+
<h2>Zoom In</h2>
|
|
267
|
+
<p>Grows into view</p>
|
|
268
|
+
</div>
|
|
425
269
|
```
|
|
426
270
|
|
|
427
271
|
#### `zoom-out`
|
|
@@ -429,12 +273,10 @@ Scales from 50% to 100% while fading in.
|
|
|
429
273
|
Scales from 150% to 100% while fading in.
|
|
430
274
|
|
|
431
275
|
```svelte
|
|
432
|
-
<
|
|
433
|
-
<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
</div>
|
|
437
|
-
</Rs>
|
|
276
|
+
<div use:runeScroller={{ animation: 'zoom-out' }}>
|
|
277
|
+
<h2>Zoom Out</h2>
|
|
278
|
+
<p>Shrinks into view</p>
|
|
279
|
+
</div>
|
|
438
280
|
```
|
|
439
281
|
|
|
440
282
|
#### `zoom-in-up`
|
|
@@ -442,12 +284,10 @@ Scales from 150% to 100% while fading in.
|
|
|
442
284
|
Scales from 50% while translating up 50px.
|
|
443
285
|
|
|
444
286
|
```svelte
|
|
445
|
-
<
|
|
446
|
-
<
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
</div>
|
|
450
|
-
</Rs>
|
|
287
|
+
<div use:runeScroller={{ animation: 'zoom-in-up' }}>
|
|
288
|
+
<h2>Zoom In Up</h2>
|
|
289
|
+
<p>Grows while moving up</p>
|
|
290
|
+
</div>
|
|
451
291
|
```
|
|
452
292
|
|
|
453
293
|
#### `zoom-in-left`
|
|
@@ -455,12 +295,10 @@ Scales from 50% while translating up 50px.
|
|
|
455
295
|
Scales from 50% while translating left 50px.
|
|
456
296
|
|
|
457
297
|
```svelte
|
|
458
|
-
<
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
</div>
|
|
463
|
-
</Rs>
|
|
298
|
+
<div use:runeScroller={{ animation: 'zoom-in-left' }}>
|
|
299
|
+
<h2>Zoom In Left</h2>
|
|
300
|
+
<p>Grows while moving left</p>
|
|
301
|
+
</div>
|
|
464
302
|
```
|
|
465
303
|
|
|
466
304
|
#### `zoom-in-right`
|
|
@@ -468,12 +306,10 @@ Scales from 50% while translating left 50px.
|
|
|
468
306
|
Scales from 50% while translating right 50px.
|
|
469
307
|
|
|
470
308
|
```svelte
|
|
471
|
-
<
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
</div>
|
|
476
|
-
</Rs>
|
|
309
|
+
<div use:runeScroller={{ animation: 'zoom-in-right' }}>
|
|
310
|
+
<h2>Zoom In Right</h2>
|
|
311
|
+
<p>Grows while moving right</p>
|
|
312
|
+
</div>
|
|
477
313
|
```
|
|
478
314
|
|
|
479
315
|
---
|
|
@@ -485,12 +321,10 @@ Scales from 50% while translating right 50px.
|
|
|
485
321
|
3D rotation on Y axis (left to right).
|
|
486
322
|
|
|
487
323
|
```svelte
|
|
488
|
-
<
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
</div>
|
|
493
|
-
</Rs>
|
|
324
|
+
<div use:runeScroller={{ animation: 'flip' }}>
|
|
325
|
+
<h2>Flip</h2>
|
|
326
|
+
<p>Rotates on Y axis</p>
|
|
327
|
+
</div>
|
|
494
328
|
```
|
|
495
329
|
|
|
496
330
|
#### `flip-x`
|
|
@@ -498,12 +332,10 @@ Scales from 50% while translating right 50px.
|
|
|
498
332
|
3D rotation on X axis (top to bottom).
|
|
499
333
|
|
|
500
334
|
```svelte
|
|
501
|
-
<
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
</div>
|
|
506
|
-
</Rs>
|
|
335
|
+
<div use:runeScroller={{ animation: 'flip-x' }}>
|
|
336
|
+
<h2>Flip X</h2>
|
|
337
|
+
<p>Rotates on X axis</p>
|
|
338
|
+
</div>
|
|
507
339
|
```
|
|
508
340
|
|
|
509
341
|
---
|
|
@@ -515,12 +347,10 @@ Scales from 50% while translating right 50px.
|
|
|
515
347
|
Slides from left while rotating 45 degrees.
|
|
516
348
|
|
|
517
349
|
```svelte
|
|
518
|
-
<
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
</div>
|
|
523
|
-
</Rs>
|
|
350
|
+
<div use:runeScroller={{ animation: 'slide-rotate' }}>
|
|
351
|
+
<h2>Slide Rotate</h2>
|
|
352
|
+
<p>Slides and spins</p>
|
|
353
|
+
</div>
|
|
524
354
|
```
|
|
525
355
|
|
|
526
356
|
---
|
|
@@ -532,30 +362,28 @@ Slides from left while rotating 45 degrees.
|
|
|
532
362
|
Bouncy entrance with scaling keyframe animation.
|
|
533
363
|
|
|
534
364
|
```svelte
|
|
535
|
-
<
|
|
536
|
-
<
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
</div>
|
|
540
|
-
</Rs>
|
|
365
|
+
<div use:runeScroller={{ animation: 'bounce-in', duration: 800 }}>
|
|
366
|
+
<h2>Bounce In</h2>
|
|
367
|
+
<p>Bounces into view</p>
|
|
368
|
+
</div>
|
|
541
369
|
```
|
|
542
370
|
|
|
543
371
|
---
|
|
544
372
|
|
|
545
|
-
###
|
|
373
|
+
### One-Time vs Repeat Animations
|
|
546
374
|
|
|
547
|
-
|
|
375
|
+
Control animation behavior with the `repeat` option:
|
|
548
376
|
|
|
549
377
|
```svelte
|
|
550
|
-
<!-- Plays once on scroll
|
|
551
|
-
<
|
|
552
|
-
|
|
553
|
-
</
|
|
378
|
+
<!-- Plays once on scroll (default) -->
|
|
379
|
+
<div use:runeScroller={{ animation: 'fade-in-up' }}>
|
|
380
|
+
Animates once when scrolled into view
|
|
381
|
+
</div>
|
|
554
382
|
|
|
555
383
|
<!-- Repeats each time you scroll by -->
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
</
|
|
384
|
+
<div use:runeScroller={{ animation: 'fade-in-up', repeat: true }}>
|
|
385
|
+
Animates every time you scroll past it
|
|
386
|
+
</div>
|
|
559
387
|
```
|
|
560
388
|
|
|
561
389
|
---
|
|
@@ -568,17 +396,17 @@ Animate cards with progressive delays:
|
|
|
568
396
|
|
|
569
397
|
```svelte
|
|
570
398
|
<script>
|
|
571
|
-
import
|
|
399
|
+
import runeScroller from 'rune-scroller';
|
|
572
400
|
</script>
|
|
573
401
|
|
|
574
402
|
<div class="grid">
|
|
575
403
|
{#each items as item, i}
|
|
576
|
-
<
|
|
404
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 800 + i * 100 }}>
|
|
577
405
|
<div class="card">
|
|
578
406
|
<h3>{item.title}</h3>
|
|
579
407
|
<p>{item.description}</p>
|
|
580
408
|
</div>
|
|
581
|
-
</
|
|
409
|
+
</div>
|
|
582
410
|
{/each}
|
|
583
411
|
</div>
|
|
584
412
|
```
|
|
@@ -586,40 +414,42 @@ Animate cards with progressive delays:
|
|
|
586
414
|
### Mixed Animations
|
|
587
415
|
|
|
588
416
|
```svelte
|
|
589
|
-
<
|
|
590
|
-
|
|
591
|
-
</
|
|
417
|
+
<script>
|
|
418
|
+
import runeScroller from 'rune-scroller';
|
|
419
|
+
</script>
|
|
592
420
|
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
</
|
|
421
|
+
<section use:runeScroller={{ animation: 'fade-in' }}>
|
|
422
|
+
Content fades in
|
|
423
|
+
</section>
|
|
424
|
+
|
|
425
|
+
<section use:runeScroller={{ animation: 'slide-rotate' }}>
|
|
426
|
+
Content slides and rotates
|
|
427
|
+
</section>
|
|
596
428
|
|
|
597
|
-
<
|
|
598
|
-
|
|
599
|
-
</
|
|
429
|
+
<section use:runeScroller={{ animation: 'zoom-in', repeat: true }}>
|
|
430
|
+
Content zooms in repeatedly
|
|
431
|
+
</section>
|
|
600
432
|
```
|
|
601
433
|
|
|
602
434
|
### Hero Section
|
|
603
435
|
|
|
604
436
|
```svelte
|
|
605
437
|
<script>
|
|
606
|
-
import
|
|
438
|
+
import runeScroller from 'rune-scroller';
|
|
607
439
|
</script>
|
|
608
440
|
|
|
609
441
|
<section class="hero">
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
</Rs>
|
|
622
|
-
</div>
|
|
442
|
+
<h1 use:runeScroller={{ animation: 'fade-in', duration: 1000 }}>
|
|
443
|
+
Welcome
|
|
444
|
+
</h1>
|
|
445
|
+
|
|
446
|
+
<p use:runeScroller={{ animation: 'fade-in', duration: 1200 }}>
|
|
447
|
+
Scroll to reveal more
|
|
448
|
+
</p>
|
|
449
|
+
|
|
450
|
+
<button use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
|
|
451
|
+
Get Started
|
|
452
|
+
</button>
|
|
623
453
|
</section>
|
|
624
454
|
```
|
|
625
455
|
|
|
@@ -653,7 +483,7 @@ function runeScroller(
|
|
|
653
483
|
|
|
654
484
|
```svelte
|
|
655
485
|
<script>
|
|
656
|
-
import
|
|
486
|
+
import runeScroller from 'rune-scroller';
|
|
657
487
|
</script>
|
|
658
488
|
|
|
659
489
|
<!-- Animation plays once when sentinel enters viewport -->
|
|
@@ -675,7 +505,7 @@ function runeScroller(
|
|
|
675
505
|
|
|
676
506
|
```svelte
|
|
677
507
|
<script>
|
|
678
|
-
import
|
|
508
|
+
import runeScroller from 'rune-scroller';
|
|
679
509
|
</script>
|
|
680
510
|
|
|
681
511
|
<!-- Fade in once on scroll -->
|
|
@@ -713,7 +543,6 @@ function runeScroller(
|
|
|
713
543
|
- ✅ Consistent timing across layouts
|
|
714
544
|
- ✅ Minimal overhead applications
|
|
715
545
|
- ✅ Both one-time and repeating animations
|
|
716
|
-
- ❌ Complex layout with component isolation (use `Rs` component instead)
|
|
717
546
|
|
|
718
547
|
---
|
|
719
548
|
|
|
@@ -789,13 +618,9 @@ function animate(
|
|
|
789
618
|
2. **dom-utils.svelte.ts** - Reusable DOM manipulation utilities (CSS variables, animation setup, sentinel creation)
|
|
790
619
|
3. **useIntersection.svelte.ts** - IntersectionObserver composables for element visibility detection
|
|
791
620
|
|
|
792
|
-
**Middle Layer - Base Implementation:**
|
|
793
|
-
4. **animate.svelte.ts** - Action for direct DOM node animation control
|
|
794
|
-
5. **runeScroller.svelte.ts** - **Recommended** - Sentinel-based action for precise animation timing
|
|
795
|
-
6. **BaseAnimated.svelte** - Base component handling intersection observer + animation logic
|
|
796
|
-
|
|
797
621
|
**Top Layer - Consumer API:**
|
|
798
|
-
|
|
622
|
+
4. **runeScroller.svelte.ts** - **Recommended** - Sentinel-based action for scroll animation triggering
|
|
623
|
+
5. **animate.svelte.ts** - Alternative action for direct DOM node animation control
|
|
799
624
|
|
|
800
625
|
**Styles:**
|
|
801
626
|
- **animations.css** - All animation keyframes & styles (14 animations, GPU-accelerated)
|
|
@@ -903,6 +728,8 @@ pnpm preview
|
|
|
903
728
|
|
|
904
729
|
## 🔗 Links
|
|
905
730
|
|
|
731
|
+
- [Live Demo](https://runescroller.lelab.dev/) - Interactive showcase of all animations
|
|
732
|
+
- [npm Package](https://www.npmjs.com/package/rune-scroller) - Install from npm
|
|
906
733
|
- [Svelte 5 Documentation](https://svelte.dev)
|
|
907
734
|
- [IntersectionObserver MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
|
|
908
735
|
- [LeLab.dev](https://lelab.dev)
|
package/dist/animate.svelte.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { Action } from 'svelte/action';
|
|
2
|
-
import {
|
|
3
|
-
export interface AnimateOptions {
|
|
4
|
-
animation?: AnimationType;
|
|
5
|
-
duration?: number;
|
|
6
|
-
delay?: number;
|
|
7
|
-
offset?: number;
|
|
8
|
-
threshold?: number;
|
|
9
|
-
rootMargin?: string;
|
|
10
|
-
}
|
|
2
|
+
import type { AnimateOptions } from './types';
|
|
11
3
|
/**
|
|
12
4
|
* Svelte action for scroll animations
|
|
13
5
|
* Triggers animation once when element enters viewport
|
package/dist/animate.svelte.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { calculateRootMargin } from './animations';
|
|
2
|
+
import { setCSSVariables, setupAnimationElement } from './dom-utils.svelte';
|
|
2
3
|
/**
|
|
3
4
|
* Svelte action for scroll animations
|
|
4
5
|
* Triggers animation once when element enters viewport
|
|
@@ -11,17 +12,15 @@ import { calculateRootMargin } from './animations';
|
|
|
11
12
|
* ```
|
|
12
13
|
*/
|
|
13
14
|
export const animate = (node, options = {}) => {
|
|
14
|
-
|
|
15
|
+
let { animation = 'fade-in', duration = 800, delay = 0, offset, threshold = 0, rootMargin } = options;
|
|
15
16
|
// Calculate rootMargin from offset (0-100%)
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
node
|
|
19
|
-
node
|
|
20
|
-
// Add base animation class and data attribute
|
|
21
|
-
node.classList.add('scroll-animate');
|
|
22
|
-
node.setAttribute('data-animation', animation);
|
|
17
|
+
let finalRootMargin = calculateRootMargin(offset, rootMargin);
|
|
18
|
+
// Setup animation with utilities
|
|
19
|
+
setupAnimationElement(node, animation);
|
|
20
|
+
setCSSVariables(node, duration, delay);
|
|
23
21
|
// Track if animation has been triggered
|
|
24
22
|
let animated = false;
|
|
23
|
+
let observerConnected = true;
|
|
25
24
|
// Create IntersectionObserver for one-time animation
|
|
26
25
|
const observer = new IntersectionObserver((entries) => {
|
|
27
26
|
entries.forEach((entry) => {
|
|
@@ -31,6 +30,7 @@ export const animate = (node, options = {}) => {
|
|
|
31
30
|
animated = true;
|
|
32
31
|
// Stop observing after animation triggers
|
|
33
32
|
observer.unobserve(node);
|
|
33
|
+
observerConnected = false;
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
}, {
|
|
@@ -40,20 +40,40 @@ export const animate = (node, options = {}) => {
|
|
|
40
40
|
observer.observe(node);
|
|
41
41
|
return {
|
|
42
42
|
update(newOptions) {
|
|
43
|
-
const { duration: newDuration
|
|
43
|
+
const { duration: newDuration, delay: newDelay, animation: newAnimation, offset: newOffset, threshold: newThreshold, rootMargin: newRootMargin } = newOptions;
|
|
44
44
|
// Update CSS properties
|
|
45
|
-
if (newDuration !==
|
|
46
|
-
|
|
45
|
+
if (newDuration !== undefined) {
|
|
46
|
+
duration = newDuration;
|
|
47
|
+
setCSSVariables(node, duration, newDelay ?? delay);
|
|
47
48
|
}
|
|
48
|
-
if (newDelay !== delay) {
|
|
49
|
-
|
|
49
|
+
if (newDelay !== undefined && newDelay !== delay) {
|
|
50
|
+
delay = newDelay;
|
|
51
|
+
setCSSVariables(node, duration, delay);
|
|
50
52
|
}
|
|
51
53
|
if (newAnimation && newAnimation !== animation) {
|
|
54
|
+
animation = newAnimation;
|
|
52
55
|
node.setAttribute('data-animation', newAnimation);
|
|
53
56
|
}
|
|
57
|
+
// Recreate observer if threshold or rootMargin changed
|
|
58
|
+
if (newThreshold !== undefined || newOffset !== undefined || newRootMargin !== undefined) {
|
|
59
|
+
if (observerConnected) {
|
|
60
|
+
observer.disconnect();
|
|
61
|
+
observerConnected = false;
|
|
62
|
+
}
|
|
63
|
+
threshold = newThreshold ?? threshold;
|
|
64
|
+
offset = newOffset ?? offset;
|
|
65
|
+
rootMargin = newRootMargin ?? rootMargin;
|
|
66
|
+
finalRootMargin = calculateRootMargin(offset, rootMargin);
|
|
67
|
+
if (!animated) {
|
|
68
|
+
observer.observe(node);
|
|
69
|
+
observerConnected = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
54
72
|
},
|
|
55
73
|
destroy() {
|
|
56
|
-
|
|
74
|
+
if (observerConnected) {
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
}
|
|
57
77
|
}
|
|
58
78
|
};
|
|
59
79
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnimationType } from './animations';
|
|
2
|
+
/**
|
|
3
|
+
* Set CSS custom properties on an element
|
|
4
|
+
* @param element - Target DOM element
|
|
5
|
+
* @param duration - Animation duration in milliseconds
|
|
6
|
+
* @param delay - Animation delay in milliseconds
|
|
7
|
+
*/
|
|
8
|
+
export declare function setCSSVariables(element: HTMLElement, duration?: number, delay?: number): void;
|
|
9
|
+
/**
|
|
10
|
+
* Setup animation element with required classes and attributes
|
|
11
|
+
* @param element - Target DOM element
|
|
12
|
+
* @param animation - Animation type to apply
|
|
13
|
+
*/
|
|
14
|
+
export declare function setupAnimationElement(element: HTMLElement, animation: AnimationType): void;
|
|
15
|
+
/**
|
|
16
|
+
* Create sentinel element for observer-based triggering
|
|
17
|
+
* Positioned absolutely after element, stays fixed while element animates
|
|
18
|
+
* @param element - Reference element (used to position sentinel at its bottom)
|
|
19
|
+
* @param debug - If true, shows the sentinel as a visible line for debugging
|
|
20
|
+
* @returns The created sentinel element
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSentinel(element: HTMLElement, debug?: boolean): HTMLElement;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set CSS custom properties on an element
|
|
3
|
+
* @param element - Target DOM element
|
|
4
|
+
* @param duration - Animation duration in milliseconds
|
|
5
|
+
* @param delay - Animation delay in milliseconds
|
|
6
|
+
*/
|
|
7
|
+
export function setCSSVariables(element, duration, delay = 0) {
|
|
8
|
+
if (duration !== undefined) {
|
|
9
|
+
element.style.setProperty('--duration', `${duration}ms`);
|
|
10
|
+
}
|
|
11
|
+
element.style.setProperty('--delay', `${delay}ms`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Setup animation element with required classes and attributes
|
|
15
|
+
* @param element - Target DOM element
|
|
16
|
+
* @param animation - Animation type to apply
|
|
17
|
+
*/
|
|
18
|
+
export function setupAnimationElement(element, animation) {
|
|
19
|
+
element.classList.add('scroll-animate');
|
|
20
|
+
element.setAttribute('data-animation', animation);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create sentinel element for observer-based triggering
|
|
24
|
+
* Positioned absolutely after element, stays fixed while element animates
|
|
25
|
+
* @param element - Reference element (used to position sentinel at its bottom)
|
|
26
|
+
* @param debug - If true, shows the sentinel as a visible line for debugging
|
|
27
|
+
* @returns The created sentinel element
|
|
28
|
+
*/
|
|
29
|
+
export function createSentinel(element, debug = false) {
|
|
30
|
+
const sentinel = document.createElement('div');
|
|
31
|
+
// Get element dimensions to position sentinel at its bottom
|
|
32
|
+
const rect = element.getBoundingClientRect();
|
|
33
|
+
const elementHeight = rect.height;
|
|
34
|
+
if (debug) {
|
|
35
|
+
// Debug mode: visible primary color line (cyan #00e0ff)
|
|
36
|
+
sentinel.style.cssText =
|
|
37
|
+
`position:absolute;top:${elementHeight}px;left:0;right:0;height:3px;background:#00e0ff;margin:0;padding:0;box-sizing:border-box;z-index:999;pointer-events:none`;
|
|
38
|
+
sentinel.setAttribute('data-sentinel-debug', 'true');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Production: invisible positioned absolutely (no layout impact)
|
|
42
|
+
sentinel.style.cssText =
|
|
43
|
+
`position:absolute;top:${elementHeight}px;left:0;right:0;height:1px;visibility:hidden;margin:0;padding:0;box-sizing:border-box;pointer-events:none`;
|
|
44
|
+
}
|
|
45
|
+
return sentinel;
|
|
46
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { RuneScrollerOptions, AnimateOptions, IntersectionOptions, UseIntersectionReturn } from './types';
|
|
2
|
+
export type { AnimationType } from './animations';
|
|
3
|
+
export { runeScroller as default } from './runeScroller.svelte.ts';
|
|
4
|
+
export { default as RuneScroller } from './runeScroller.svelte';
|
|
2
5
|
export { animate } from './animate.svelte';
|
|
3
|
-
export type { AnimateOptions } from './animate.svelte';
|
|
4
|
-
export { runeScroller } from './runeScroller.svelte';
|
|
5
|
-
export type { RuneScrollerOptions } from './runeScroller.svelte';
|
|
6
6
|
export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
|
|
7
|
-
export type { IntersectionOptions, UseIntersectionReturn } from './useIntersection.svelte';
|
|
8
7
|
export { calculateRootMargin } from './animations';
|
|
9
|
-
export type { AnimationType } from './animations';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
export {
|
|
3
|
-
//
|
|
1
|
+
// Main action (default export)
|
|
2
|
+
export { runeScroller as default } from './runeScroller.svelte.ts';
|
|
3
|
+
// Component
|
|
4
|
+
export { default as RuneScroller } from './runeScroller.svelte';
|
|
5
|
+
// Alternative actions
|
|
4
6
|
export { animate } from './animate.svelte';
|
|
5
|
-
export { runeScroller } from './runeScroller.svelte';
|
|
6
7
|
// Composables
|
|
7
8
|
export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
|
|
8
9
|
// Utilities
|
|
@@ -1,29 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { AnimationType } from './animations';
|
|
2
|
-
|
|
3
|
+
interface Props {
|
|
3
4
|
animation?: AnimationType;
|
|
5
|
+
threshold?: number;
|
|
6
|
+
rootMargin?: string;
|
|
7
|
+
offset?: number;
|
|
4
8
|
duration?: number;
|
|
9
|
+
delay?: number;
|
|
5
10
|
repeat?: boolean;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
[key: string]: any;
|
|
6
13
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @param options - Options d'animation (animation type, duration, et repeat)
|
|
11
|
-
* @returns Objet action Svelte
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```svelte
|
|
15
|
-
* <!-- Animation une seule fois -->
|
|
16
|
-
* <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
|
|
17
|
-
* Content
|
|
18
|
-
* </div>
|
|
19
|
-
*
|
|
20
|
-
* <!-- Animation répétée à chaque scroll -->
|
|
21
|
-
* <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000, repeat: true }}>
|
|
22
|
-
* Content
|
|
23
|
-
* </div>
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export declare function runeScroller(element: HTMLElement, options?: RuneScrollerOptions): {
|
|
27
|
-
update(newOptions?: RuneScrollerOptions): void;
|
|
28
|
-
destroy(): void;
|
|
29
|
-
};
|
|
14
|
+
declare const RuneScroller: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type RuneScroller = ReturnType<typeof RuneScroller>;
|
|
16
|
+
export default RuneScroller;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setCSSVariables, setupAnimationElement, createSentinel } from './dom-utils.svelte';
|
|
1
2
|
/**
|
|
2
3
|
* Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
|
|
3
4
|
* @param element - L'élément à animer
|
|
@@ -18,27 +19,26 @@
|
|
|
18
19
|
* ```
|
|
19
20
|
*/
|
|
20
21
|
export function runeScroller(element, options) {
|
|
21
|
-
// Setup animation classes et variables CSS
|
|
22
|
+
// Setup animation classes et variables CSS
|
|
22
23
|
if (options?.animation || options?.duration) {
|
|
23
|
-
element.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
setupAnimationElement(element, options.animation);
|
|
25
|
+
setCSSVariables(element, options.duration);
|
|
26
|
+
}
|
|
27
|
+
// Créer le sentinel invisible (ou visible si debug=true)
|
|
28
|
+
// Sentinel positioned absolutely relative to parent (stays fixed while element animates)
|
|
29
|
+
const sentinel = createSentinel(element, options?.debug);
|
|
30
|
+
const parent = element.parentElement;
|
|
31
|
+
if (parent) {
|
|
32
|
+
// Ensure parent has position context for absolute positioning
|
|
33
|
+
const parentPosition = window.getComputedStyle(parent).position;
|
|
34
|
+
if (parentPosition === 'static') {
|
|
35
|
+
parent.style.position = 'relative';
|
|
29
36
|
}
|
|
30
|
-
element
|
|
37
|
+
// Insert sentinel after element, positioned absolutely
|
|
38
|
+
element.insertAdjacentElement('afterend', sentinel);
|
|
31
39
|
}
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
sentinel.style.height = '20px';
|
|
35
|
-
sentinel.style.margin = '0';
|
|
36
|
-
sentinel.style.padding = '0';
|
|
37
|
-
sentinel.style.marginTop = '0.5rem';
|
|
38
|
-
sentinel.style.visibility = 'hidden';
|
|
39
|
-
// Insérer le sentinel après l'élément
|
|
40
|
-
element.parentNode?.insertBefore(sentinel, element.nextSibling);
|
|
41
|
-
// Observer le sentinel
|
|
40
|
+
// Observer le sentinel avec cleanup tracking
|
|
41
|
+
let observerConnected = true;
|
|
42
42
|
const observer = new IntersectionObserver((entries) => {
|
|
43
43
|
const isIntersecting = entries[0].isIntersecting;
|
|
44
44
|
if (isIntersecting) {
|
|
@@ -47,6 +47,7 @@ export function runeScroller(element, options) {
|
|
|
47
47
|
// Déconnecter si pas en mode repeat
|
|
48
48
|
if (!options?.repeat) {
|
|
49
49
|
observer.disconnect();
|
|
50
|
+
observerConnected = false;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
else if (options?.repeat) {
|
|
@@ -61,7 +62,7 @@ export function runeScroller(element, options) {
|
|
|
61
62
|
element.setAttribute('data-animation', newOptions.animation);
|
|
62
63
|
}
|
|
63
64
|
if (newOptions?.duration) {
|
|
64
|
-
element
|
|
65
|
+
setCSSVariables(element, newOptions.duration);
|
|
65
66
|
}
|
|
66
67
|
// Update repeat option
|
|
67
68
|
if (newOptions?.repeat !== undefined && newOptions.repeat !== options?.repeat) {
|
|
@@ -69,7 +70,9 @@ export function runeScroller(element, options) {
|
|
|
69
70
|
}
|
|
70
71
|
},
|
|
71
72
|
destroy() {
|
|
72
|
-
|
|
73
|
+
if (observerConnected) {
|
|
74
|
+
observer.disconnect();
|
|
75
|
+
}
|
|
73
76
|
sentinel.remove();
|
|
74
77
|
}
|
|
75
78
|
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized type definitions for Rune Scroller library
|
|
3
|
+
* All types are defined here for consistency and ease of maintenance
|
|
4
|
+
*/
|
|
5
|
+
import type { AnimationType } from './animations';
|
|
6
|
+
/**
|
|
7
|
+
* Options for the runeScroller action
|
|
8
|
+
* Sentinel-based scroll animation triggering
|
|
9
|
+
*/
|
|
10
|
+
export interface RuneScrollerOptions {
|
|
11
|
+
animation?: AnimationType;
|
|
12
|
+
duration?: number;
|
|
13
|
+
repeat?: boolean;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options for the animate action
|
|
18
|
+
* Direct DOM node animation control with threshold/offset support
|
|
19
|
+
*/
|
|
20
|
+
export interface AnimateOptions {
|
|
21
|
+
animation?: AnimationType;
|
|
22
|
+
duration?: number;
|
|
23
|
+
delay?: number;
|
|
24
|
+
offset?: number;
|
|
25
|
+
threshold?: number;
|
|
26
|
+
rootMargin?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configuration options for IntersectionObserver
|
|
30
|
+
* Used by useIntersection and useIntersectionOnce composables
|
|
31
|
+
*/
|
|
32
|
+
export interface IntersectionOptions {
|
|
33
|
+
threshold?: number | number[];
|
|
34
|
+
rootMargin?: string;
|
|
35
|
+
root?: Element | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Return type for useIntersection and useIntersectionOnce composables
|
|
39
|
+
* Provides reactive element reference and visibility state
|
|
40
|
+
*/
|
|
41
|
+
export interface UseIntersectionReturn {
|
|
42
|
+
element: HTMLElement | null;
|
|
43
|
+
isVisible: boolean;
|
|
44
|
+
}
|
package/dist/types.js
ADDED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Composable for handling IntersectionObserver logic
|
|
3
|
-
* Reduces duplication between animation components
|
|
4
|
-
*/
|
|
5
|
-
export interface IntersectionOptions {
|
|
6
|
-
threshold?: number | number[];
|
|
7
|
-
rootMargin?: string;
|
|
8
|
-
root?: Element | null;
|
|
9
|
-
}
|
|
10
|
-
export interface UseIntersectionReturn {
|
|
11
|
-
element: HTMLElement | null;
|
|
12
|
-
isVisible: boolean;
|
|
13
|
-
}
|
|
1
|
+
import type { IntersectionOptions } from './types';
|
|
14
2
|
/**
|
|
15
3
|
* Track element visibility with IntersectionObserver
|
|
16
4
|
* Updates isVisible whenever visibility changes
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { onMount } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Composable for handling IntersectionObserver logic
|
|
4
|
+
* Reduces duplication between animation components
|
|
5
|
+
*/
|
|
2
6
|
/**
|
|
3
7
|
* Factory function to create intersection observer composables
|
|
4
8
|
* Eliminates duplication between useIntersection and useIntersectionOnce
|
package/package.json
CHANGED
package/dist/Rs.svelte.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { AnimationType } from './animations';
|
|
3
|
-
interface Props {
|
|
4
|
-
animation?: AnimationType;
|
|
5
|
-
threshold?: number;
|
|
6
|
-
rootMargin?: string;
|
|
7
|
-
offset?: number;
|
|
8
|
-
duration?: number;
|
|
9
|
-
delay?: number;
|
|
10
|
-
repeat?: boolean;
|
|
11
|
-
children: Snippet;
|
|
12
|
-
[key: string]: any;
|
|
13
|
-
}
|
|
14
|
-
declare const Rs: import("svelte").Component<Props, {}, "">;
|
|
15
|
-
type Rs = ReturnType<typeof Rs>;
|
|
16
|
-
export default Rs;
|
|
File without changes
|