@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,275 @@
1
+ /**
2
+ * Image Utilities
3
+ *
4
+ * Image loading, processing, and pixel manipulation for ASCII conversion.
5
+ */
6
+
7
+ /**
8
+ * Image loading options
9
+ */
10
+ export interface ImageLoadOptions {
11
+ /** Cross-origin setting for external images */
12
+ crossOrigin?: 'anonymous' | 'use-credentials' | '';
13
+ /** Maximum width to scale image to */
14
+ maxWidth?: number;
15
+ /** Maximum height to scale image to */
16
+ maxHeight?: number;
17
+ /** Whether to preserve aspect ratio when scaling */
18
+ preserveAspectRatio?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Load an image from a URL
23
+ */
24
+ export function loadImage(src: string, options: ImageLoadOptions = {}): Promise<HTMLImageElement> {
25
+ return new Promise((resolve, reject) => {
26
+ const img = new Image();
27
+
28
+ if (options.crossOrigin !== undefined) {
29
+ img.crossOrigin = options.crossOrigin;
30
+ }
31
+
32
+ img.onload = () => resolve(img);
33
+ img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
34
+
35
+ img.src = src;
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Load and scale an image to fit within bounds
41
+ */
42
+ export async function loadAndScaleImage(
43
+ src: string,
44
+ maxWidth: number,
45
+ maxHeight: number,
46
+ options: ImageLoadOptions = {}
47
+ ): Promise<{ image: HTMLImageElement; width: number; height: number }> {
48
+ const img = await loadImage(src, options);
49
+
50
+ let width = img.naturalWidth;
51
+ let height = img.naturalHeight;
52
+
53
+ // Scale down if needed
54
+ if (width > maxWidth || height > maxHeight) {
55
+ const widthRatio = maxWidth / width;
56
+ const heightRatio = maxHeight / height;
57
+ const scale = Math.min(widthRatio, heightRatio);
58
+
59
+ width = Math.floor(width * scale);
60
+ height = Math.floor(height * scale);
61
+ }
62
+
63
+ return { image: img, width, height };
64
+ }
65
+
66
+ /**
67
+ * Draw an image to a canvas and get its pixel data
68
+ */
69
+ export function imageToPixelData(
70
+ image: HTMLImageElement | HTMLCanvasElement | ImageBitmap,
71
+ width?: number,
72
+ height?: number
73
+ ): ImageData {
74
+ const w = width ?? (image instanceof HTMLImageElement ? image.naturalWidth : image.width);
75
+ const h = height ?? (image instanceof HTMLImageElement ? image.naturalHeight : image.height);
76
+
77
+ const canvas = document.createElement('canvas');
78
+ canvas.width = w;
79
+ canvas.height = h;
80
+
81
+ const ctx = canvas.getContext('2d');
82
+ if (!ctx) {
83
+ throw new Error('Failed to get 2D context');
84
+ }
85
+
86
+ ctx.drawImage(image, 0, 0, w, h);
87
+ return ctx.getImageData(0, 0, w, h);
88
+ }
89
+
90
+ /**
91
+ * Extract brightness values from image data
92
+ */
93
+ export function extractBrightness(
94
+ imageData: ImageData,
95
+ brightnessFunction: (r: number, g: number, b: number) => number = (r, g, b) => 0.21 * r + 0.72 * g + 0.07 * b
96
+ ): number[] {
97
+ const { data, width, height } = imageData;
98
+ const brightness: number[] = new Array(width * height);
99
+
100
+ for (let i = 0; i < data.length; i += 4) {
101
+ brightness[i / 4] = brightnessFunction(data[i], data[i + 1], data[i + 2]);
102
+ }
103
+
104
+ return brightness;
105
+ }
106
+
107
+ /**
108
+ * Sample image data at cell-based intervals
109
+ */
110
+ export function sampleImageCells(
111
+ imageData: ImageData,
112
+ cellWidth: number,
113
+ cellHeight: number,
114
+ brightnessFunction: (r: number, g: number, b: number) => number = (r, g, b) => 0.21 * r + 0.72 * g + 0.07 * b
115
+ ): { brightness: number; color: string }[][] {
116
+ const { data, width, height } = imageData;
117
+ const cols = Math.ceil(width / cellWidth);
118
+ const rows = Math.ceil(height / cellHeight);
119
+
120
+ const result: { brightness: number; color: string }[][] = [];
121
+
122
+ for (let row = 0; row < rows; row++) {
123
+ result[row] = [];
124
+
125
+ for (let col = 0; col < cols; col++) {
126
+ const cellData = sampleCell(data, width, col * cellWidth, row * cellHeight, cellWidth, cellHeight);
127
+ const brightness = brightnessFunction(cellData.r, cellData.g, cellData.b);
128
+
129
+ result[row][col] = {
130
+ brightness,
131
+ color: `rgb(${Math.round(cellData.r)}, ${Math.round(cellData.g)}, ${Math.round(cellData.b)})`,
132
+ };
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Sample average color from a cell region
141
+ */
142
+ function sampleCell(
143
+ data: Uint8ClampedArray,
144
+ imageWidth: number,
145
+ startX: number,
146
+ startY: number,
147
+ cellWidth: number,
148
+ cellHeight: number
149
+ ): { r: number; g: number; b: number; a: number } {
150
+ let totalR = 0;
151
+ let totalG = 0;
152
+ let totalB = 0;
153
+ let totalA = 0;
154
+ let count = 0;
155
+
156
+ for (let y = startY; y < startY + cellHeight; y++) {
157
+ for (let x = startX; x < startX + cellWidth; x++) {
158
+ const px = (y * imageWidth + x) * 4;
159
+
160
+ if (px >= 0 && px + 3 < data.length) {
161
+ totalR += data[px];
162
+ totalG += data[px + 1];
163
+ totalB += data[px + 2];
164
+ totalA += data[px + 3];
165
+ count++;
166
+ }
167
+ }
168
+ }
169
+
170
+ if (count === 0) {
171
+ return { r: 0, g: 0, b: 0, a: 0 };
172
+ }
173
+
174
+ return {
175
+ r: totalR / count,
176
+ g: totalG / count,
177
+ b: totalB / count,
178
+ a: totalA / count,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Convert RGB to hex color string
184
+ */
185
+ export function rgbToHex(r: number, g: number, b: number): string {
186
+ const toHex = (n: number) =>
187
+ Math.max(0, Math.min(255, Math.round(n)))
188
+ .toString(16)
189
+ .padStart(2, '0');
190
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
191
+ }
192
+
193
+ /**
194
+ * Convert hex color to RGB
195
+ */
196
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
197
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
198
+ return result
199
+ ? {
200
+ r: parseInt(result[1], 16),
201
+ g: parseInt(result[2], 16),
202
+ b: parseInt(result[3], 16),
203
+ }
204
+ : null;
205
+ }
206
+
207
+ /**
208
+ * Adjust image brightness
209
+ */
210
+ export function adjustBrightness(imageData: ImageData, amount: number): ImageData {
211
+ const { data, width, height } = imageData;
212
+ const adjusted = new Uint8ClampedArray(data.length);
213
+
214
+ for (let i = 0; i < data.length; i += 4) {
215
+ adjusted[i] = Math.min(255, Math.max(0, data[i] + amount));
216
+ adjusted[i + 1] = Math.min(255, Math.max(0, data[i + 1] + amount));
217
+ adjusted[i + 2] = Math.min(255, Math.max(0, data[i + 2] + amount));
218
+ adjusted[i + 3] = data[i + 3];
219
+ }
220
+
221
+ return new ImageData(adjusted, width, height);
222
+ }
223
+
224
+ /**
225
+ * Adjust image contrast
226
+ */
227
+ export function adjustContrast(imageData: ImageData, amount: number): ImageData {
228
+ const { data, width, height } = imageData;
229
+ const adjusted = new Uint8ClampedArray(data.length);
230
+ const factor = (259 * (amount + 255)) / (255 * (259 - amount));
231
+
232
+ for (let i = 0; i < data.length; i += 4) {
233
+ adjusted[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128));
234
+ adjusted[i + 1] = Math.min(255, Math.max(0, factor * (data[i + 1] - 128) + 128));
235
+ adjusted[i + 2] = Math.min(255, Math.max(0, factor * (data[i + 2] - 128) + 128));
236
+ adjusted[i + 3] = data[i + 3];
237
+ }
238
+
239
+ return new ImageData(adjusted, width, height);
240
+ }
241
+
242
+ /**
243
+ * Invert image colors
244
+ */
245
+ export function invertColors(imageData: ImageData): ImageData {
246
+ const { data, width, height } = imageData;
247
+ const inverted = new Uint8ClampedArray(data.length);
248
+
249
+ for (let i = 0; i < data.length; i += 4) {
250
+ inverted[i] = 255 - data[i];
251
+ inverted[i + 1] = 255 - data[i + 1];
252
+ inverted[i + 2] = 255 - data[i + 2];
253
+ inverted[i + 3] = data[i + 3];
254
+ }
255
+
256
+ return new ImageData(inverted, width, height);
257
+ }
258
+
259
+ /**
260
+ * Convert image to grayscale
261
+ */
262
+ export function toGrayscale(imageData: ImageData): ImageData {
263
+ const { data, width, height } = imageData;
264
+ const grayscale = new Uint8ClampedArray(data.length);
265
+
266
+ for (let i = 0; i < data.length; i += 4) {
267
+ const gray = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2];
268
+ grayscale[i] = gray;
269
+ grayscale[i + 1] = gray;
270
+ grayscale[i + 2] = gray;
271
+ grayscale[i + 3] = data[i + 3];
272
+ }
273
+
274
+ return new ImageData(grayscale, width, height);
275
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Performance Utilities
3
+ *
4
+ * Visibility detection, resource management, and optimization helpers.
5
+ */
6
+
7
+ /**
8
+ * Visibility observer callback type
9
+ */
10
+ export type VisibilityCallback = (isVisible: boolean, entry: IntersectionObserverEntry) => void;
11
+
12
+ /**
13
+ * Create an IntersectionObserver for visibility-based animation control
14
+ *
15
+ * @param element - Element to observe
16
+ * @param callback - Called when visibility changes
17
+ * @param threshold - Visibility threshold (0-1, default: 0.1)
18
+ * @returns Cleanup function to disconnect observer
19
+ */
20
+ export function createVisibilityObserver(
21
+ element: Element,
22
+ callback: VisibilityCallback,
23
+ threshold: number = 0.1
24
+ ): () => void {
25
+ const observer = new IntersectionObserver(
26
+ (entries) => {
27
+ for (const entry of entries) {
28
+ callback(entry.isIntersecting, entry);
29
+ }
30
+ },
31
+ { threshold }
32
+ );
33
+
34
+ observer.observe(element);
35
+
36
+ return () => {
37
+ observer.disconnect();
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Create a ResizeObserver for responsive canvas sizing
43
+ *
44
+ * @param element - Element to observe
45
+ * @param callback - Called on resize with new dimensions
46
+ * @param debounceMs - Debounce delay in ms (default: 100)
47
+ * @returns Cleanup function to disconnect observer
48
+ */
49
+ export function createResizeObserver(
50
+ element: Element,
51
+ callback: (width: number, height: number, entry: ResizeObserverEntry) => void,
52
+ debounceMs: number = 100
53
+ ): () => void {
54
+ let timeout: ReturnType<typeof setTimeout> | null = null;
55
+
56
+ const observer = new ResizeObserver((entries) => {
57
+ if (timeout) {
58
+ clearTimeout(timeout);
59
+ }
60
+
61
+ timeout = setTimeout(() => {
62
+ const entry = entries[0];
63
+ if (entry) {
64
+ const { width, height } = entry.contentRect;
65
+ callback(width, height, entry);
66
+ }
67
+ }, debounceMs);
68
+ });
69
+
70
+ observer.observe(element);
71
+
72
+ return () => {
73
+ if (timeout) {
74
+ clearTimeout(timeout);
75
+ }
76
+ observer.disconnect();
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Check if the user prefers reduced motion
82
+ */
83
+ export function prefersReducedMotion(): boolean {
84
+ if (typeof window === 'undefined') return false;
85
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
86
+ }
87
+
88
+ /**
89
+ * Create a listener for reduced motion preference changes
90
+ *
91
+ * @param callback - Called when preference changes
92
+ * @returns Cleanup function to remove listener
93
+ */
94
+ export function onReducedMotionChange(callback: (prefersReduced: boolean) => void): () => void {
95
+ if (typeof window === 'undefined') {
96
+ return () => {};
97
+ }
98
+
99
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
100
+
101
+ const handler = (event: MediaQueryListEvent) => {
102
+ callback(event.matches);
103
+ };
104
+
105
+ mediaQuery.addEventListener('change', handler);
106
+
107
+ // Call immediately with current value
108
+ callback(mediaQuery.matches);
109
+
110
+ return () => {
111
+ mediaQuery.removeEventListener('change', handler);
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Check if the browser is in a low-power mode or has reduced capabilities
117
+ */
118
+ export function isLowPowerMode(): boolean {
119
+ // Check for battery API (if available)
120
+ if (typeof navigator !== 'undefined' && 'getBattery' in navigator) {
121
+ // Battery API is async, so this is a simplified check
122
+ // Real implementation would need to be async
123
+ return false;
124
+ }
125
+
126
+ // Fallback: check for hardware concurrency
127
+ if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
128
+ return navigator.hardwareConcurrency <= 2;
129
+ }
130
+
131
+ return false;
132
+ }
133
+
134
+ /**
135
+ * Get recommended FPS based on device capabilities
136
+ */
137
+ export function getRecommendedFPS(): number {
138
+ if (prefersReducedMotion()) {
139
+ return 0; // No animation
140
+ }
141
+
142
+ if (isLowPowerMode()) {
143
+ return 15; // Low power mode
144
+ }
145
+
146
+ // Default for capable devices
147
+ return 30;
148
+ }
149
+
150
+ /**
151
+ * Performance monitoring for debugging
152
+ */
153
+ export interface PerformanceMetrics {
154
+ fps: number;
155
+ frameTime: number;
156
+ droppedFrames: number;
157
+ }
158
+
159
+ /**
160
+ * Create a simple FPS counter
161
+ */
162
+ export function createFPSCounter(): {
163
+ tick: () => void;
164
+ getFPS: () => number;
165
+ getMetrics: () => PerformanceMetrics;
166
+ reset: () => void;
167
+ } {
168
+ const frameTimes: number[] = [];
169
+ const maxSamples = 60;
170
+ let droppedFrames = 0;
171
+ let lastFrameTime = performance.now();
172
+
173
+ function tick(): void {
174
+ const now = performance.now();
175
+ const delta = now - lastFrameTime;
176
+ lastFrameTime = now;
177
+
178
+ frameTimes.push(now);
179
+
180
+ if (frameTimes.length > maxSamples) {
181
+ frameTimes.shift();
182
+ }
183
+
184
+ // Count dropped frames (assuming 60fps target, frames taking >20ms are "dropped")
185
+ if (delta > 20) {
186
+ droppedFrames += Math.floor(delta / 16.67) - 1;
187
+ }
188
+ }
189
+
190
+ function getFPS(): number {
191
+ if (frameTimes.length < 2) return 0;
192
+
193
+ const oldest = frameTimes[0];
194
+ const newest = frameTimes[frameTimes.length - 1];
195
+ const elapsed = newest - oldest;
196
+
197
+ if (elapsed === 0) return 0;
198
+
199
+ return ((frameTimes.length - 1) / elapsed) * 1000;
200
+ }
201
+
202
+ function getMetrics(): PerformanceMetrics {
203
+ const fps = getFPS();
204
+ const frameTime = fps > 0 ? 1000 / fps : 0;
205
+
206
+ return {
207
+ fps: Math.round(fps * 10) / 10,
208
+ frameTime: Math.round(frameTime * 100) / 100,
209
+ droppedFrames,
210
+ };
211
+ }
212
+
213
+ function reset(): void {
214
+ frameTimes.length = 0;
215
+ droppedFrames = 0;
216
+ lastFrameTime = performance.now();
217
+ }
218
+
219
+ return { tick, getFPS, getMetrics, reset };
220
+ }
221
+
222
+ /**
223
+ * Request idle callback with fallback
224
+ */
225
+ export function requestIdleCallback(
226
+ callback: () => void,
227
+ options?: { timeout?: number }
228
+ ): number {
229
+ // Use globalThis for cross-environment compatibility
230
+ const global = globalThis as typeof globalThis & {
231
+ requestIdleCallback?: typeof window.requestIdleCallback;
232
+ setTimeout: typeof setTimeout;
233
+ };
234
+
235
+ if (typeof global.requestIdleCallback === 'function') {
236
+ return global.requestIdleCallback(callback, options);
237
+ }
238
+
239
+ // Fallback using setTimeout
240
+ return global.setTimeout(callback, options?.timeout ?? 1) as unknown as number;
241
+ }
242
+
243
+ /**
244
+ * Cancel idle callback with fallback
245
+ */
246
+ export function cancelIdleCallback(id: number): void {
247
+ // Use globalThis for cross-environment compatibility
248
+ const global = globalThis as typeof globalThis & {
249
+ cancelIdleCallback?: typeof window.cancelIdleCallback;
250
+ clearTimeout: typeof clearTimeout;
251
+ };
252
+
253
+ if (typeof global.cancelIdleCallback === 'function') {
254
+ global.cancelIdleCallback(id);
255
+ } else {
256
+ global.clearTimeout(id);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Check if we're running in a browser environment
262
+ */
263
+ export function isBrowser(): boolean {
264
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
265
+ }
266
+
267
+ /**
268
+ * Check if Canvas is supported
269
+ */
270
+ export function isCanvasSupported(): boolean {
271
+ if (!isBrowser()) return false;
272
+
273
+ const canvas = document.createElement('canvas');
274
+ return !!(canvas.getContext && canvas.getContext('2d'));
275
+ }
276
+
277
+ /**
278
+ * Check if OffscreenCanvas is supported
279
+ */
280
+ export function isOffscreenCanvasSupported(): boolean {
281
+ return typeof OffscreenCanvas !== 'undefined';
282
+ }