@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.
Files changed (50) 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/index.d.ts +37 -11
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1284 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/patterns.d.ts +100 -0
  10. package/dist/patterns.d.ts.map +1 -0
  11. package/dist/renderer.d.ts +113 -0
  12. package/dist/renderer.d.ts.map +1 -0
  13. package/dist/style.css +124 -0
  14. package/dist/svelte/GossamerBorder.svelte.d.ts +1 -0
  15. package/dist/svelte/GossamerClouds.svelte.d.ts +1 -0
  16. package/dist/svelte/GossamerImage.svelte.d.ts +1 -0
  17. package/dist/svelte/GossamerOverlay.svelte.d.ts +1 -0
  18. package/dist/svelte/GossamerText.svelte.d.ts +1 -0
  19. package/dist/svelte/index.d.ts +20 -0
  20. package/dist/svelte/index.d.ts.map +1 -0
  21. package/dist/svelte/index.js +3651 -0
  22. package/dist/svelte/index.js.map +1 -0
  23. package/dist/svelte/presets.d.ts +38 -0
  24. package/dist/svelte/presets.d.ts.map +1 -0
  25. package/dist/utils/canvas.d.ts +73 -0
  26. package/dist/utils/canvas.d.ts.map +1 -0
  27. package/dist/utils/image.d.ts +74 -0
  28. package/dist/utils/image.d.ts.map +1 -0
  29. package/dist/utils/performance.d.ts +86 -0
  30. package/dist/utils/performance.d.ts.map +1 -0
  31. package/package.json +23 -5
  32. package/src/animation.test.ts +254 -0
  33. package/src/animation.ts +243 -0
  34. package/src/characters.test.ts +148 -0
  35. package/src/characters.ts +164 -0
  36. package/src/index.test.ts +115 -0
  37. package/src/index.ts +133 -11
  38. package/src/patterns.test.ts +273 -0
  39. package/src/patterns.ts +316 -0
  40. package/src/renderer.ts +309 -0
  41. package/src/svelte/GossamerBorder.svelte +326 -0
  42. package/src/svelte/GossamerClouds.svelte +269 -0
  43. package/src/svelte/GossamerImage.svelte +266 -0
  44. package/src/svelte/GossamerOverlay.svelte +232 -0
  45. package/src/svelte/GossamerText.svelte +239 -0
  46. package/src/svelte/index.ts +75 -0
  47. package/src/svelte/presets.ts +174 -0
  48. package/src/utils/canvas.ts +210 -0
  49. package/src/utils/image.ts +275 -0
  50. package/src/utils/performance.ts +282 -0
@@ -0,0 +1,266 @@
1
+ <script lang="ts" module>
2
+ export interface GossamerImageProps {
3
+ /** Image source URL */
4
+ src: string;
5
+ /** Alt text for accessibility */
6
+ alt: string;
7
+ /** Character set (light to dark) */
8
+ characters?: string;
9
+ /** Cell size for ASCII detail level */
10
+ cellSize?: number;
11
+ /** Single color or 'preserve' to keep image colors */
12
+ color?: string | 'preserve';
13
+ /** Invert brightness mapping */
14
+ invert?: boolean;
15
+ /** Output width in pixels */
16
+ width?: number;
17
+ /** Output height in pixels */
18
+ height?: number;
19
+ /** Show original image on hover */
20
+ showOriginalOnHover?: boolean;
21
+ /** Hover transition duration in ms */
22
+ transitionDuration?: number;
23
+ /** Additional CSS class */
24
+ class?: string;
25
+ }
26
+ </script>
27
+
28
+ <script lang="ts">
29
+ import { onMount } from 'svelte';
30
+ import {
31
+ loadImage,
32
+ imageToPixelData,
33
+ sampleImageCells,
34
+ brightnessToChar,
35
+ invertCharacters,
36
+ CHARACTER_SETS,
37
+ } from '../index';
38
+
39
+ // Props with defaults
40
+ let {
41
+ src,
42
+ alt,
43
+ characters = CHARACTER_SETS.standard.characters,
44
+ cellSize = 8,
45
+ color = '#ffffff',
46
+ invert = false,
47
+ width,
48
+ height,
49
+ showOriginalOnHover = false,
50
+ transitionDuration = 300,
51
+ class: className = '',
52
+ }: GossamerImageProps = $props();
53
+
54
+ // State
55
+ let canvas: HTMLCanvasElement;
56
+ let container: HTMLDivElement;
57
+ let isLoading = true;
58
+ let hasError = false;
59
+ let isHovered = false;
60
+ let loadedImage: HTMLImageElement | null = null;
61
+ let imageWidth = 0;
62
+ let imageHeight = 0;
63
+
64
+ // Effective characters (possibly inverted)
65
+ const effectiveCharacters = $derived(invert ? invertCharacters(characters) : characters);
66
+
67
+ async function loadAndRender(): Promise<void> {
68
+ isLoading = true;
69
+ hasError = false;
70
+
71
+ try {
72
+ const img = await loadImage(src, { crossOrigin: 'anonymous' });
73
+ loadedImage = img;
74
+
75
+ // Calculate dimensions
76
+ const naturalWidth = img.naturalWidth;
77
+ const naturalHeight = img.naturalHeight;
78
+ const aspectRatio = naturalWidth / naturalHeight;
79
+
80
+ if (width && height) {
81
+ imageWidth = width;
82
+ imageHeight = height;
83
+ } else if (width) {
84
+ imageWidth = width;
85
+ imageHeight = Math.round(width / aspectRatio);
86
+ } else if (height) {
87
+ imageHeight = height;
88
+ imageWidth = Math.round(height * aspectRatio);
89
+ } else {
90
+ imageWidth = naturalWidth;
91
+ imageHeight = naturalHeight;
92
+ }
93
+
94
+ renderASCII();
95
+ isLoading = false;
96
+ } catch {
97
+ hasError = true;
98
+ isLoading = false;
99
+ }
100
+ }
101
+
102
+ function renderASCII(): void {
103
+ if (!canvas || !loadedImage) return;
104
+
105
+ canvas.width = imageWidth;
106
+ canvas.height = imageHeight;
107
+
108
+ const ctx = canvas.getContext('2d');
109
+ if (!ctx) return;
110
+
111
+ // Get image pixel data
112
+ const pixelData = imageToPixelData(loadedImage, imageWidth, imageHeight);
113
+ const cells = sampleImageCells(pixelData, cellSize, cellSize);
114
+
115
+ // Clear canvas
116
+ ctx.clearRect(0, 0, imageWidth, imageHeight);
117
+
118
+ // Set up text rendering
119
+ ctx.font = `${cellSize}px monospace`;
120
+ ctx.textBaseline = 'top';
121
+ ctx.textAlign = 'left';
122
+
123
+ // Render each cell
124
+ for (let row = 0; row < cells.length; row++) {
125
+ for (let col = 0; col < cells[row].length; col++) {
126
+ const cell = cells[row][col];
127
+ const char = brightnessToChar(cell.brightness, effectiveCharacters);
128
+
129
+ if (char !== ' ') {
130
+ // Use image color or fixed color
131
+ ctx.fillStyle = color === 'preserve' ? cell.color : color;
132
+ ctx.fillText(char, col * cellSize, row * cellSize);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Lifecycle
139
+ onMount(() => {
140
+ loadAndRender();
141
+ });
142
+
143
+ // React to src changes
144
+ $effect(() => {
145
+ if (src) {
146
+ loadAndRender();
147
+ }
148
+ });
149
+
150
+ // React to render config changes
151
+ $effect(() => {
152
+ // Track these dependencies
153
+ const _ = [characters, cellSize, color, invert, width, height];
154
+ if (loadedImage) {
155
+ renderASCII();
156
+ }
157
+ });
158
+
159
+ function handleMouseEnter(): void {
160
+ if (showOriginalOnHover) {
161
+ isHovered = true;
162
+ }
163
+ }
164
+
165
+ function handleMouseLeave(): void {
166
+ isHovered = false;
167
+ }
168
+ </script>
169
+
170
+ <div
171
+ bind:this={container}
172
+ class="gossamer-image {className}"
173
+ class:loading={isLoading}
174
+ class:error={hasError}
175
+ class:hoverable={showOriginalOnHover}
176
+ style:width={imageWidth ? `${imageWidth}px` : undefined}
177
+ style:height={imageHeight ? `${imageHeight}px` : undefined}
178
+ role="img"
179
+ aria-label={alt}
180
+ onmouseenter={handleMouseEnter}
181
+ onmouseleave={handleMouseLeave}
182
+ >
183
+ {#if isLoading}
184
+ <div class="gossamer-image-loading">
185
+ <span>Loading...</span>
186
+ </div>
187
+ {:else if hasError}
188
+ <div class="gossamer-image-error">
189
+ <span>Failed to load image</span>
190
+ </div>
191
+ {:else}
192
+ <canvas
193
+ bind:this={canvas}
194
+ class="gossamer-canvas"
195
+ class:hidden={showOriginalOnHover && isHovered}
196
+ style:transition-duration="{transitionDuration}ms"
197
+ aria-hidden="true"
198
+ ></canvas>
199
+
200
+ {#if showOriginalOnHover && loadedImage}
201
+ <img
202
+ {src}
203
+ {alt}
204
+ class="gossamer-original"
205
+ class:visible={isHovered}
206
+ style:transition-duration="{transitionDuration}ms"
207
+ width={imageWidth}
208
+ height={imageHeight}
209
+ />
210
+ {/if}
211
+ {/if}
212
+ </div>
213
+
214
+ <style>
215
+ .gossamer-image {
216
+ position: relative;
217
+ display: inline-block;
218
+ overflow: hidden;
219
+ }
220
+
221
+ .gossamer-image.hoverable {
222
+ cursor: pointer;
223
+ }
224
+
225
+ .gossamer-canvas {
226
+ display: block;
227
+ opacity: 1;
228
+ transition-property: opacity;
229
+ transition-timing-function: ease-in-out;
230
+ }
231
+
232
+ .gossamer-canvas.hidden {
233
+ opacity: 0;
234
+ }
235
+
236
+ .gossamer-original {
237
+ position: absolute;
238
+ inset: 0;
239
+ width: 100%;
240
+ height: 100%;
241
+ object-fit: cover;
242
+ opacity: 0;
243
+ transition-property: opacity;
244
+ transition-timing-function: ease-in-out;
245
+ }
246
+
247
+ .gossamer-original.visible {
248
+ opacity: 1;
249
+ }
250
+
251
+ .gossamer-image-loading,
252
+ .gossamer-image-error {
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ width: 100%;
257
+ height: 100%;
258
+ min-height: 100px;
259
+ color: currentColor;
260
+ opacity: 0.5;
261
+ }
262
+
263
+ .gossamer-image-error {
264
+ color: #ef4444;
265
+ }
266
+ </style>
@@ -0,0 +1,232 @@
1
+ <script lang="ts" module>
2
+ import type { PatternType } from '../index';
3
+
4
+ export type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay' | 'soft-light' | 'hard-light' | 'difference';
5
+
6
+ export interface GossamerOverlayProps {
7
+ /** Pattern type */
8
+ pattern?: PatternType;
9
+ /** Character set (light to dark) */
10
+ characters?: string;
11
+ /** Foreground color */
12
+ color?: string;
13
+ /** Overall opacity (0-1) */
14
+ opacity?: number;
15
+ /** CSS blend mode */
16
+ blendMode?: BlendMode;
17
+ /** Enable animation */
18
+ animated?: boolean;
19
+ /** Animation speed */
20
+ speed?: number;
21
+ /** Pattern frequency */
22
+ frequency?: number;
23
+ /** Pattern amplitude */
24
+ amplitude?: number;
25
+ /** Cell size in pixels */
26
+ cellSize?: number;
27
+ /** Target FPS */
28
+ fps?: number;
29
+ /** Additional CSS class */
30
+ class?: string;
31
+ }
32
+ </script>
33
+
34
+ <script lang="ts">
35
+ import { onMount } from 'svelte';
36
+ import {
37
+ GossamerRenderer,
38
+ generateBrightnessGrid,
39
+ createVisibilityObserver,
40
+ createResizeObserver,
41
+ onReducedMotionChange,
42
+ CHARACTER_SETS,
43
+ } from '../index';
44
+
45
+ // Props with defaults
46
+ let {
47
+ pattern = 'perlin',
48
+ characters = CHARACTER_SETS.minimal.characters,
49
+ color = 'currentColor',
50
+ opacity = 0.15,
51
+ blendMode = 'overlay',
52
+ animated = true,
53
+ speed = 0.3,
54
+ frequency = 0.03,
55
+ amplitude = 0.6,
56
+ cellSize = 16,
57
+ fps = 30,
58
+ class: className = '',
59
+ }: GossamerOverlayProps = $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
+ const shouldAnimate = $derived(animated && isVisible && !reducedMotion);
70
+
71
+ let startTime = 0;
72
+
73
+ function animate(currentTime: number): void {
74
+ if (!renderer || !shouldAnimate) return;
75
+
76
+ const elapsed = (currentTime - startTime) / 1000;
77
+ const { cols, rows } = renderer.getCellCount();
78
+
79
+ const grid = generateBrightnessGrid(
80
+ cols,
81
+ rows,
82
+ pattern,
83
+ elapsed,
84
+ { frequency, amplitude, speed }
85
+ );
86
+
87
+ renderer.renderFromBrightnessGrid(grid);
88
+ animationId = requestAnimationFrame(animate);
89
+ }
90
+
91
+ function startAnimation(): void {
92
+ if (animationId !== null) return;
93
+ startTime = performance.now();
94
+ animationId = requestAnimationFrame(animate);
95
+ }
96
+
97
+ function stopAnimation(): void {
98
+ if (animationId !== null) {
99
+ cancelAnimationFrame(animationId);
100
+ animationId = null;
101
+ }
102
+ }
103
+
104
+ function renderStatic(): void {
105
+ if (!renderer) return;
106
+
107
+ const { cols, rows } = renderer.getCellCount();
108
+ const grid = generateBrightnessGrid(
109
+ cols,
110
+ rows,
111
+ pattern,
112
+ 0,
113
+ { frequency, amplitude, speed: 0 }
114
+ );
115
+
116
+ renderer.renderFromBrightnessGrid(grid);
117
+ }
118
+
119
+ function setupRenderer(width: number, height: number): void {
120
+ if (!canvas) return;
121
+
122
+ if (renderer) {
123
+ renderer.destroy();
124
+ }
125
+
126
+ canvas.width = width;
127
+ canvas.height = height;
128
+
129
+ renderer = new GossamerRenderer(canvas, {
130
+ characters,
131
+ cellWidth: cellSize,
132
+ cellHeight: cellSize,
133
+ color,
134
+ });
135
+
136
+ if (shouldAnimate) {
137
+ startAnimation();
138
+ } else {
139
+ renderStatic();
140
+ }
141
+ }
142
+
143
+ // Lifecycle
144
+ onMount(() => {
145
+ const cleanupMotion = onReducedMotionChange((prefers) => {
146
+ reducedMotion = prefers;
147
+ });
148
+
149
+ const cleanupVisibility = createVisibilityObserver(
150
+ container,
151
+ (visible) => {
152
+ isVisible = visible;
153
+ if (visible && shouldAnimate) {
154
+ startAnimation();
155
+ } else {
156
+ stopAnimation();
157
+ }
158
+ },
159
+ 0.1
160
+ );
161
+
162
+ const cleanupResize = createResizeObserver(
163
+ container,
164
+ (width, height) => {
165
+ setupRenderer(width, height);
166
+ },
167
+ 100
168
+ );
169
+
170
+ const rect = container.getBoundingClientRect();
171
+ if (rect.width > 0 && rect.height > 0) {
172
+ setupRenderer(rect.width, rect.height);
173
+ }
174
+
175
+ return () => {
176
+ cleanupMotion();
177
+ cleanupVisibility();
178
+ cleanupResize();
179
+ stopAnimation();
180
+ renderer?.destroy();
181
+ };
182
+ });
183
+
184
+ $effect(() => {
185
+ if (renderer) {
186
+ renderer.updateConfig({
187
+ characters,
188
+ color,
189
+ cellWidth: cellSize,
190
+ cellHeight: cellSize,
191
+ });
192
+
193
+ if (shouldAnimate) {
194
+ startAnimation();
195
+ } else {
196
+ stopAnimation();
197
+ renderStatic();
198
+ }
199
+ }
200
+ });
201
+ </script>
202
+
203
+ <div
204
+ bind:this={container}
205
+ class="gossamer-overlay {className}"
206
+ style:opacity
207
+ style:mix-blend-mode={blendMode}
208
+ >
209
+ <canvas
210
+ bind:this={canvas}
211
+ aria-hidden="true"
212
+ class="gossamer-canvas"
213
+ ></canvas>
214
+ </div>
215
+
216
+ <style>
217
+ .gossamer-overlay {
218
+ position: absolute;
219
+ inset: 0;
220
+ width: 100%;
221
+ height: 100%;
222
+ overflow: hidden;
223
+ pointer-events: none;
224
+ z-index: 1;
225
+ }
226
+
227
+ .gossamer-canvas {
228
+ display: block;
229
+ width: 100%;
230
+ height: 100%;
231
+ }
232
+ </style>