rune-scroller 0.0.2 → 0.1.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 +339 -52
- package/dist/animate.svelte.js +34 -14
- package/dist/animations.d.ts +1 -1
- package/dist/animations.js +1 -1
- package/dist/dom-utils.svelte.d.ts +20 -0
- package/dist/dom-utils.svelte.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/runeScroller.svelte.d.ts +29 -0
- package/dist/runeScroller.svelte.js +68 -0
- package/package.json +1 -2
- package/dist/assets/favicon.svg +0 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- ✅ **Svelte 5 Runes** : `$state`, `$props()` with snippets
|
|
16
16
|
- ✅ **Zero Dependencies** : Pure Svelte 5 + IntersectionObserver
|
|
17
17
|
- ✅ **Native Performance** : GPU-accelerated CSS animations
|
|
18
|
-
- ✅ **
|
|
18
|
+
- ✅ **14 Built-in Animations** : Fade (5), Zoom (5), Flip (2), Slide Rotate, Bounce
|
|
19
19
|
- ✅ **TypeScript** : Full type coverage with strict mode
|
|
20
20
|
- ✅ **Customizable** : Duration, delay, threshold, offset per element
|
|
21
21
|
- ✅ **Play Once or Repeat** : Control animation behavior
|
|
@@ -55,19 +55,24 @@ For a typical SvelteKit app:
|
|
|
55
55
|
## 📦 Project Structure
|
|
56
56
|
|
|
57
57
|
```
|
|
58
|
-
rune-scroller/
|
|
58
|
+
rune-scroller-lib/
|
|
59
59
|
├── src/lib/
|
|
60
60
|
│ ├── Rs.svelte # Main animation component (one-time or repeat)
|
|
61
|
-
│ ├── BaseAnimated.svelte # Base
|
|
62
|
-
│ ├──
|
|
63
|
-
│ ├──
|
|
64
|
-
│ ├──
|
|
61
|
+
│ ├── BaseAnimated.svelte # Base animation implementation
|
|
62
|
+
│ ├── runeScroller.svelte.ts # Sentinel-based animation action (recommended)
|
|
63
|
+
│ ├── useIntersection.svelte.ts # IntersectionObserver composables
|
|
64
|
+
│ ├── animate.svelte.ts # Animation action for direct DOM control
|
|
65
|
+
│ ├── animations.ts # Animation configuration & validation
|
|
66
|
+
│ ├── animations.css # Animation styles (14 animations)
|
|
67
|
+
│ ├── animations.test.ts # Animation configuration tests
|
|
68
|
+
│ ├── scroll-animate.test.ts # Component behavior tests
|
|
65
69
|
│ └── index.ts # Library entry point
|
|
66
|
-
├──
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
├── dist/ # Built library (created by pnpm build)
|
|
71
|
+
├── package.json # npm package configuration
|
|
72
|
+
├── svelte.config.js # SvelteKit configuration
|
|
73
|
+
├── vite.config.ts # Vite build configuration
|
|
74
|
+
├── tsconfig.json # TypeScript configuration
|
|
75
|
+
└── eslint.config.js # ESLint configuration
|
|
71
76
|
```
|
|
72
77
|
|
|
73
78
|
---
|
|
@@ -153,6 +158,107 @@ The `Rs` component is the unified API for all scroll animations:
|
|
|
153
158
|
|
|
154
159
|
---
|
|
155
160
|
|
|
161
|
+
## 🎯 Sentinel-Based Animation Triggering with `runeScroller`
|
|
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.
|
|
164
|
+
|
|
165
|
+
### Why Sentinels?
|
|
166
|
+
|
|
167
|
+
- **Accurate Timing** - Instead of triggering when the element enters, sentinel triggers slightly earlier
|
|
168
|
+
- **Consistent Behavior** - Same timing across all screen sizes and content heights
|
|
169
|
+
- **Simple API** - No complex offset calculations needed
|
|
170
|
+
- **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
|
+
```
|
|
204
|
+
|
|
205
|
+
### Sentinel-Based Examples
|
|
206
|
+
|
|
207
|
+
**Staggered animations with sentinels:**
|
|
208
|
+
|
|
209
|
+
```svelte
|
|
210
|
+
<script>
|
|
211
|
+
import { runeScroller } from 'rune-scroller';
|
|
212
|
+
</script>
|
|
213
|
+
|
|
214
|
+
<div class="grid">
|
|
215
|
+
{#each items as item, i}
|
|
216
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
|
|
217
|
+
<h3>{item.title}</h3>
|
|
218
|
+
<p>{item.description}</p>
|
|
219
|
+
</div>
|
|
220
|
+
{/each}
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Hero section with sentinel triggering:**
|
|
225
|
+
|
|
226
|
+
```svelte
|
|
227
|
+
<div use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>
|
|
228
|
+
<h1>Welcome to Our Site</h1>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>
|
|
232
|
+
<p>Engaging content appears as you scroll</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
|
|
236
|
+
<button class="cta">Get Started</button>
|
|
237
|
+
</div>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `runeScroller` Options
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
interface RuneScrollerOptions {
|
|
244
|
+
animation?: AnimationType; // Animation type (e.g., 'fade-in-up')
|
|
245
|
+
duration?: number; // Duration in milliseconds (default: 2000)
|
|
246
|
+
repeat?: boolean; // Repeat animation on each scroll (default: false)
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
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
|
+
|
|
156
262
|
## ⚙️ Component Props
|
|
157
263
|
|
|
158
264
|
### Rs Component
|
|
@@ -303,7 +409,7 @@ Fades in while moving right 100px.
|
|
|
303
409
|
|
|
304
410
|
---
|
|
305
411
|
|
|
306
|
-
### Zoom (
|
|
412
|
+
### Zoom (5 variants)
|
|
307
413
|
|
|
308
414
|
#### `zoom-in`
|
|
309
415
|
|
|
@@ -331,6 +437,45 @@ Scales from 150% to 100% while fading in.
|
|
|
331
437
|
</Rs>
|
|
332
438
|
```
|
|
333
439
|
|
|
440
|
+
#### `zoom-in-up`
|
|
441
|
+
|
|
442
|
+
Scales from 50% while translating up 50px.
|
|
443
|
+
|
|
444
|
+
```svelte
|
|
445
|
+
<Rs animation="zoom-in-up">
|
|
446
|
+
<div class="card">
|
|
447
|
+
<h2>Zoom In Up</h2>
|
|
448
|
+
<p>Grows while moving up</p>
|
|
449
|
+
</div>
|
|
450
|
+
</Rs>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### `zoom-in-left`
|
|
454
|
+
|
|
455
|
+
Scales from 50% while translating left 50px.
|
|
456
|
+
|
|
457
|
+
```svelte
|
|
458
|
+
<Rs animation="zoom-in-left">
|
|
459
|
+
<div class="card">
|
|
460
|
+
<h2>Zoom In Left</h2>
|
|
461
|
+
<p>Grows while moving left</p>
|
|
462
|
+
</div>
|
|
463
|
+
</Rs>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### `zoom-in-right`
|
|
467
|
+
|
|
468
|
+
Scales from 50% while translating right 50px.
|
|
469
|
+
|
|
470
|
+
```svelte
|
|
471
|
+
<Rs animation="zoom-in-right">
|
|
472
|
+
<div class="card">
|
|
473
|
+
<h2>Zoom In Right</h2>
|
|
474
|
+
<p>Grows while moving right</p>
|
|
475
|
+
</div>
|
|
476
|
+
</Rs>
|
|
477
|
+
```
|
|
478
|
+
|
|
334
479
|
---
|
|
335
480
|
|
|
336
481
|
### Flip (2 variants)
|
|
@@ -480,65 +625,115 @@ Animate cards with progressive delays:
|
|
|
480
625
|
|
|
481
626
|
---
|
|
482
627
|
|
|
483
|
-
##
|
|
628
|
+
## 🔧 Composables & Actions
|
|
484
629
|
|
|
485
|
-
|
|
630
|
+
### runeScroller (Recommended)
|
|
486
631
|
|
|
487
|
-
|
|
632
|
+
The `runeScroller` action provides sentinel-based animation triggering for precise timing control:
|
|
488
633
|
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
634
|
+
```typescript
|
|
635
|
+
function runeScroller(
|
|
636
|
+
element: HTMLElement,
|
|
637
|
+
options?: {
|
|
638
|
+
animation?: AnimationType; // Animation type
|
|
639
|
+
duration?: number; // Duration in ms (default: 2000)
|
|
640
|
+
repeat?: boolean; // Repeat animation on each scroll (default: false)
|
|
641
|
+
}
|
|
642
|
+
): { update?: (newOptions) => void; destroy?: () => void }
|
|
496
643
|
```
|
|
497
644
|
|
|
498
|
-
|
|
645
|
+
**Key Features:**
|
|
646
|
+
- Automatically creates an invisible 20px sentinel element below your content
|
|
647
|
+
- Triggers animation when sentinel enters viewport
|
|
648
|
+
- Provides consistent timing across all screen sizes
|
|
649
|
+
- Minimal configuration needed
|
|
650
|
+
- Supports both one-time and repeating animations
|
|
651
|
+
|
|
652
|
+
**Basic Example (One-time animation):**
|
|
499
653
|
|
|
500
654
|
```svelte
|
|
501
655
|
<script>
|
|
502
|
-
import
|
|
656
|
+
import { runeScroller } from 'rune-scroller';
|
|
503
657
|
</script>
|
|
504
658
|
|
|
505
|
-
<!--
|
|
506
|
-
<
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
659
|
+
<!-- Animation plays once when sentinel enters viewport -->
|
|
660
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
|
|
661
|
+
Animated content with sentinel-based triggering
|
|
662
|
+
</div>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Repeating Animation:**
|
|
666
|
+
|
|
667
|
+
```svelte
|
|
668
|
+
<!-- Animation repeats each time sentinel enters viewport -->
|
|
669
|
+
<div use:runeScroller={{ animation: 'bounce-in', duration: 800, repeat: true }}>
|
|
670
|
+
This animates every time you scroll past it
|
|
671
|
+
</div>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Complete Examples:**
|
|
675
|
+
|
|
676
|
+
```svelte
|
|
677
|
+
<script>
|
|
678
|
+
import { runeScroller } from 'rune-scroller';
|
|
679
|
+
</script>
|
|
680
|
+
|
|
681
|
+
<!-- Fade in once on scroll -->
|
|
682
|
+
<div use:runeScroller={{ animation: 'fade-in', duration: 600 }}>
|
|
683
|
+
<h2>Section Title</h2>
|
|
684
|
+
<p>Fades in when scrolled into view</p>
|
|
685
|
+
</div>
|
|
512
686
|
|
|
513
|
-
<!--
|
|
514
|
-
<
|
|
687
|
+
<!-- Zoom in with longer duration -->
|
|
688
|
+
<div use:runeScroller={{ animation: 'zoom-in-up', duration: 1200 }}>
|
|
515
689
|
<div class="card">
|
|
516
|
-
<h3>
|
|
690
|
+
<h3>Card Title</h3>
|
|
691
|
+
<p>Zooms in from below</p>
|
|
517
692
|
</div>
|
|
518
|
-
</
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<!-- Repeating animation for interactive effect -->
|
|
696
|
+
<div use:runeScroller={{ animation: 'bounce-in', duration: 700, repeat: true }}>
|
|
697
|
+
<button class="interactive-button">Bounces on each scroll</button>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<!-- Complex staggered layout -->
|
|
701
|
+
<div class="grid">
|
|
702
|
+
{#each items as item, i}
|
|
703
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
|
|
704
|
+
<h3>{item.title}</h3>
|
|
705
|
+
<p>{item.description}</p>
|
|
706
|
+
</div>
|
|
707
|
+
{/each}
|
|
708
|
+
</div>
|
|
519
709
|
```
|
|
520
710
|
|
|
521
|
-
|
|
711
|
+
**When to use:**
|
|
712
|
+
- ✅ Simple element animations
|
|
713
|
+
- ✅ Consistent timing across layouts
|
|
714
|
+
- ✅ Minimal overhead applications
|
|
715
|
+
- ✅ Both one-time and repeating animations
|
|
716
|
+
- ❌ Complex layout with component isolation (use `Rs` component instead)
|
|
522
717
|
|
|
523
|
-
|
|
718
|
+
---
|
|
524
719
|
|
|
525
720
|
### useIntersectionOnce
|
|
526
721
|
|
|
527
|
-
For animations
|
|
722
|
+
For one-time animations:
|
|
528
723
|
|
|
529
724
|
```typescript
|
|
530
725
|
function useIntersectionOnce(options?: {
|
|
531
726
|
threshold?: number;
|
|
532
727
|
rootMargin?: string;
|
|
533
728
|
root?: Element | null;
|
|
534
|
-
});
|
|
729
|
+
}): { element: HTMLElement | null; isVisible: boolean }
|
|
535
730
|
```
|
|
536
731
|
|
|
537
|
-
Returns `{ element, isVisible }` — bind `element` to your target, `isVisible` becomes `true` once.
|
|
732
|
+
Returns `{ element, isVisible }` — bind `element` to your target, `isVisible` becomes `true` once, then observer unobserves.
|
|
538
733
|
|
|
539
734
|
### useIntersection
|
|
540
735
|
|
|
541
|
-
For repeating animations
|
|
736
|
+
For repeating animations:
|
|
542
737
|
|
|
543
738
|
```typescript
|
|
544
739
|
function useIntersection(
|
|
@@ -548,22 +743,62 @@ function useIntersection(
|
|
|
548
743
|
root?: Element | null;
|
|
549
744
|
},
|
|
550
745
|
onVisible?: (isVisible: boolean) => void
|
|
551
|
-
);
|
|
746
|
+
): { element: HTMLElement | null; isVisible: boolean }
|
|
552
747
|
```
|
|
553
748
|
|
|
554
|
-
Returns `{ element, isVisible }` — `isVisible` toggles on each scroll.
|
|
749
|
+
Returns `{ element, isVisible }` — `isVisible` toggles `true`/`false` on each scroll pass.
|
|
750
|
+
|
|
751
|
+
### animate Action
|
|
752
|
+
|
|
753
|
+
For direct DOM animation control without component wrapper:
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
function animate(
|
|
757
|
+
node: HTMLElement,
|
|
758
|
+
options?: {
|
|
759
|
+
animation?: AnimationType; // Default: 'fade-in'
|
|
760
|
+
duration?: number; // Default: 800
|
|
761
|
+
delay?: number; // Default: 0
|
|
762
|
+
offset?: number; // Optional trigger offset
|
|
763
|
+
threshold?: number; // Default: 0
|
|
764
|
+
rootMargin?: string; // Optional custom margin
|
|
765
|
+
}
|
|
766
|
+
): { update?: (newOptions) => void; destroy?: () => void }
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Example:**
|
|
770
|
+
|
|
771
|
+
```svelte
|
|
772
|
+
<script>
|
|
773
|
+
import { animate } from 'rune-scroller';
|
|
774
|
+
</script>
|
|
775
|
+
|
|
776
|
+
<div use:animate={{ animation: 'fade-in-up', duration: 1000, delay: 200 }}>
|
|
777
|
+
Animated content
|
|
778
|
+
</div>
|
|
779
|
+
```
|
|
555
780
|
|
|
556
781
|
---
|
|
557
782
|
|
|
558
783
|
## 🏗️ Architecture
|
|
559
784
|
|
|
560
|
-
###
|
|
785
|
+
### Core Layer Architecture
|
|
561
786
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
787
|
+
**Bottom Layer - Browser APIs & Utilities:**
|
|
788
|
+
1. **animations.ts** - Animation type definitions, validation, and utilities
|
|
789
|
+
2. **dom-utils.svelte.ts** - Reusable DOM manipulation utilities (CSS variables, animation setup, sentinel creation)
|
|
790
|
+
3. **useIntersection.svelte.ts** - IntersectionObserver composables for element visibility detection
|
|
791
|
+
|
|
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
|
+
**Top Layer - Consumer API:**
|
|
798
|
+
7. **Rs.svelte** - Main unified component (supports one-time & repeating via `repeat` prop)
|
|
799
|
+
|
|
800
|
+
**Styles:**
|
|
801
|
+
- **animations.css** - All animation keyframes & styles (14 animations, GPU-accelerated)
|
|
567
802
|
|
|
568
803
|
### Key Principles
|
|
569
804
|
|
|
@@ -571,6 +806,45 @@ Returns `{ element, isVisible }` — `isVisible` toggles on each scroll.
|
|
|
571
806
|
- **CSS-Based** : Animations use CSS transforms + transitions (hardware-accelerated)
|
|
572
807
|
- **Type-Safe** : Full TypeScript support
|
|
573
808
|
- **Composable** : Use hooks directly or wrapped components
|
|
809
|
+
- **DRY (Don't Repeat Yourself)** : Utility functions eliminate code duplication
|
|
810
|
+
- **Optimal DOM Manipulation** : Uses `cssText` for efficient single-statement styling
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## 🚀 Optimizations
|
|
815
|
+
|
|
816
|
+
### Recent Improvements (v1.1.0)
|
|
817
|
+
|
|
818
|
+
**DOM Utility Extraction**
|
|
819
|
+
- Extracted repeated DOM manipulation patterns into reusable utilities (`dom-utils.svelte.ts`)
|
|
820
|
+
- `setCSSVariables()` - Centralizes CSS custom property management
|
|
821
|
+
- `setupAnimationElement()` - Consistent animation class/attribute setup
|
|
822
|
+
- `createSentinel()` - Optimized sentinel creation using single `cssText` statement
|
|
823
|
+
- **Result**: Reduced code duplication, improved maintainability, cleaner codebase
|
|
824
|
+
|
|
825
|
+
**Memory Leak Fixes**
|
|
826
|
+
- Fixed potential memory leaks in repeat mode by tracking observer connection state
|
|
827
|
+
- Observer now properly disconnects in destroy lifecycle
|
|
828
|
+
- Prevents accumulation of observers on long-scroll pages
|
|
829
|
+
- **Result**: Better performance on content-heavy sites with many animations
|
|
830
|
+
|
|
831
|
+
**Observer Logic Improvements**
|
|
832
|
+
- Fixed `animate.svelte.ts` to properly handle dynamic threshold/rootMargin changes
|
|
833
|
+
- Observer now recreates when trigger options change at runtime
|
|
834
|
+
- Maintains correct state throughout component lifecycle
|
|
835
|
+
- **Result**: More reliable dynamic animation updates
|
|
836
|
+
|
|
837
|
+
**Bundle Size Optimization**
|
|
838
|
+
- Updated `.npmignore` to exclude test files from npm distribution
|
|
839
|
+
- Removes `*.test.ts`, `*.test.js` and built test files
|
|
840
|
+
- **Result**: ~3.6 KB reduction in package size
|
|
841
|
+
|
|
842
|
+
### Performance Impact
|
|
843
|
+
|
|
844
|
+
- **Code Size**: Reduced duplication without sacrificing readability
|
|
845
|
+
- **Runtime Performance**: Fewer DOM operations via optimized `cssText` usage
|
|
846
|
+
- **Memory Efficiency**: Proper observer cleanup prevents memory leaks
|
|
847
|
+
- **Bundle Size**: Test files excluded from distribution
|
|
574
848
|
|
|
575
849
|
---
|
|
576
850
|
|
|
@@ -596,10 +870,22 @@ pnpm dev
|
|
|
596
870
|
# Type checking
|
|
597
871
|
pnpm check
|
|
598
872
|
|
|
873
|
+
# Type checking in watch mode
|
|
874
|
+
pnpm check:watch
|
|
875
|
+
|
|
599
876
|
# Format code
|
|
600
877
|
pnpm format
|
|
601
878
|
|
|
602
|
-
#
|
|
879
|
+
# Lint code
|
|
880
|
+
pnpm lint
|
|
881
|
+
|
|
882
|
+
# Build library for npm
|
|
883
|
+
pnpm build
|
|
884
|
+
|
|
885
|
+
# Run tests
|
|
886
|
+
pnpm test
|
|
887
|
+
|
|
888
|
+
# Preview built library
|
|
603
889
|
pnpm preview
|
|
604
890
|
```
|
|
605
891
|
|
|
@@ -608,9 +894,10 @@ pnpm preview
|
|
|
608
894
|
## 📝 Notes
|
|
609
895
|
|
|
610
896
|
- **Why "Rune"?** Svelte 5 uses **Runes** (`$state`, `$props()`) as core reactivity primitives
|
|
611
|
-
- **
|
|
612
|
-
- **No Dependencies** : Pure Svelte 5 + Browser APIs
|
|
897
|
+
- **Zero Dependencies** : Pure Svelte 5 + Native Browser APIs (IntersectionObserver)
|
|
613
898
|
- **Extensible** : Add new animations by extending `animations.ts` and `animations.css`
|
|
899
|
+
- **Library Only** : This is the library repository. The demo website is in `rune-scroller-site`
|
|
900
|
+
- **Published as npm Package** : `rune-scroller` on npm registry
|
|
614
901
|
|
|
615
902
|
---
|
|
616
903
|
|
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
|
};
|
package/dist/animations.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
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';
|
|
9
9
|
/**
|
|
10
10
|
* Calculate rootMargin for IntersectionObserver from offset or custom rootMargin
|
|
11
|
-
* @param offset - Viewport offset (0-100). 0 = bottom
|
|
11
|
+
* @param offset - Viewport offset (0-100). 0 = bottom of viewport touches top of element, 100 = top of viewport touches top of element
|
|
12
12
|
* @param rootMargin - Custom rootMargin string (takes precedence over offset)
|
|
13
13
|
* @returns rootMargin string for IntersectionObserver
|
|
14
14
|
*/
|
package/dist/animations.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* Calculate rootMargin for IntersectionObserver from offset or custom rootMargin
|
|
6
|
-
* @param offset - Viewport offset (0-100). 0 = bottom
|
|
6
|
+
* @param offset - Viewport offset (0-100). 0 = bottom of viewport touches top of element, 100 = top of viewport touches top of element
|
|
7
7
|
* @param rootMargin - Custom rootMargin string (takes precedence over offset)
|
|
8
8
|
* @returns rootMargin string for IntersectionObserver
|
|
9
9
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
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 and inject invisible sentinel element for observer-based triggering
|
|
17
|
+
* @param element - Reference element (sentinel will be placed after it)
|
|
18
|
+
* @returns The created sentinel element
|
|
19
|
+
*/
|
|
20
|
+
export declare function createSentinel(element: HTMLElement): HTMLElement;
|
|
@@ -0,0 +1,33 @@
|
|
|
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 and inject invisible sentinel element for observer-based triggering
|
|
24
|
+
* @param element - Reference element (sentinel will be placed after it)
|
|
25
|
+
* @returns The created sentinel element
|
|
26
|
+
*/
|
|
27
|
+
export function createSentinel(element) {
|
|
28
|
+
const sentinel = document.createElement('div');
|
|
29
|
+
// Use cssText for efficient single-statement styling
|
|
30
|
+
sentinel.style.cssText = 'height:20px;margin-top:0.5rem;visibility:hidden';
|
|
31
|
+
element.parentNode?.insertBefore(sentinel, element.nextSibling);
|
|
32
|
+
return sentinel;
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as rs } from './Rs.svelte';
|
|
2
2
|
export { animate } from './animate.svelte';
|
|
3
3
|
export type { AnimateOptions } from './animate.svelte';
|
|
4
|
+
export { runeScroller } from './runeScroller.svelte';
|
|
5
|
+
export type { RuneScrollerOptions } from './runeScroller.svelte';
|
|
4
6
|
export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
|
|
5
7
|
export type { IntersectionOptions, UseIntersectionReturn } from './useIntersection.svelte';
|
|
6
8
|
export { calculateRootMargin } from './animations';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { default as rs } from './Rs.svelte';
|
|
3
3
|
// Actions
|
|
4
4
|
export { animate } from './animate.svelte';
|
|
5
|
+
export { runeScroller } from './runeScroller.svelte';
|
|
5
6
|
// Composables
|
|
6
7
|
export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
|
|
7
8
|
// Utilities
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AnimationType } from './animations';
|
|
2
|
+
export interface RuneScrollerOptions {
|
|
3
|
+
animation?: AnimationType;
|
|
4
|
+
duration?: number;
|
|
5
|
+
repeat?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
|
|
9
|
+
* @param element - L'élément à animer
|
|
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
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { setCSSVariables, setupAnimationElement, createSentinel } from './dom-utils.svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
|
|
4
|
+
* @param element - L'élément à animer
|
|
5
|
+
* @param options - Options d'animation (animation type, duration, et repeat)
|
|
6
|
+
* @returns Objet action Svelte
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <!-- Animation une seule fois -->
|
|
11
|
+
* <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
|
|
12
|
+
* Content
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* <!-- Animation répétée à chaque scroll -->
|
|
16
|
+
* <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000, repeat: true }}>
|
|
17
|
+
* Content
|
|
18
|
+
* </div>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function runeScroller(element, options) {
|
|
22
|
+
// Setup animation classes et variables CSS
|
|
23
|
+
if (options?.animation || options?.duration) {
|
|
24
|
+
setupAnimationElement(element, options.animation);
|
|
25
|
+
setCSSVariables(element, options.duration);
|
|
26
|
+
}
|
|
27
|
+
// Créer le sentinel invisible juste en dessous
|
|
28
|
+
const sentinel = createSentinel(element);
|
|
29
|
+
// Observer le sentinel avec cleanup tracking
|
|
30
|
+
let observerConnected = true;
|
|
31
|
+
const observer = new IntersectionObserver((entries) => {
|
|
32
|
+
const isIntersecting = entries[0].isIntersecting;
|
|
33
|
+
if (isIntersecting) {
|
|
34
|
+
// Ajouter la classe is-visible à l'élément
|
|
35
|
+
element.classList.add('is-visible');
|
|
36
|
+
// Déconnecter si pas en mode repeat
|
|
37
|
+
if (!options?.repeat) {
|
|
38
|
+
observer.disconnect();
|
|
39
|
+
observerConnected = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (options?.repeat) {
|
|
43
|
+
// En mode repeat, retirer la classe quand le sentinel sort
|
|
44
|
+
element.classList.remove('is-visible');
|
|
45
|
+
}
|
|
46
|
+
}, { threshold: 0 });
|
|
47
|
+
observer.observe(sentinel);
|
|
48
|
+
return {
|
|
49
|
+
update(newOptions) {
|
|
50
|
+
if (newOptions?.animation) {
|
|
51
|
+
element.setAttribute('data-animation', newOptions.animation);
|
|
52
|
+
}
|
|
53
|
+
if (newOptions?.duration) {
|
|
54
|
+
setCSSVariables(element, newOptions.duration);
|
|
55
|
+
}
|
|
56
|
+
// Update repeat option
|
|
57
|
+
if (newOptions?.repeat !== undefined && newOptions.repeat !== options?.repeat) {
|
|
58
|
+
options = { ...options, repeat: newOptions.repeat };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
destroy() {
|
|
62
|
+
if (observerConnected) {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
}
|
|
65
|
+
sentinel.remove();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-scroller",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@eslint/compat": "^1.4.0",
|
|
58
58
|
"@eslint/js": "^9.36.0",
|
|
59
|
-
"@sveltejs/adapter-auto": "^3.2.2",
|
|
60
59
|
"@sveltejs/kit": "^2.43.2",
|
|
61
60
|
"@sveltejs/package": "^2.5.4",
|
|
62
61
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
package/dist/assets/favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|