@autumnsgrove/gossamer 0.0.1 → 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.
Files changed (53) hide show
  1. package/dist/animation.d.ts +80 -0
  2. package/dist/animation.d.ts.map +1 -0
  3. package/dist/characters.d.ts +49 -0
  4. package/dist/characters.d.ts.map +1 -0
  5. package/dist/colors.d.ts +312 -0
  6. package/dist/colors.d.ts.map +1 -0
  7. package/dist/index.d.ts +39 -11
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1826 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/patterns.d.ts +217 -0
  12. package/dist/patterns.d.ts.map +1 -0
  13. package/dist/renderer.d.ts +140 -0
  14. package/dist/renderer.d.ts.map +1 -0
  15. package/dist/style.css +124 -0
  16. package/dist/svelte/GossamerBorder.svelte.d.ts +1 -0
  17. package/dist/svelte/GossamerClouds.svelte.d.ts +1 -0
  18. package/dist/svelte/GossamerImage.svelte.d.ts +1 -0
  19. package/dist/svelte/GossamerOverlay.svelte.d.ts +1 -0
  20. package/dist/svelte/GossamerText.svelte.d.ts +1 -0
  21. package/dist/svelte/index.d.ts +20 -0
  22. package/dist/svelte/index.d.ts.map +1 -0
  23. package/dist/svelte/index.js +3648 -0
  24. package/dist/svelte/index.js.map +1 -0
  25. package/dist/svelte/presets.d.ts +38 -0
  26. package/dist/svelte/presets.d.ts.map +1 -0
  27. package/dist/utils/canvas.d.ts +73 -0
  28. package/dist/utils/canvas.d.ts.map +1 -0
  29. package/dist/utils/image.d.ts +74 -0
  30. package/dist/utils/image.d.ts.map +1 -0
  31. package/dist/utils/performance.d.ts +86 -0
  32. package/dist/utils/performance.d.ts.map +1 -0
  33. package/package.json +34 -7
  34. package/src/animation.test.ts +254 -0
  35. package/src/animation.ts +243 -0
  36. package/src/characters.test.ts +148 -0
  37. package/src/characters.ts +219 -0
  38. package/src/colors.ts +234 -0
  39. package/src/index.test.ts +115 -0
  40. package/src/index.ts +164 -11
  41. package/src/patterns.test.ts +273 -0
  42. package/src/patterns.ts +760 -0
  43. package/src/renderer.ts +470 -0
  44. package/src/svelte/GossamerBorder.svelte +326 -0
  45. package/src/svelte/GossamerClouds.svelte +269 -0
  46. package/src/svelte/GossamerImage.svelte +266 -0
  47. package/src/svelte/GossamerOverlay.svelte +232 -0
  48. package/src/svelte/GossamerText.svelte +239 -0
  49. package/src/svelte/index.ts +75 -0
  50. package/src/svelte/presets.ts +174 -0
  51. package/src/utils/canvas.ts +210 -0
  52. package/src/utils/image.ts +275 -0
  53. 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.by(() => {
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>