@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,470 @@
1
+ /**
2
+ * Gossamer Core Renderer
3
+ *
4
+ * Canvas-based ASCII rendering engine. Converts image data to ASCII characters
5
+ * by mapping brightness values to a character set.
6
+ */
7
+
8
+ // Define locally to avoid circular dependency with index.ts
9
+ const DEFAULT_CHARACTERS = ' .:-=+*#%@';
10
+
11
+ function calculateBrightness(r: number, g: number, b: number): number {
12
+ return 0.21 * r + 0.72 * g + 0.07 * b;
13
+ }
14
+
15
+ /**
16
+ * Configuration for the Gossamer renderer
17
+ */
18
+ export interface RenderConfig {
19
+ /** Canvas element to render to */
20
+ canvas: HTMLCanvasElement;
21
+ /** Character set ordered from light to dark */
22
+ characters: string;
23
+ /** Width of each character cell in pixels */
24
+ cellWidth: number;
25
+ /** Height of each character cell in pixels */
26
+ cellHeight: number;
27
+ /** Color for rendering characters */
28
+ color: string;
29
+ /** Background color (empty string for transparent) */
30
+ backgroundColor: string;
31
+ /** Font family */
32
+ fontFamily: string;
33
+ /** Custom brightness calculation function */
34
+ brightnessFunction: (r: number, g: number, b: number) => number;
35
+ }
36
+
37
+ /**
38
+ * Default render configuration
39
+ */
40
+ const DEFAULT_RENDER_CONFIG: Omit<RenderConfig, 'canvas'> = {
41
+ characters: DEFAULT_CHARACTERS,
42
+ cellWidth: 8,
43
+ cellHeight: 12,
44
+ color: '#ffffff',
45
+ backgroundColor: '',
46
+ fontFamily: 'monospace',
47
+ brightnessFunction: calculateBrightness,
48
+ };
49
+
50
+ /**
51
+ * Core ASCII renderer class
52
+ *
53
+ * Handles all canvas rendering operations for ASCII effects.
54
+ * Supports both static rendering and animation loops.
55
+ */
56
+ export class GossamerRenderer {
57
+ private ctx: CanvasRenderingContext2D;
58
+ private config: RenderConfig;
59
+ private animationId: number | null = null;
60
+ private lastFrameTime: number = 0;
61
+ private isRunning: boolean = false;
62
+
63
+ // Performance: Character texture atlas
64
+ private charAtlas: OffscreenCanvas | HTMLCanvasElement | null = null;
65
+ private atlasCharacters: string = '';
66
+
67
+ constructor(canvas: HTMLCanvasElement, config: Partial<Omit<RenderConfig, 'canvas'>> = {}) {
68
+ const context = canvas.getContext('2d');
69
+ if (!context) {
70
+ throw new Error('Failed to get 2D rendering context');
71
+ }
72
+
73
+ this.ctx = context;
74
+ this.config = {
75
+ canvas,
76
+ ...DEFAULT_RENDER_CONFIG,
77
+ ...config,
78
+ };
79
+
80
+ this.setupCanvas();
81
+ this.buildCharacterAtlas();
82
+ }
83
+
84
+ /**
85
+ * Build character texture atlas for fast rendering
86
+ * Pre-renders all characters to an offscreen canvas, then uses drawImage
87
+ * instead of fillText for 5-10x faster rendering
88
+ */
89
+ private buildCharacterAtlas(): void {
90
+ const { characters, cellWidth, cellHeight, color, fontFamily } = this.config;
91
+
92
+ // Skip if atlas already built with same characters
93
+ if (this.atlasCharacters === characters && this.charAtlas) {
94
+ return;
95
+ }
96
+
97
+ // Create offscreen canvas (use OffscreenCanvas if available for better perf)
98
+ const atlasWidth = characters.length * cellWidth;
99
+ const atlasHeight = cellHeight;
100
+
101
+ if (typeof OffscreenCanvas !== 'undefined') {
102
+ this.charAtlas = new OffscreenCanvas(atlasWidth, atlasHeight);
103
+ } else {
104
+ this.charAtlas = document.createElement('canvas');
105
+ this.charAtlas.width = atlasWidth;
106
+ this.charAtlas.height = atlasHeight;
107
+ }
108
+
109
+ const ctx = this.charAtlas.getContext('2d');
110
+ if (!ctx) {
111
+ this.charAtlas = null;
112
+ return;
113
+ }
114
+
115
+
116
+ // Clear with transparent background
117
+ ctx.clearRect(0, 0, atlasWidth, atlasHeight);
118
+
119
+ // Render each character
120
+ ctx.fillStyle = color;
121
+ ctx.font = `${cellHeight}px ${fontFamily}`;
122
+ ctx.textBaseline = 'top';
123
+
124
+ for (let i = 0; i < characters.length; i++) {
125
+ const char = characters[i];
126
+ if (char !== ' ') {
127
+ ctx.fillText(char, i * cellWidth, 0);
128
+ }
129
+ }
130
+
131
+ this.atlasCharacters = characters;
132
+ }
133
+
134
+ /**
135
+ * Set up the canvas with optimal rendering settings
136
+ */
137
+ private setupCanvas(): void {
138
+ const { fontFamily, cellHeight } = this.config;
139
+
140
+ // Set font for consistent character sizing
141
+ this.ctx.font = `${cellHeight}px ${fontFamily}`;
142
+ this.ctx.textBaseline = 'top';
143
+
144
+ // Enable image smoothing for better quality
145
+ this.ctx.imageSmoothingEnabled = true;
146
+ this.ctx.imageSmoothingQuality = 'high';
147
+ }
148
+
149
+ /**
150
+ * Update the renderer configuration
151
+ */
152
+ updateConfig(config: Partial<Omit<RenderConfig, 'canvas'>>): void {
153
+ const needsAtlasRebuild =
154
+ config.characters !== undefined ||
155
+ config.color !== undefined ||
156
+ config.cellWidth !== undefined ||
157
+ config.cellHeight !== undefined ||
158
+ config.fontFamily !== undefined;
159
+
160
+ this.config = { ...this.config, ...config };
161
+ this.setupCanvas();
162
+
163
+ if (needsAtlasRebuild) {
164
+ this.atlasCharacters = ''; // Force rebuild
165
+ this.buildCharacterAtlas();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Resize the canvas to match new dimensions
171
+ */
172
+ resize(width: number, height: number): void {
173
+ const { canvas } = this.config;
174
+ canvas.width = width;
175
+ canvas.height = height;
176
+ this.setupCanvas();
177
+ }
178
+
179
+ /**
180
+ * Get the current canvas dimensions
181
+ */
182
+ getDimensions(): { width: number; height: number } {
183
+ return {
184
+ width: this.config.canvas.width,
185
+ height: this.config.canvas.height,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Calculate the number of cells that fit in the canvas
191
+ */
192
+ getCellCount(): { cols: number; rows: number } {
193
+ const { width, height } = this.getDimensions();
194
+ const { cellWidth, cellHeight } = this.config;
195
+
196
+ return {
197
+ cols: Math.ceil(width / cellWidth),
198
+ rows: Math.ceil(height / cellHeight),
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Clear the canvas
204
+ */
205
+ clear(): void {
206
+ const { canvas, backgroundColor } = this.config;
207
+
208
+ if (backgroundColor) {
209
+ this.ctx.fillStyle = backgroundColor;
210
+ this.ctx.fillRect(0, 0, canvas.width, canvas.height);
211
+ } else {
212
+ this.ctx.clearRect(0, 0, canvas.width, canvas.height);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Render a single frame from image data
218
+ */
219
+ renderFrame(imageData: ImageData): void {
220
+ const { canvas, characters, cellWidth, cellHeight, color, brightnessFunction } = this.config;
221
+ const { width, data } = imageData;
222
+
223
+ this.clear();
224
+ this.ctx.fillStyle = color;
225
+
226
+ for (let y = 0; y < canvas.height; y += cellHeight) {
227
+ for (let x = 0; x < canvas.width; x += cellWidth) {
228
+ const brightness = this.getCellBrightness(data, x, y, width, cellWidth, cellHeight, brightnessFunction);
229
+ const charIndex = Math.floor((brightness / 255) * (characters.length - 1));
230
+ const char = characters[Math.min(charIndex, characters.length - 1)];
231
+
232
+ if (char !== ' ') {
233
+ this.ctx.fillText(char, x, y);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Render ASCII from a brightness grid (for pattern-based rendering)
241
+ */
242
+ renderFromBrightnessGrid(grid: number[][]): void {
243
+ const { characters, cellWidth, cellHeight, color } = this.config;
244
+
245
+ this.clear();
246
+ this.ctx.fillStyle = color;
247
+
248
+ for (let row = 0; row < grid.length; row++) {
249
+ for (let col = 0; col < grid[row].length; col++) {
250
+ const brightness = grid[row][col];
251
+ const charIndex = Math.floor((brightness / 255) * (characters.length - 1));
252
+ const char = characters[Math.min(charIndex, characters.length - 1)];
253
+
254
+ if (char !== ' ') {
255
+ this.ctx.fillText(char, col * cellWidth, row * cellHeight);
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Render ASCII with per-cell colors (for colored image rendering)
263
+ */
264
+ renderWithColors(data: Array<{ char: string; color: string; x: number; y: number }>): void {
265
+ this.clear();
266
+
267
+ for (const { char, color, x, y } of data) {
268
+ if (char !== ' ') {
269
+ this.ctx.fillStyle = color;
270
+ this.ctx.fillText(char, x, y);
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * PERFORMANCE: Render from BrightnessBuffer using texture atlas
277
+ *
278
+ * Uses pre-rendered character sprites instead of fillText calls.
279
+ * 5-10x faster than renderFromBrightnessGrid for large canvases.
280
+ *
281
+ * @param buffer - BrightnessBuffer from fillBrightnessBuffer
282
+ */
283
+ renderFromBuffer(buffer: { data: Uint8Array; cols: number; rows: number }): void {
284
+ const { characters, cellWidth, cellHeight } = this.config;
285
+
286
+ this.clear();
287
+
288
+ // Fall back to fillText if atlas not available
289
+ if (!this.charAtlas) {
290
+ this.ctx.fillStyle = this.config.color;
291
+ const charLen = characters.length - 1;
292
+ let idx = 0;
293
+ for (let row = 0; row < buffer.rows; row++) {
294
+ for (let col = 0; col < buffer.cols; col++) {
295
+ const brightness = buffer.data[idx++];
296
+ const charIndex = (brightness / 255 * charLen) | 0;
297
+ const char = characters[Math.min(charIndex, charLen)];
298
+ if (char !== ' ') {
299
+ this.ctx.fillText(char, col * cellWidth, row * cellHeight);
300
+ }
301
+ }
302
+ }
303
+ return;
304
+ }
305
+
306
+ // Use atlas for fast rendering via drawImage
307
+ const charLen = characters.length - 1;
308
+ let idx = 0;
309
+
310
+ for (let row = 0; row < buffer.rows; row++) {
311
+ const y = row * cellHeight;
312
+ for (let col = 0; col < buffer.cols; col++) {
313
+ const brightness = buffer.data[idx++];
314
+ const charIndex = (brightness / 255 * charLen) | 0;
315
+
316
+ // Skip space characters (index 0 in most charsets)
317
+ if (charIndex === 0 && characters[0] === ' ') {
318
+ continue;
319
+ }
320
+
321
+ // Draw from atlas: source is the character's position in the atlas
322
+ this.ctx.drawImage(
323
+ this.charAtlas,
324
+ charIndex * cellWidth, 0, cellWidth, cellHeight, // source
325
+ col * cellWidth, y, cellWidth, cellHeight // destination
326
+ );
327
+ }
328
+ }
329
+ }
330
+
331
+ /**
332
+ * PERFORMANCE: Render brightness grid using atlas (legacy grid format)
333
+ *
334
+ * @param grid - 2D array of brightness values
335
+ */
336
+ renderGridFast(grid: number[][]): void {
337
+ const { characters, cellWidth, cellHeight } = this.config;
338
+
339
+ this.clear();
340
+
341
+ if (!this.charAtlas) {
342
+ // Fallback to standard method
343
+ this.renderFromBrightnessGrid(grid);
344
+ return;
345
+ }
346
+
347
+ const charLen = characters.length - 1;
348
+
349
+ for (let row = 0; row < grid.length; row++) {
350
+ const y = row * cellHeight;
351
+ const rowData = grid[row];
352
+ for (let col = 0; col < rowData.length; col++) {
353
+ const brightness = rowData[col];
354
+ const charIndex = (brightness / 255 * charLen) | 0;
355
+
356
+ if (charIndex === 0 && characters[0] === ' ') {
357
+ continue;
358
+ }
359
+
360
+ this.ctx.drawImage(
361
+ this.charAtlas,
362
+ charIndex * cellWidth, 0, cellWidth, cellHeight,
363
+ col * cellWidth, y, cellWidth, cellHeight
364
+ );
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Calculate average brightness for a cell region
371
+ */
372
+ private getCellBrightness(
373
+ data: Uint8ClampedArray,
374
+ startX: number,
375
+ startY: number,
376
+ imageWidth: number,
377
+ cellWidth: number,
378
+ cellHeight: number,
379
+ brightnessFunction: (r: number, g: number, b: number) => number
380
+ ): number {
381
+ let total = 0;
382
+ let count = 0;
383
+
384
+ for (let cy = 0; cy < cellHeight; cy++) {
385
+ for (let cx = 0; cx < cellWidth; cx++) {
386
+ const px = ((startY + cy) * imageWidth + (startX + cx)) * 4;
387
+ if (px >= 0 && px + 2 < data.length) {
388
+ total += brightnessFunction(data[px], data[px + 1], data[px + 2]);
389
+ count++;
390
+ }
391
+ }
392
+ }
393
+
394
+ return count > 0 ? total / count : 0;
395
+ }
396
+
397
+ /**
398
+ * Start an animation loop with FPS limiting
399
+ */
400
+ startAnimation(
401
+ updateFn: (time: number, deltaTime: number) => ImageData | number[][],
402
+ fps: number = 30
403
+ ): void {
404
+ if (this.isRunning) {
405
+ this.stopAnimation();
406
+ }
407
+
408
+ this.isRunning = true;
409
+ const frameInterval = 1000 / fps;
410
+ this.lastFrameTime = performance.now();
411
+
412
+ const animate = (currentTime: number): void => {
413
+ if (!this.isRunning) return;
414
+
415
+ const deltaTime = currentTime - this.lastFrameTime;
416
+
417
+ if (deltaTime >= frameInterval) {
418
+ const result = updateFn(currentTime, deltaTime);
419
+
420
+ if (result instanceof ImageData) {
421
+ this.renderFrame(result);
422
+ } else {
423
+ this.renderFromBrightnessGrid(result);
424
+ }
425
+
426
+ this.lastFrameTime = currentTime - (deltaTime % frameInterval);
427
+ }
428
+
429
+ this.animationId = requestAnimationFrame(animate);
430
+ };
431
+
432
+ this.animationId = requestAnimationFrame(animate);
433
+ }
434
+
435
+ /**
436
+ * Stop the animation loop
437
+ */
438
+ stopAnimation(): void {
439
+ this.isRunning = false;
440
+ if (this.animationId !== null) {
441
+ cancelAnimationFrame(this.animationId);
442
+ this.animationId = null;
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Check if animation is currently running
448
+ */
449
+ isAnimating(): boolean {
450
+ return this.isRunning;
451
+ }
452
+
453
+ /**
454
+ * Pause animation (can be resumed)
455
+ */
456
+ pause(): void {
457
+ this.isRunning = false;
458
+ if (this.animationId !== null) {
459
+ cancelAnimationFrame(this.animationId);
460
+ this.animationId = null;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Clean up and destroy the renderer
466
+ */
467
+ destroy(): void {
468
+ this.stopAnimation();
469
+ }
470
+ }