@autumnsgrove/gossamer 0.0.1 → 0.1.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/dist/animation.d.ts +80 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/characters.d.ts +49 -0
- package/dist/characters.d.ts.map +1 -0
- package/dist/index.d.ts +37 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1284 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns.d.ts +100 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/renderer.d.ts +113 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/style.css +124 -0
- package/dist/svelte/GossamerBorder.svelte.d.ts +1 -0
- package/dist/svelte/GossamerClouds.svelte.d.ts +1 -0
- package/dist/svelte/GossamerImage.svelte.d.ts +1 -0
- package/dist/svelte/GossamerOverlay.svelte.d.ts +1 -0
- package/dist/svelte/GossamerText.svelte.d.ts +1 -0
- package/dist/svelte/index.d.ts +20 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3651 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/svelte/presets.d.ts +38 -0
- package/dist/svelte/presets.d.ts.map +1 -0
- package/dist/utils/canvas.d.ts +73 -0
- package/dist/utils/canvas.d.ts.map +1 -0
- package/dist/utils/image.d.ts +74 -0
- package/dist/utils/image.d.ts.map +1 -0
- package/dist/utils/performance.d.ts +86 -0
- package/dist/utils/performance.d.ts.map +1 -0
- package/package.json +23 -5
- package/src/animation.test.ts +254 -0
- package/src/animation.ts +243 -0
- package/src/characters.test.ts +148 -0
- package/src/characters.ts +164 -0
- package/src/index.test.ts +115 -0
- package/src/index.ts +133 -11
- package/src/patterns.test.ts +273 -0
- package/src/patterns.ts +316 -0
- package/src/renderer.ts +309 -0
- package/src/svelte/GossamerBorder.svelte +326 -0
- package/src/svelte/GossamerClouds.svelte +269 -0
- package/src/svelte/GossamerImage.svelte +266 -0
- package/src/svelte/GossamerOverlay.svelte +232 -0
- package/src/svelte/GossamerText.svelte +239 -0
- package/src/svelte/index.ts +75 -0
- package/src/svelte/presets.ts +174 -0
- package/src/utils/canvas.ts +210 -0
- package/src/utils/image.ts +275 -0
- package/src/utils/performance.ts +282 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type BorderStyle = 'dots' | 'dashes' | 'stars' | 'corners' | 'simple' | 'double';
|
|
3
|
+
|
|
4
|
+
export interface GossamerBorderProps {
|
|
5
|
+
/** Border style preset */
|
|
6
|
+
style?: BorderStyle;
|
|
7
|
+
/** Custom characters for border (overrides style) */
|
|
8
|
+
characters?: {
|
|
9
|
+
horizontal?: string;
|
|
10
|
+
vertical?: string;
|
|
11
|
+
topLeft?: string;
|
|
12
|
+
topRight?: string;
|
|
13
|
+
bottomLeft?: string;
|
|
14
|
+
bottomRight?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Border color */
|
|
17
|
+
color?: string;
|
|
18
|
+
/** Border thickness in characters */
|
|
19
|
+
thickness?: number;
|
|
20
|
+
/** Character size in pixels */
|
|
21
|
+
charSize?: number;
|
|
22
|
+
/** Enable animation */
|
|
23
|
+
animated?: boolean;
|
|
24
|
+
/** Animation speed */
|
|
25
|
+
speed?: number;
|
|
26
|
+
/** Padding inside the border */
|
|
27
|
+
padding?: number;
|
|
28
|
+
/** Additional CSS class */
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script lang="ts">
|
|
34
|
+
import { onMount } from 'svelte';
|
|
35
|
+
import {
|
|
36
|
+
createResizeObserver,
|
|
37
|
+
createVisibilityObserver,
|
|
38
|
+
onReducedMotionChange,
|
|
39
|
+
} from '../index';
|
|
40
|
+
|
|
41
|
+
// Props with defaults
|
|
42
|
+
let {
|
|
43
|
+
style = 'simple',
|
|
44
|
+
characters,
|
|
45
|
+
color = 'currentColor',
|
|
46
|
+
thickness = 1,
|
|
47
|
+
charSize = 12,
|
|
48
|
+
animated = false,
|
|
49
|
+
speed = 0.5,
|
|
50
|
+
padding = 0,
|
|
51
|
+
class: className = '',
|
|
52
|
+
}: GossamerBorderProps = $props();
|
|
53
|
+
|
|
54
|
+
// Border character presets
|
|
55
|
+
const BORDER_STYLES: Record<BorderStyle, {
|
|
56
|
+
horizontal: string;
|
|
57
|
+
vertical: string;
|
|
58
|
+
topLeft: string;
|
|
59
|
+
topRight: string;
|
|
60
|
+
bottomLeft: string;
|
|
61
|
+
bottomRight: string;
|
|
62
|
+
}> = {
|
|
63
|
+
simple: {
|
|
64
|
+
horizontal: '─',
|
|
65
|
+
vertical: '│',
|
|
66
|
+
topLeft: '┌',
|
|
67
|
+
topRight: '┐',
|
|
68
|
+
bottomLeft: '└',
|
|
69
|
+
bottomRight: '┘',
|
|
70
|
+
},
|
|
71
|
+
double: {
|
|
72
|
+
horizontal: '═',
|
|
73
|
+
vertical: '║',
|
|
74
|
+
topLeft: '╔',
|
|
75
|
+
topRight: '╗',
|
|
76
|
+
bottomLeft: '╚',
|
|
77
|
+
bottomRight: '╝',
|
|
78
|
+
},
|
|
79
|
+
dots: {
|
|
80
|
+
horizontal: '·',
|
|
81
|
+
vertical: '·',
|
|
82
|
+
topLeft: '·',
|
|
83
|
+
topRight: '·',
|
|
84
|
+
bottomLeft: '·',
|
|
85
|
+
bottomRight: '·',
|
|
86
|
+
},
|
|
87
|
+
dashes: {
|
|
88
|
+
horizontal: '─',
|
|
89
|
+
vertical: '¦',
|
|
90
|
+
topLeft: '┌',
|
|
91
|
+
topRight: '┐',
|
|
92
|
+
bottomLeft: '└',
|
|
93
|
+
bottomRight: '┘',
|
|
94
|
+
},
|
|
95
|
+
stars: {
|
|
96
|
+
horizontal: '*',
|
|
97
|
+
vertical: '*',
|
|
98
|
+
topLeft: '*',
|
|
99
|
+
topRight: '*',
|
|
100
|
+
bottomLeft: '*',
|
|
101
|
+
bottomRight: '*',
|
|
102
|
+
},
|
|
103
|
+
corners: {
|
|
104
|
+
horizontal: ' ',
|
|
105
|
+
vertical: ' ',
|
|
106
|
+
topLeft: '╭',
|
|
107
|
+
topRight: '╮',
|
|
108
|
+
bottomLeft: '╰',
|
|
109
|
+
bottomRight: '╯',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// State
|
|
114
|
+
let canvas: HTMLCanvasElement;
|
|
115
|
+
let container: HTMLDivElement;
|
|
116
|
+
let isVisible = true;
|
|
117
|
+
let reducedMotion = false;
|
|
118
|
+
let animationId: number | null = null;
|
|
119
|
+
let borderWidth = 0;
|
|
120
|
+
let borderHeight = 0;
|
|
121
|
+
|
|
122
|
+
// Get effective border characters
|
|
123
|
+
const borderChars = $derived({
|
|
124
|
+
...BORDER_STYLES[style],
|
|
125
|
+
...characters,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const shouldAnimate = $derived(animated && isVisible && !reducedMotion);
|
|
129
|
+
|
|
130
|
+
function renderBorder(time: number = 0): void {
|
|
131
|
+
if (!canvas) return;
|
|
132
|
+
|
|
133
|
+
const ctx = canvas.getContext('2d');
|
|
134
|
+
if (!ctx) return;
|
|
135
|
+
|
|
136
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
137
|
+
ctx.font = `${charSize}px monospace`;
|
|
138
|
+
ctx.textBaseline = 'top';
|
|
139
|
+
ctx.fillStyle = color;
|
|
140
|
+
|
|
141
|
+
const cols = Math.floor(borderWidth / charSize);
|
|
142
|
+
const rows = Math.floor(borderHeight / charSize);
|
|
143
|
+
|
|
144
|
+
if (cols < 3 || rows < 3) return;
|
|
145
|
+
|
|
146
|
+
// Animation offset for marching effect
|
|
147
|
+
const offset = animated ? Math.floor(time * speed * 0.01) : 0;
|
|
148
|
+
|
|
149
|
+
// Draw corners
|
|
150
|
+
ctx.fillText(borderChars.topLeft, 0, 0);
|
|
151
|
+
ctx.fillText(borderChars.topRight, (cols - 1) * charSize, 0);
|
|
152
|
+
ctx.fillText(borderChars.bottomLeft, 0, (rows - 1) * charSize);
|
|
153
|
+
ctx.fillText(borderChars.bottomRight, (cols - 1) * charSize, (rows - 1) * charSize);
|
|
154
|
+
|
|
155
|
+
// Draw horizontal borders (top and bottom)
|
|
156
|
+
for (let col = 1; col < cols - 1; col++) {
|
|
157
|
+
const animIndex = (col + offset) % 2;
|
|
158
|
+
const topChar = animated && animIndex === 0 ? ' ' : borderChars.horizontal;
|
|
159
|
+
const bottomChar = animated && animIndex === 1 ? ' ' : borderChars.horizontal;
|
|
160
|
+
|
|
161
|
+
if (topChar !== ' ') {
|
|
162
|
+
ctx.fillText(topChar, col * charSize, 0);
|
|
163
|
+
}
|
|
164
|
+
if (bottomChar !== ' ') {
|
|
165
|
+
ctx.fillText(bottomChar, col * charSize, (rows - 1) * charSize);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Draw vertical borders (left and right)
|
|
170
|
+
for (let row = 1; row < rows - 1; row++) {
|
|
171
|
+
const animIndex = (row + offset) % 2;
|
|
172
|
+
const leftChar = animated && animIndex === 0 ? ' ' : borderChars.vertical;
|
|
173
|
+
const rightChar = animated && animIndex === 1 ? ' ' : borderChars.vertical;
|
|
174
|
+
|
|
175
|
+
if (leftChar !== ' ') {
|
|
176
|
+
ctx.fillText(leftChar, 0, row * charSize);
|
|
177
|
+
}
|
|
178
|
+
if (rightChar !== ' ') {
|
|
179
|
+
ctx.fillText(rightChar, (cols - 1) * charSize, row * charSize);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Draw additional thickness layers if needed
|
|
184
|
+
for (let t = 1; t < thickness; t++) {
|
|
185
|
+
const innerCol = t;
|
|
186
|
+
const outerCol = cols - 1 - t;
|
|
187
|
+
const innerRow = t;
|
|
188
|
+
const outerRow = rows - 1 - t;
|
|
189
|
+
|
|
190
|
+
// Inner horizontal lines
|
|
191
|
+
for (let col = innerCol; col <= outerCol; col++) {
|
|
192
|
+
ctx.fillText(borderChars.horizontal, col * charSize, innerRow * charSize);
|
|
193
|
+
ctx.fillText(borderChars.horizontal, col * charSize, outerRow * charSize);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Inner vertical lines
|
|
197
|
+
for (let row = innerRow; row <= outerRow; row++) {
|
|
198
|
+
ctx.fillText(borderChars.vertical, innerCol * charSize, row * charSize);
|
|
199
|
+
ctx.fillText(borderChars.vertical, outerCol * charSize, row * charSize);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let startTime = 0;
|
|
205
|
+
|
|
206
|
+
function animate(currentTime: number): void {
|
|
207
|
+
if (!shouldAnimate) return;
|
|
208
|
+
|
|
209
|
+
const elapsed = currentTime - startTime;
|
|
210
|
+
renderBorder(elapsed);
|
|
211
|
+
animationId = requestAnimationFrame(animate);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function startAnimation(): void {
|
|
215
|
+
if (animationId !== null) return;
|
|
216
|
+
startTime = performance.now();
|
|
217
|
+
animationId = requestAnimationFrame(animate);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stopAnimation(): void {
|
|
221
|
+
if (animationId !== null) {
|
|
222
|
+
cancelAnimationFrame(animationId);
|
|
223
|
+
animationId = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setupCanvas(width: number, height: number): void {
|
|
228
|
+
if (!canvas) return;
|
|
229
|
+
|
|
230
|
+
borderWidth = width;
|
|
231
|
+
borderHeight = height;
|
|
232
|
+
canvas.width = width;
|
|
233
|
+
canvas.height = height;
|
|
234
|
+
|
|
235
|
+
if (shouldAnimate) {
|
|
236
|
+
startAnimation();
|
|
237
|
+
} else {
|
|
238
|
+
renderBorder();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Lifecycle
|
|
243
|
+
onMount(() => {
|
|
244
|
+
const cleanupMotion = onReducedMotionChange((prefers) => {
|
|
245
|
+
reducedMotion = prefers;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const cleanupVisibility = createVisibilityObserver(
|
|
249
|
+
container,
|
|
250
|
+
(visible) => {
|
|
251
|
+
isVisible = visible;
|
|
252
|
+
if (visible && shouldAnimate) {
|
|
253
|
+
startAnimation();
|
|
254
|
+
} else {
|
|
255
|
+
stopAnimation();
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
0.1
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const cleanupResize = createResizeObserver(
|
|
262
|
+
container,
|
|
263
|
+
(width, height) => {
|
|
264
|
+
setupCanvas(width, height);
|
|
265
|
+
},
|
|
266
|
+
100
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const rect = container.getBoundingClientRect();
|
|
270
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
271
|
+
setupCanvas(rect.width, rect.height);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return () => {
|
|
275
|
+
cleanupMotion();
|
|
276
|
+
cleanupVisibility();
|
|
277
|
+
cleanupResize();
|
|
278
|
+
stopAnimation();
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
$effect(() => {
|
|
283
|
+
if (shouldAnimate) {
|
|
284
|
+
startAnimation();
|
|
285
|
+
} else {
|
|
286
|
+
stopAnimation();
|
|
287
|
+
renderBorder();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<div
|
|
293
|
+
bind:this={container}
|
|
294
|
+
class="gossamer-border {className}"
|
|
295
|
+
style:padding="{padding}px"
|
|
296
|
+
>
|
|
297
|
+
<canvas
|
|
298
|
+
bind:this={canvas}
|
|
299
|
+
aria-hidden="true"
|
|
300
|
+
class="gossamer-border-canvas"
|
|
301
|
+
></canvas>
|
|
302
|
+
<div class="gossamer-border-content">
|
|
303
|
+
<slot />
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<style>
|
|
308
|
+
.gossamer-border {
|
|
309
|
+
position: relative;
|
|
310
|
+
display: block;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.gossamer-border-canvas {
|
|
314
|
+
position: absolute;
|
|
315
|
+
inset: 0;
|
|
316
|
+
width: 100%;
|
|
317
|
+
height: 100%;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
z-index: 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.gossamer-border-content {
|
|
323
|
+
position: relative;
|
|
324
|
+
z-index: 1;
|
|
325
|
+
}
|
|
326
|
+
</style>
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { PatternType, PatternConfig } from '../index';
|
|
3
|
+
|
|
4
|
+
export interface GossamerCloudsProps {
|
|
5
|
+
/** Pattern type for generation */
|
|
6
|
+
pattern?: PatternType;
|
|
7
|
+
/** Character set (light to dark) */
|
|
8
|
+
characters?: string;
|
|
9
|
+
/** Foreground color */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Overall opacity (0-1) */
|
|
12
|
+
opacity?: number;
|
|
13
|
+
/** Enable animation */
|
|
14
|
+
animated?: boolean;
|
|
15
|
+
/** Animation speed multiplier */
|
|
16
|
+
speed?: number;
|
|
17
|
+
/** Pattern frequency (scale) */
|
|
18
|
+
frequency?: number;
|
|
19
|
+
/** Pattern amplitude (intensity) */
|
|
20
|
+
amplitude?: number;
|
|
21
|
+
/** Cell size in pixels */
|
|
22
|
+
cellSize?: number;
|
|
23
|
+
/** Target FPS for animation */
|
|
24
|
+
fps?: number;
|
|
25
|
+
/** Use a preset configuration */
|
|
26
|
+
preset?: string;
|
|
27
|
+
/** Additional CSS class */
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
import { onMount } from 'svelte';
|
|
34
|
+
import {
|
|
35
|
+
GossamerRenderer,
|
|
36
|
+
generateBrightnessGrid,
|
|
37
|
+
createVisibilityObserver,
|
|
38
|
+
createResizeObserver,
|
|
39
|
+
prefersReducedMotion,
|
|
40
|
+
onReducedMotionChange,
|
|
41
|
+
CHARACTER_SETS,
|
|
42
|
+
} from '../index';
|
|
43
|
+
import { PRESETS } from './presets';
|
|
44
|
+
|
|
45
|
+
// Props with defaults
|
|
46
|
+
let {
|
|
47
|
+
pattern = 'perlin',
|
|
48
|
+
characters = CHARACTER_SETS.grove.characters,
|
|
49
|
+
color = 'currentColor',
|
|
50
|
+
opacity = 0.3,
|
|
51
|
+
animated = true,
|
|
52
|
+
speed = 0.5,
|
|
53
|
+
frequency = 0.05,
|
|
54
|
+
amplitude = 1.0,
|
|
55
|
+
cellSize = 12,
|
|
56
|
+
fps = 30,
|
|
57
|
+
preset,
|
|
58
|
+
class: className = '',
|
|
59
|
+
}: GossamerCloudsProps = $props();
|
|
60
|
+
|
|
61
|
+
// State
|
|
62
|
+
let canvas: HTMLCanvasElement;
|
|
63
|
+
let container: HTMLDivElement;
|
|
64
|
+
let renderer: GossamerRenderer | null = null;
|
|
65
|
+
let isVisible = true;
|
|
66
|
+
let reducedMotion = false;
|
|
67
|
+
let animationId: number | null = null;
|
|
68
|
+
|
|
69
|
+
// Apply preset if specified
|
|
70
|
+
const config = $derived(() => {
|
|
71
|
+
if (preset && PRESETS[preset]) {
|
|
72
|
+
const p = PRESETS[preset];
|
|
73
|
+
return {
|
|
74
|
+
pattern: p.pattern as PatternType,
|
|
75
|
+
characters: p.characters,
|
|
76
|
+
frequency: p.frequency,
|
|
77
|
+
amplitude: p.amplitude,
|
|
78
|
+
speed: p.speed,
|
|
79
|
+
opacity: p.opacity,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { pattern, characters, frequency, amplitude, speed, opacity };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Should animate based on all factors
|
|
86
|
+
const shouldAnimate = $derived(animated && isVisible && !reducedMotion);
|
|
87
|
+
|
|
88
|
+
// Animation state
|
|
89
|
+
let startTime = 0;
|
|
90
|
+
|
|
91
|
+
function animate(currentTime: number): void {
|
|
92
|
+
if (!renderer || !shouldAnimate) return;
|
|
93
|
+
|
|
94
|
+
const elapsed = (currentTime - startTime) / 1000;
|
|
95
|
+
const { cols, rows } = renderer.getCellCount();
|
|
96
|
+
const cfg = config();
|
|
97
|
+
|
|
98
|
+
const grid = generateBrightnessGrid(
|
|
99
|
+
cols,
|
|
100
|
+
rows,
|
|
101
|
+
cfg.pattern,
|
|
102
|
+
elapsed,
|
|
103
|
+
{
|
|
104
|
+
frequency: cfg.frequency,
|
|
105
|
+
amplitude: cfg.amplitude,
|
|
106
|
+
speed: cfg.speed,
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
renderer.renderFromBrightnessGrid(grid);
|
|
111
|
+
animationId = requestAnimationFrame(animate);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function startAnimation(): void {
|
|
115
|
+
if (animationId !== null) return;
|
|
116
|
+
startTime = performance.now();
|
|
117
|
+
animationId = requestAnimationFrame(animate);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stopAnimation(): void {
|
|
121
|
+
if (animationId !== null) {
|
|
122
|
+
cancelAnimationFrame(animationId);
|
|
123
|
+
animationId = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderStatic(): void {
|
|
128
|
+
if (!renderer) return;
|
|
129
|
+
|
|
130
|
+
const { cols, rows } = renderer.getCellCount();
|
|
131
|
+
const cfg = config();
|
|
132
|
+
|
|
133
|
+
const grid = generateBrightnessGrid(
|
|
134
|
+
cols,
|
|
135
|
+
rows,
|
|
136
|
+
cfg.pattern,
|
|
137
|
+
0,
|
|
138
|
+
{
|
|
139
|
+
frequency: cfg.frequency,
|
|
140
|
+
amplitude: cfg.amplitude,
|
|
141
|
+
speed: 0,
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
renderer.renderFromBrightnessGrid(grid);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setupRenderer(width: number, height: number): void {
|
|
149
|
+
if (!canvas) return;
|
|
150
|
+
|
|
151
|
+
const cfg = config();
|
|
152
|
+
|
|
153
|
+
// Create or update renderer
|
|
154
|
+
if (renderer) {
|
|
155
|
+
renderer.destroy();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
canvas.width = width;
|
|
159
|
+
canvas.height = height;
|
|
160
|
+
|
|
161
|
+
renderer = new GossamerRenderer(canvas, {
|
|
162
|
+
characters: cfg.characters,
|
|
163
|
+
cellWidth: cellSize,
|
|
164
|
+
cellHeight: cellSize,
|
|
165
|
+
color,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (shouldAnimate) {
|
|
169
|
+
startAnimation();
|
|
170
|
+
} else {
|
|
171
|
+
renderStatic();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Lifecycle
|
|
176
|
+
onMount(() => {
|
|
177
|
+
// Watch for reduced motion preference
|
|
178
|
+
const cleanupMotion = onReducedMotionChange((prefers) => {
|
|
179
|
+
reducedMotion = prefers;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Watch for visibility changes
|
|
183
|
+
const cleanupVisibility = createVisibilityObserver(
|
|
184
|
+
container,
|
|
185
|
+
(visible) => {
|
|
186
|
+
isVisible = visible;
|
|
187
|
+
if (visible && shouldAnimate) {
|
|
188
|
+
startAnimation();
|
|
189
|
+
} else {
|
|
190
|
+
stopAnimation();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
0.1
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Watch for resize
|
|
197
|
+
const cleanupResize = createResizeObserver(
|
|
198
|
+
container,
|
|
199
|
+
(width, height) => {
|
|
200
|
+
setupRenderer(width, height);
|
|
201
|
+
},
|
|
202
|
+
100
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Initial setup
|
|
206
|
+
const rect = container.getBoundingClientRect();
|
|
207
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
208
|
+
setupRenderer(rect.width, rect.height);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
cleanupMotion();
|
|
213
|
+
cleanupVisibility();
|
|
214
|
+
cleanupResize();
|
|
215
|
+
stopAnimation();
|
|
216
|
+
renderer?.destroy();
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// React to prop changes
|
|
221
|
+
$effect(() => {
|
|
222
|
+
if (renderer) {
|
|
223
|
+
const cfg = config();
|
|
224
|
+
renderer.updateConfig({
|
|
225
|
+
characters: cfg.characters,
|
|
226
|
+
color,
|
|
227
|
+
cellWidth: cellSize,
|
|
228
|
+
cellHeight: cellSize,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (shouldAnimate) {
|
|
232
|
+
startAnimation();
|
|
233
|
+
} else {
|
|
234
|
+
stopAnimation();
|
|
235
|
+
renderStatic();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<div
|
|
242
|
+
bind:this={container}
|
|
243
|
+
class="gossamer-clouds {className}"
|
|
244
|
+
style:opacity={config().opacity}
|
|
245
|
+
>
|
|
246
|
+
<canvas
|
|
247
|
+
bind:this={canvas}
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
class="gossamer-canvas"
|
|
250
|
+
></canvas>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<style>
|
|
254
|
+
.gossamer-clouds {
|
|
255
|
+
position: absolute;
|
|
256
|
+
inset: 0;
|
|
257
|
+
width: 100%;
|
|
258
|
+
height: 100%;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
pointer-events: none;
|
|
261
|
+
z-index: 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.gossamer-canvas {
|
|
265
|
+
display: block;
|
|
266
|
+
width: 100%;
|
|
267
|
+
height: 100%;
|
|
268
|
+
}
|
|
269
|
+
</style>
|