fancy-ui-svelte 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/dist/fancy-ui/dock/index.d.ts +1 -1
- package/dist/fancy-ui/dock/index.js +0 -1
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +181 -0
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte.d.ts +26 -0
- package/dist/fancy-ui/editorial-engine/README.md +56 -0
- package/dist/fancy-ui/editorial-engine/engine.d.ts +7 -0
- package/dist/fancy-ui/editorial-engine/engine.js +581 -0
- package/dist/fancy-ui/editorial-engine/index.d.ts +2 -0
- package/dist/fancy-ui/editorial-engine/index.js +1 -0
- package/dist/fancy-ui/index.d.ts +4 -0
- package/dist/fancy-ui/index.js +4 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte +168 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte.d.ts +33 -0
- package/dist/fancy-ui/line-reveal/README.md +42 -0
- package/dist/fancy-ui/line-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/line-reveal/index.js +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte +323 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte.d.ts +19 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.d.ts +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.js +123 -0
- package/dist/fancy-ui/noise-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/noise-reveal/index.js +1 -0
- package/dist/fancy-ui/registry.js +128 -11
- package/dist/types.d.ts +2 -0
- package/package.json +4 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Props for LineReveal
|
|
4
|
+
*
|
|
5
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
6
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
7
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
8
|
+
*/
|
|
9
|
+
export interface LineRevealProps {
|
|
10
|
+
/** Text to reveal */
|
|
11
|
+
text: string;
|
|
12
|
+
/**
|
|
13
|
+
* CSS font shorthand used for both measurement and rendering
|
|
14
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
15
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
16
|
+
* canvas measurement and DOM rendering.
|
|
17
|
+
*/
|
|
18
|
+
font?: string;
|
|
19
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
20
|
+
lineHeight?: number;
|
|
21
|
+
/** Delay between lines in seconds */
|
|
22
|
+
stagger?: number;
|
|
23
|
+
/** Animation duration per line in seconds */
|
|
24
|
+
duration?: number;
|
|
25
|
+
/** Initial delay before the first line in seconds */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Animate only the first time the component enters the viewport */
|
|
28
|
+
once?: boolean;
|
|
29
|
+
/** Additional CSS classes */
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { cn } from "../../utils.js";
|
|
36
|
+
import { onMount } from "svelte";
|
|
37
|
+
import {
|
|
38
|
+
prepareWithSegments,
|
|
39
|
+
layoutWithLines,
|
|
40
|
+
type PreparedTextWithSegments,
|
|
41
|
+
} from "@chenglou/pretext";
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
text,
|
|
45
|
+
// Named families only: canvas and CSS can resolve `system-ui` to
|
|
46
|
+
// different fonts (notably on macOS), which skews measured widths.
|
|
47
|
+
font = '600 32px "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
48
|
+
lineHeight,
|
|
49
|
+
stagger = 0.08,
|
|
50
|
+
duration = 0.7,
|
|
51
|
+
delay = 0,
|
|
52
|
+
once = true,
|
|
53
|
+
class: className,
|
|
54
|
+
}: LineRevealProps = $props();
|
|
55
|
+
|
|
56
|
+
const fontSize = $derived(Number(/(\d+(?:\.\d+)?)px/.exec(font)?.[1] ?? 32));
|
|
57
|
+
const resolvedLineHeight = $derived(lineHeight ?? Math.round(fontSize * 1.2));
|
|
58
|
+
|
|
59
|
+
let containerRef: HTMLDivElement;
|
|
60
|
+
let containerWidth = $state(0);
|
|
61
|
+
let revealed = $state(false);
|
|
62
|
+
let prepared = $state<PreparedTextWithSegments | null>(null);
|
|
63
|
+
|
|
64
|
+
// Measurement requires a canvas context, so this only runs in the browser
|
|
65
|
+
// ($effect never runs during SSR). Web fonts must be loaded first or the
|
|
66
|
+
// canvas silently measures with the fallback font.
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const currentText = text;
|
|
69
|
+
const currentFont = font;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
|
|
72
|
+
document.fonts
|
|
73
|
+
.load(currentFont)
|
|
74
|
+
.catch(() => {})
|
|
75
|
+
.then(() => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
prepared = prepareWithSegments(currentText, currentFont);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Pure arithmetic on cached measurements — cheap to re-run on every resize
|
|
87
|
+
// (containerWidth is tracked by Svelte's bind:clientWidth ResizeObserver).
|
|
88
|
+
const layoutResult = $derived(
|
|
89
|
+
prepared && containerWidth > 0
|
|
90
|
+
? layoutWithLines(prepared, containerWidth, resolvedLineHeight)
|
|
91
|
+
: null
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
onMount(() => {
|
|
95
|
+
const observer = new IntersectionObserver(
|
|
96
|
+
(entries) => {
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.isIntersecting) {
|
|
99
|
+
revealed = true;
|
|
100
|
+
if (once) observer.disconnect();
|
|
101
|
+
} else if (!once) {
|
|
102
|
+
revealed = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{ threshold: 0.1 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
observer.observe(containerRef);
|
|
110
|
+
|
|
111
|
+
return () => observer.disconnect();
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div
|
|
116
|
+
bind:this={containerRef}
|
|
117
|
+
bind:clientWidth={containerWidth}
|
|
118
|
+
class={cn("relative w-full", className)}
|
|
119
|
+
style:font
|
|
120
|
+
style:line-height="{resolvedLineHeight}px"
|
|
121
|
+
style:height={layoutResult ? `${layoutResult.height}px` : undefined}
|
|
122
|
+
>
|
|
123
|
+
<span class="sr-only">{text}</span>
|
|
124
|
+
{#if layoutResult}
|
|
125
|
+
{#each layoutResult.lines as line, i (i)}
|
|
126
|
+
<div class="line-mask" style:height="{resolvedLineHeight}px" aria-hidden="true">
|
|
127
|
+
<div
|
|
128
|
+
class="line"
|
|
129
|
+
class:revealed
|
|
130
|
+
style:transition-duration="{duration}s"
|
|
131
|
+
style:transition-delay="{delay + i * stagger}s"
|
|
132
|
+
>
|
|
133
|
+
{line.text}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{/each}
|
|
137
|
+
{:else}
|
|
138
|
+
<!-- SSR / pre-measure fallback: keeps text in the HTML and reserves space -->
|
|
139
|
+
<div style:visibility="hidden" aria-hidden="true">{text}</div>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<style>
|
|
144
|
+
.line-mask {
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
/* preserve exact spacing so rendered width matches pretext measurement */
|
|
147
|
+
white-space: pre;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.line {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
transform: translateY(105%);
|
|
153
|
+
transition-property: transform, opacity;
|
|
154
|
+
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.line.revealed {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
transform: translateY(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
.line {
|
|
164
|
+
transition-duration: 0.1s !important;
|
|
165
|
+
transition-delay: 0s !important;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for LineReveal
|
|
3
|
+
*
|
|
4
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
5
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
6
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
7
|
+
*/
|
|
8
|
+
export interface LineRevealProps {
|
|
9
|
+
/** Text to reveal */
|
|
10
|
+
text: string;
|
|
11
|
+
/**
|
|
12
|
+
* CSS font shorthand used for both measurement and rendering
|
|
13
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
14
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
15
|
+
* canvas measurement and DOM rendering.
|
|
16
|
+
*/
|
|
17
|
+
font?: string;
|
|
18
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
19
|
+
lineHeight?: number;
|
|
20
|
+
/** Delay between lines in seconds */
|
|
21
|
+
stagger?: number;
|
|
22
|
+
/** Animation duration per line in seconds */
|
|
23
|
+
duration?: number;
|
|
24
|
+
/** Initial delay before the first line in seconds */
|
|
25
|
+
delay?: number;
|
|
26
|
+
/** Animate only the first time the component enters the viewport */
|
|
27
|
+
once?: boolean;
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
declare const LineReveal: import("svelte").Component<LineRevealProps, {}, "">;
|
|
32
|
+
type LineReveal = ReturnType<typeof LineReveal>;
|
|
33
|
+
export default LineReveal;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LineReveal
|
|
2
|
+
|
|
3
|
+
Reveals text line by line with a staggered slide-up animation.
|
|
4
|
+
|
|
5
|
+
Unlike DOM-splitting approaches, line breaks are computed with
|
|
6
|
+
[@chenglou/pretext](https://github.com/chenglou/pretext) — canvas-accurate text
|
|
7
|
+
measurement in pure JS. Each computed line is rendered as its own masked
|
|
8
|
+
element, so the per-line stagger works without measuring or mutating the DOM,
|
|
9
|
+
and lines re-wrap instantly on container resize.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { LineReveal } from "$lib/fancy-ui/line-reveal";
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<LineReveal
|
|
19
|
+
text="Text measurement and layout without touching the DOM."
|
|
20
|
+
font="700 40px Helvetica, Arial, sans-serif"
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
| ------------ | --------- | -------------------------------- | ------------------------------------------------------------ |
|
|
28
|
+
| `text` | `string` | — (required) | Text to reveal |
|
|
29
|
+
| `font` | `string` | `"600 32px \"Helvetica Neue\", Helvetica, Arial, sans-serif"` | CSS font shorthand, used for both measurement and rendering |
|
|
30
|
+
| `lineHeight` | `number` | `1.2 × font size` | Line height in pixels |
|
|
31
|
+
| `stagger` | `number` | `0.08` | Delay between lines in seconds |
|
|
32
|
+
| `duration` | `number` | `0.7` | Animation duration per line in seconds |
|
|
33
|
+
| `delay` | `number` | `0` | Initial delay in seconds |
|
|
34
|
+
| `once` | `boolean` | `true` | Animate only on first viewport entry |
|
|
35
|
+
| `class` | `string` | — | Additional CSS classes |
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- `font` must be a valid canvas `ctx.font` shorthand (weight, size in px, family).
|
|
40
|
+
- Measurement happens client-side only (pretext needs a canvas); during SSR the
|
|
41
|
+
raw text is rendered hidden so content stays in the HTML.
|
|
42
|
+
- Resizing the container re-wraps lines without replaying the animation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LineReveal } from "./LineReveal.svelte";
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface NoiseRevealProps {
|
|
3
|
+
/** Image URL */
|
|
4
|
+
src: string;
|
|
5
|
+
/** Accessible label for the image */
|
|
6
|
+
alt?: string;
|
|
7
|
+
/** Reveal trigger: "view" = on viewport entry, "manual" = via `revealed` */
|
|
8
|
+
trigger?: "view" | "manual";
|
|
9
|
+
/** Manual reveal state (used with trigger="manual"; also allows re-hiding) */
|
|
10
|
+
revealed?: boolean;
|
|
11
|
+
/** Reveal animation duration in seconds */
|
|
12
|
+
duration?: number;
|
|
13
|
+
/** Delay before reveal in seconds (trigger="view") */
|
|
14
|
+
delay?: number;
|
|
15
|
+
/** Additional CSS classes */
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
import { untrack } from "svelte";
|
|
23
|
+
import { cn } from "../../utils.js";
|
|
24
|
+
import { easeInOutCubic, getProgress, lerp } from "../../utils/animation.js";
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
src,
|
|
28
|
+
alt,
|
|
29
|
+
trigger = "view",
|
|
30
|
+
revealed = false,
|
|
31
|
+
duration = 1.5,
|
|
32
|
+
delay = 0,
|
|
33
|
+
class: className = "",
|
|
34
|
+
}: NoiseRevealProps = $props();
|
|
35
|
+
|
|
36
|
+
let container: HTMLDivElement;
|
|
37
|
+
let setTarget: ((value: number) => void) | null = $state.raw(null);
|
|
38
|
+
|
|
39
|
+
const vertexShader = `
|
|
40
|
+
uniform float uProgress;
|
|
41
|
+
varying vec2 vUv;
|
|
42
|
+
|
|
43
|
+
void main() {
|
|
44
|
+
vec3 newPosition = position;
|
|
45
|
+
|
|
46
|
+
float distanceToCenter = distance(vec2(0.5), uv);
|
|
47
|
+
float wave = (1.0 - uProgress) * sin(distanceToCenter * 20.0 - uProgress * 5.0);
|
|
48
|
+
newPosition.z += wave;
|
|
49
|
+
|
|
50
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
51
|
+
vUv = uv;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// cnoise: classic 3D Perlin noise by Stefan Gustavson (MIT, github.com/stegu/webgl-noise)
|
|
56
|
+
const fragmentShader = `
|
|
57
|
+
uniform sampler2D uTexture;
|
|
58
|
+
uniform float uTime;
|
|
59
|
+
uniform float uProgress;
|
|
60
|
+
uniform vec2 uRes;
|
|
61
|
+
uniform vec2 uImageRes;
|
|
62
|
+
varying vec2 vUv;
|
|
63
|
+
|
|
64
|
+
vec4 permute(vec4 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
|
|
65
|
+
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
|
|
66
|
+
vec3 fade(vec3 t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
|
|
67
|
+
|
|
68
|
+
float cnoise(vec3 P) {
|
|
69
|
+
vec3 Pi0 = floor(P);
|
|
70
|
+
vec3 Pi1 = Pi0 + vec3(1.0);
|
|
71
|
+
Pi0 = mod(Pi0, 289.0);
|
|
72
|
+
Pi1 = mod(Pi1, 289.0);
|
|
73
|
+
vec3 Pf0 = fract(P);
|
|
74
|
+
vec3 Pf1 = Pf0 - vec3(1.0);
|
|
75
|
+
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
76
|
+
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
77
|
+
vec4 iz0 = Pi0.zzzz;
|
|
78
|
+
vec4 iz1 = Pi1.zzzz;
|
|
79
|
+
|
|
80
|
+
vec4 ixy = permute(permute(ix) + iy);
|
|
81
|
+
vec4 ixy0 = permute(ixy + iz0);
|
|
82
|
+
vec4 ixy1 = permute(ixy + iz1);
|
|
83
|
+
|
|
84
|
+
vec4 gx0 = ixy0 / 7.0;
|
|
85
|
+
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
|
|
86
|
+
gx0 = fract(gx0);
|
|
87
|
+
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
|
88
|
+
vec4 sz0 = step(gz0, vec4(0.0));
|
|
89
|
+
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
|
90
|
+
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
|
91
|
+
|
|
92
|
+
vec4 gx1 = ixy1 / 7.0;
|
|
93
|
+
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
|
|
94
|
+
gx1 = fract(gx1);
|
|
95
|
+
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
|
96
|
+
vec4 sz1 = step(gz1, vec4(0.0));
|
|
97
|
+
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
|
98
|
+
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
|
99
|
+
|
|
100
|
+
vec3 g000 = vec3(gx0.x, gy0.x, gz0.x);
|
|
101
|
+
vec3 g100 = vec3(gx0.y, gy0.y, gz0.y);
|
|
102
|
+
vec3 g010 = vec3(gx0.z, gy0.z, gz0.z);
|
|
103
|
+
vec3 g110 = vec3(gx0.w, gy0.w, gz0.w);
|
|
104
|
+
vec3 g001 = vec3(gx1.x, gy1.x, gz1.x);
|
|
105
|
+
vec3 g101 = vec3(gx1.y, gy1.y, gz1.y);
|
|
106
|
+
vec3 g011 = vec3(gx1.z, gy1.z, gz1.z);
|
|
107
|
+
vec3 g111 = vec3(gx1.w, gy1.w, gz1.w);
|
|
108
|
+
|
|
109
|
+
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
|
|
110
|
+
g000 *= norm0.x;
|
|
111
|
+
g010 *= norm0.y;
|
|
112
|
+
g100 *= norm0.z;
|
|
113
|
+
g110 *= norm0.w;
|
|
114
|
+
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
|
|
115
|
+
g001 *= norm1.x;
|
|
116
|
+
g011 *= norm1.y;
|
|
117
|
+
g101 *= norm1.z;
|
|
118
|
+
g111 *= norm1.w;
|
|
119
|
+
|
|
120
|
+
float n000 = dot(g000, Pf0);
|
|
121
|
+
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
|
|
122
|
+
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
|
|
123
|
+
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
|
|
124
|
+
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
|
|
125
|
+
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
|
|
126
|
+
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
|
|
127
|
+
float n111 = dot(g111, Pf1);
|
|
128
|
+
|
|
129
|
+
vec3 fade_xyz = fade(Pf0);
|
|
130
|
+
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
|
|
131
|
+
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
|
|
132
|
+
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
|
133
|
+
return 2.2 * n_xyz;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
vec2 CoverUV(vec2 u, vec2 s, vec2 i) {
|
|
137
|
+
float rs = s.x / s.y;
|
|
138
|
+
float ri = i.x / i.y;
|
|
139
|
+
vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x);
|
|
140
|
+
vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st;
|
|
141
|
+
return u * s / st + o;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
void main() {
|
|
145
|
+
vec2 newUv = CoverUV(vUv, uRes, uImageRes);
|
|
146
|
+
|
|
147
|
+
vec2 displacedUv = vUv + cnoise(vec3(vUv * 5.0, uTime * 0.1));
|
|
148
|
+
float strength = cnoise(vec3(displacedUv * 5.0, uTime * 0.2));
|
|
149
|
+
|
|
150
|
+
float radialGradient = distance(vUv, vec2(0.5)) * 12.5 - 7.0 * uProgress;
|
|
151
|
+
strength += radialGradient;
|
|
152
|
+
|
|
153
|
+
strength = clamp(strength, 0.0, 1.0);
|
|
154
|
+
strength = 1.0 - strength;
|
|
155
|
+
|
|
156
|
+
vec3 textureColor = texture2D(uTexture, newUv).rgb;
|
|
157
|
+
|
|
158
|
+
float opacityProgress = smoothstep(0.0, 0.7, uProgress);
|
|
159
|
+
|
|
160
|
+
gl_FragColor = vec4(textureColor, strength * opacityProgress);
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
$effect(() => {
|
|
165
|
+
if (!container) return;
|
|
166
|
+
|
|
167
|
+
const _src = src;
|
|
168
|
+
const _duration = duration;
|
|
169
|
+
|
|
170
|
+
const rect = container.getBoundingClientRect();
|
|
171
|
+
const width = rect.width || 1;
|
|
172
|
+
const height = rect.height || 1;
|
|
173
|
+
if (height === 0) return;
|
|
174
|
+
|
|
175
|
+
const scene = new THREE.Scene();
|
|
176
|
+
scene.background = null;
|
|
177
|
+
|
|
178
|
+
const cameraDistance = 5;
|
|
179
|
+
const fov = 50;
|
|
180
|
+
const camera = new THREE.PerspectiveCamera(fov, width / height, 0.01, 1000);
|
|
181
|
+
camera.position.set(0, 0, cameraDistance);
|
|
182
|
+
|
|
183
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
184
|
+
renderer.setClearColor(0x000000, 0);
|
|
185
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
186
|
+
renderer.setSize(width, height, false);
|
|
187
|
+
renderer.domElement.style.width = "100%";
|
|
188
|
+
renderer.domElement.style.height = "100%";
|
|
189
|
+
container.appendChild(renderer.domElement);
|
|
190
|
+
|
|
191
|
+
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32);
|
|
192
|
+
|
|
193
|
+
const material = new THREE.ShaderMaterial({
|
|
194
|
+
uniforms: {
|
|
195
|
+
uTexture: { value: null },
|
|
196
|
+
uTime: { value: 0 },
|
|
197
|
+
uProgress: { value: trigger === "manual" && untrack(() => revealed) ? 1 : 0 },
|
|
198
|
+
uRes: { value: new THREE.Vector2(1, 1) },
|
|
199
|
+
uImageRes: { value: new THREE.Vector2(1, 1) },
|
|
200
|
+
},
|
|
201
|
+
vertexShader,
|
|
202
|
+
fragmentShader,
|
|
203
|
+
transparent: true,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const plane = new THREE.Mesh(geometry, material);
|
|
207
|
+
scene.add(plane);
|
|
208
|
+
|
|
209
|
+
// Scale the unit plane to exactly fill the camera frustum at z=0
|
|
210
|
+
const fitPlane = (w: number, h: number) => {
|
|
211
|
+
const visibleH = 2 * Math.tan((fov * Math.PI) / 360) * cameraDistance;
|
|
212
|
+
const visibleW = visibleH * (w / h);
|
|
213
|
+
plane.scale.set(visibleW, visibleH, 1);
|
|
214
|
+
(material.uniforms.uRes.value as THREE.Vector2).set(visibleW, visibleH);
|
|
215
|
+
};
|
|
216
|
+
fitPlane(width, height);
|
|
217
|
+
|
|
218
|
+
let texture: THREE.Texture | null = null;
|
|
219
|
+
new THREE.TextureLoader().load(_src, (loaded) => {
|
|
220
|
+
loaded.colorSpace = THREE.SRGBColorSpace;
|
|
221
|
+
texture = loaded;
|
|
222
|
+
material.uniforms.uTexture.value = loaded;
|
|
223
|
+
const image = loaded.image as HTMLImageElement;
|
|
224
|
+
(material.uniforms.uImageRes.value as THREE.Vector2).set(
|
|
225
|
+
image.naturalWidth || 1,
|
|
226
|
+
image.naturalHeight || 1
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
231
|
+
let progress = material.uniforms.uProgress.value as number;
|
|
232
|
+
const tween = { from: progress, to: progress, start: 0, active: false };
|
|
233
|
+
|
|
234
|
+
setTarget = (value: number) => {
|
|
235
|
+
if (value === tween.to) return;
|
|
236
|
+
tween.to = value;
|
|
237
|
+
if (reducedMotion) {
|
|
238
|
+
progress = value;
|
|
239
|
+
tween.active = false;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
tween.from = progress;
|
|
243
|
+
tween.start = performance.now();
|
|
244
|
+
tween.active = true;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const handleResize = () => {
|
|
248
|
+
const r = container.getBoundingClientRect();
|
|
249
|
+
if (r.height === 0) return;
|
|
250
|
+
camera.aspect = r.width / r.height;
|
|
251
|
+
camera.updateProjectionMatrix();
|
|
252
|
+
renderer.setSize(r.width, r.height, false);
|
|
253
|
+
fitPlane(r.width, r.height);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
window.addEventListener("resize", handleResize);
|
|
257
|
+
|
|
258
|
+
const clock = new THREE.Clock();
|
|
259
|
+
let animationId = 0;
|
|
260
|
+
const animate = () => {
|
|
261
|
+
animationId = requestAnimationFrame(animate);
|
|
262
|
+
if (tween.active) {
|
|
263
|
+
const t = getProgress(performance.now() - tween.start, _duration * 1000);
|
|
264
|
+
progress = lerp(tween.from, tween.to, easeInOutCubic(t));
|
|
265
|
+
if (t >= 1) tween.active = false;
|
|
266
|
+
}
|
|
267
|
+
material.uniforms.uProgress.value = progress;
|
|
268
|
+
material.uniforms.uTime.value = clock.getElapsedTime();
|
|
269
|
+
renderer.render(scene, camera);
|
|
270
|
+
};
|
|
271
|
+
animate();
|
|
272
|
+
|
|
273
|
+
return () => {
|
|
274
|
+
setTarget = null;
|
|
275
|
+
window.removeEventListener("resize", handleResize);
|
|
276
|
+
cancelAnimationFrame(animationId);
|
|
277
|
+
if (renderer.domElement.parentNode === container) container.removeChild(renderer.domElement);
|
|
278
|
+
renderer.dispose();
|
|
279
|
+
geometry.dispose();
|
|
280
|
+
material.dispose();
|
|
281
|
+
texture?.dispose();
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Viewport trigger: reveal once on first intersection
|
|
286
|
+
$effect(() => {
|
|
287
|
+
if (trigger !== "view" || !container || !setTarget) return;
|
|
288
|
+
|
|
289
|
+
const set = setTarget;
|
|
290
|
+
let timeoutId = 0;
|
|
291
|
+
const observer = new IntersectionObserver(
|
|
292
|
+
(entries) => {
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
if (entry.isIntersecting) {
|
|
295
|
+
observer.disconnect();
|
|
296
|
+
timeoutId = window.setTimeout(() => set(1), delay * 1000);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{ threshold: 0.1 }
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
observer.observe(container);
|
|
304
|
+
|
|
305
|
+
return () => {
|
|
306
|
+
observer.disconnect();
|
|
307
|
+
clearTimeout(timeoutId);
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Manual trigger: follow the `revealed` prop both ways
|
|
312
|
+
$effect(() => {
|
|
313
|
+
if (trigger !== "manual" || !setTarget) return;
|
|
314
|
+
setTarget(revealed ? 1 : 0);
|
|
315
|
+
});
|
|
316
|
+
</script>
|
|
317
|
+
|
|
318
|
+
<div
|
|
319
|
+
bind:this={container}
|
|
320
|
+
class={cn("relative h-[400px] w-full", className)}
|
|
321
|
+
role={alt ? "img" : undefined}
|
|
322
|
+
aria-label={alt}
|
|
323
|
+
></div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface NoiseRevealProps {
|
|
2
|
+
/** Image URL */
|
|
3
|
+
src: string;
|
|
4
|
+
/** Accessible label for the image */
|
|
5
|
+
alt?: string;
|
|
6
|
+
/** Reveal trigger: "view" = on viewport entry, "manual" = via `revealed` */
|
|
7
|
+
trigger?: "view" | "manual";
|
|
8
|
+
/** Manual reveal state (used with trigger="manual"; also allows re-hiding) */
|
|
9
|
+
revealed?: boolean;
|
|
10
|
+
/** Reveal animation duration in seconds */
|
|
11
|
+
duration?: number;
|
|
12
|
+
/** Delay before reveal in seconds (trigger="view") */
|
|
13
|
+
delay?: number;
|
|
14
|
+
/** Additional CSS classes */
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const NoiseReveal: import("svelte").Component<NoiseRevealProps, {}, "">;
|
|
18
|
+
type NoiseReveal = ReturnType<typeof NoiseReveal>;
|
|
19
|
+
export default NoiseReveal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|