@autumnsgrove/gossamer 0.1.0 → 0.2.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/dist/animation.js +165 -0
  3. package/dist/animation.test.js +204 -0
  4. package/dist/characters.d.ts.map +1 -1
  5. package/dist/characters.js +176 -0
  6. package/dist/characters.test.js +115 -0
  7. package/dist/colors.d.ts +312 -0
  8. package/dist/colors.d.ts.map +1 -0
  9. package/dist/colors.js +199 -0
  10. package/dist/index.d.ts +5 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +79 -1308
  13. package/dist/index.test.js +92 -0
  14. package/dist/patterns.d.ts +119 -2
  15. package/dist/patterns.d.ts.map +1 -1
  16. package/dist/patterns.js +539 -0
  17. package/dist/patterns.test.js +223 -0
  18. package/dist/renderer.d.ts +27 -0
  19. package/dist/renderer.d.ts.map +1 -1
  20. package/dist/renderer.js +362 -0
  21. package/dist/svelte/GossamerBorder.svelte.d.ts +56 -1
  22. package/dist/svelte/GossamerBorder.svelte.d.ts.map +1 -0
  23. package/{src → dist}/svelte/GossamerClouds.svelte +6 -6
  24. package/dist/svelte/GossamerClouds.svelte.d.ts +31 -1
  25. package/dist/svelte/GossamerClouds.svelte.d.ts.map +1 -0
  26. package/dist/svelte/GossamerImage.svelte.d.ts +28 -1
  27. package/dist/svelte/GossamerImage.svelte.d.ts.map +1 -0
  28. package/dist/svelte/GossamerOverlay.svelte.d.ts +32 -1
  29. package/dist/svelte/GossamerOverlay.svelte.d.ts.map +1 -0
  30. package/dist/svelte/GossamerText.svelte.d.ts +29 -1
  31. package/dist/svelte/GossamerText.svelte.d.ts.map +1 -0
  32. package/dist/svelte/index.js +31 -3649
  33. package/dist/svelte/presets.d.ts +4 -2
  34. package/dist/svelte/presets.js +161 -0
  35. package/dist/utils/canvas.js +139 -0
  36. package/dist/utils/image.js +195 -0
  37. package/dist/utils/performance.js +205 -0
  38. package/package.json +20 -15
  39. package/dist/index.js.map +0 -1
  40. package/dist/style.css +0 -124
  41. package/dist/svelte/index.js.map +0 -1
  42. package/src/animation.test.ts +0 -254
  43. package/src/animation.ts +0 -243
  44. package/src/characters.test.ts +0 -148
  45. package/src/characters.ts +0 -164
  46. package/src/index.test.ts +0 -115
  47. package/src/index.ts +0 -203
  48. package/src/patterns.test.ts +0 -273
  49. package/src/patterns.ts +0 -316
  50. package/src/renderer.ts +0 -309
  51. package/src/svelte/index.ts +0 -75
  52. package/src/svelte/presets.ts +0 -174
  53. package/src/utils/canvas.ts +0 -210
  54. package/src/utils/image.ts +0 -275
  55. package/src/utils/performance.ts +0 -282
  56. /package/{src → dist}/svelte/GossamerBorder.svelte +0 -0
  57. /package/{src → dist}/svelte/GossamerImage.svelte +0 -0
  58. /package/{src → dist}/svelte/GossamerOverlay.svelte +0 -0
  59. /package/{src → dist}/svelte/GossamerText.svelte +0 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Tests for pattern generators
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { perlinNoise2D, fbmNoise, wavePattern, ripplePattern, staticNoise, seededNoise2D, generateBrightnessGrid, gridToImageData, DEFAULT_PATTERN_CONFIG, } from './patterns';
6
+ describe('perlinNoise2D', () => {
7
+ it('should return value between -1 and 1', () => {
8
+ for (let i = 0; i < 100; i++) {
9
+ const x = Math.random() * 100;
10
+ const y = Math.random() * 100;
11
+ const value = perlinNoise2D(x, y);
12
+ expect(value).toBeGreaterThanOrEqual(-1);
13
+ expect(value).toBeLessThanOrEqual(1);
14
+ }
15
+ });
16
+ it('should be deterministic (same input = same output)', () => {
17
+ const val1 = perlinNoise2D(5.5, 3.2);
18
+ const val2 = perlinNoise2D(5.5, 3.2);
19
+ expect(val1).toBe(val2);
20
+ });
21
+ it('should vary with position', () => {
22
+ // Use non-integer coordinates (Perlin returns 0 at integer coords)
23
+ const val1 = perlinNoise2D(0.5, 0.5);
24
+ const val2 = perlinNoise2D(10.5, 10.5);
25
+ expect(val1).not.toBe(val2);
26
+ });
27
+ it('should produce smooth transitions', () => {
28
+ const val1 = perlinNoise2D(1.0, 1.0);
29
+ const val2 = perlinNoise2D(1.01, 1.01);
30
+ const diff = Math.abs(val1 - val2);
31
+ expect(diff).toBeLessThan(0.1);
32
+ });
33
+ });
34
+ describe('fbmNoise', () => {
35
+ it('should return value between -1 and 1', () => {
36
+ for (let i = 0; i < 50; i++) {
37
+ const x = Math.random() * 100;
38
+ const y = Math.random() * 100;
39
+ const value = fbmNoise(x, y);
40
+ expect(value).toBeGreaterThanOrEqual(-1);
41
+ expect(value).toBeLessThanOrEqual(1);
42
+ }
43
+ });
44
+ it('should be deterministic', () => {
45
+ const val1 = fbmNoise(3.3, 7.7);
46
+ const val2 = fbmNoise(3.3, 7.7);
47
+ expect(val1).toBe(val2);
48
+ });
49
+ it('should accept octaves parameter', () => {
50
+ // Use non-integer coordinates (noise returns 0 at integer coords)
51
+ const val1 = fbmNoise(5.5, 5.5, 2);
52
+ const val2 = fbmNoise(5.5, 5.5, 8);
53
+ // Different octaves should produce different results
54
+ expect(val1).not.toBe(val2);
55
+ });
56
+ it('should accept persistence parameter', () => {
57
+ // Use non-integer coordinates (noise returns 0 at integer coords)
58
+ const val1 = fbmNoise(5.5, 5.5, 4, 0.3);
59
+ const val2 = fbmNoise(5.5, 5.5, 4, 0.7);
60
+ expect(val1).not.toBe(val2);
61
+ });
62
+ });
63
+ describe('wavePattern', () => {
64
+ it('should return value between -1 and 1', () => {
65
+ for (let t = 0; t < 10; t++) {
66
+ const value = wavePattern(50, 50, t);
67
+ expect(value).toBeGreaterThanOrEqual(-1);
68
+ expect(value).toBeLessThanOrEqual(1);
69
+ }
70
+ });
71
+ it('should vary with time', () => {
72
+ const val1 = wavePattern(10, 10, 0);
73
+ const val2 = wavePattern(10, 10, 5);
74
+ expect(val1).not.toBe(val2);
75
+ });
76
+ it('should use custom config', () => {
77
+ const config = { frequency: 0.1, amplitude: 0.5, speed: 1.0 };
78
+ const value = wavePattern(10, 10, 1, config);
79
+ expect(Math.abs(value)).toBeLessThanOrEqual(0.5);
80
+ });
81
+ });
82
+ describe('ripplePattern', () => {
83
+ it('should return value between -1 and 1', () => {
84
+ for (let i = 0; i < 50; i++) {
85
+ const value = ripplePattern(Math.random() * 100, Math.random() * 100, 50, 50, Math.random() * 10);
86
+ expect(value).toBeGreaterThanOrEqual(-1);
87
+ expect(value).toBeLessThanOrEqual(1);
88
+ }
89
+ });
90
+ it('should vary with distance from center', () => {
91
+ const centerVal = ripplePattern(50, 50, 50, 50, 0);
92
+ const edgeVal = ripplePattern(100, 50, 50, 50, 0);
93
+ expect(centerVal).not.toBe(edgeVal);
94
+ });
95
+ it('should animate over time', () => {
96
+ const val1 = ripplePattern(60, 60, 50, 50, 0);
97
+ const val2 = ripplePattern(60, 60, 50, 50, 1);
98
+ expect(val1).not.toBe(val2);
99
+ });
100
+ });
101
+ describe('staticNoise', () => {
102
+ it('should return value between 0 and 1', () => {
103
+ for (let i = 0; i < 100; i++) {
104
+ const value = staticNoise();
105
+ expect(value).toBeGreaterThanOrEqual(0);
106
+ expect(value).toBeLessThanOrEqual(1);
107
+ }
108
+ });
109
+ it('should be deterministic with seed', () => {
110
+ const val1 = staticNoise(12345);
111
+ const val2 = staticNoise(12345);
112
+ expect(val1).toBe(val2);
113
+ });
114
+ it('should vary with different seeds', () => {
115
+ const val1 = staticNoise(1);
116
+ const val2 = staticNoise(2);
117
+ expect(val1).not.toBe(val2);
118
+ });
119
+ });
120
+ describe('seededNoise2D', () => {
121
+ it('should return value between 0 and 1', () => {
122
+ for (let i = 0; i < 100; i++) {
123
+ const value = seededNoise2D(Math.random() * 100, Math.random() * 100);
124
+ expect(value).toBeGreaterThanOrEqual(0);
125
+ expect(value).toBeLessThanOrEqual(1);
126
+ }
127
+ });
128
+ it('should be deterministic', () => {
129
+ const val1 = seededNoise2D(5, 10, 42);
130
+ const val2 = seededNoise2D(5, 10, 42);
131
+ expect(val1).toBe(val2);
132
+ });
133
+ it('should vary with coordinates', () => {
134
+ const val1 = seededNoise2D(0, 0);
135
+ const val2 = seededNoise2D(10, 10);
136
+ expect(val1).not.toBe(val2);
137
+ });
138
+ it('should vary with seed', () => {
139
+ const val1 = seededNoise2D(5, 5, 0);
140
+ const val2 = seededNoise2D(5, 5, 100);
141
+ expect(val1).not.toBe(val2);
142
+ });
143
+ });
144
+ describe('DEFAULT_PATTERN_CONFIG', () => {
145
+ it('should have expected default values', () => {
146
+ expect(DEFAULT_PATTERN_CONFIG.frequency).toBe(0.05);
147
+ expect(DEFAULT_PATTERN_CONFIG.amplitude).toBe(1.0);
148
+ expect(DEFAULT_PATTERN_CONFIG.speed).toBe(0.5);
149
+ });
150
+ });
151
+ describe('generateBrightnessGrid', () => {
152
+ it('should generate grid with correct dimensions', () => {
153
+ const grid = generateBrightnessGrid(10, 8, 'perlin');
154
+ expect(grid.length).toBe(8); // rows
155
+ expect(grid[0].length).toBe(10); // cols
156
+ });
157
+ it('should generate brightness values between 0 and 255', () => {
158
+ const grid = generateBrightnessGrid(20, 15, 'perlin');
159
+ for (const row of grid) {
160
+ for (const value of row) {
161
+ expect(value).toBeGreaterThanOrEqual(0);
162
+ expect(value).toBeLessThanOrEqual(255);
163
+ expect(Number.isInteger(value)).toBe(true);
164
+ }
165
+ }
166
+ });
167
+ it('should support all pattern types', () => {
168
+ const patterns = [
169
+ 'perlin',
170
+ 'waves',
171
+ 'static',
172
+ 'ripple',
173
+ 'fbm',
174
+ ];
175
+ for (const pattern of patterns) {
176
+ const grid = generateBrightnessGrid(5, 5, pattern);
177
+ expect(grid.length).toBe(5);
178
+ expect(grid[0].length).toBe(5);
179
+ }
180
+ });
181
+ it('should use time parameter for animation', () => {
182
+ const grid1 = generateBrightnessGrid(10, 10, 'perlin', 0);
183
+ const grid2 = generateBrightnessGrid(10, 10, 'perlin', 100);
184
+ // At least some values should differ
185
+ let hasDifference = false;
186
+ for (let r = 0; r < 10 && !hasDifference; r++) {
187
+ for (let c = 0; c < 10 && !hasDifference; c++) {
188
+ if (grid1[r][c] !== grid2[r][c]) {
189
+ hasDifference = true;
190
+ }
191
+ }
192
+ }
193
+ expect(hasDifference).toBe(true);
194
+ });
195
+ });
196
+ // Note: gridToImageData tests require browser environment with canvas support
197
+ // These tests are skipped in Node.js/jsdom as ImageData is not available
198
+ describe.skip('gridToImageData (browser only)', () => {
199
+ it('should create ImageData with correct dimensions', () => {
200
+ const grid = [
201
+ [100, 150],
202
+ [200, 50],
203
+ ];
204
+ const imageData = gridToImageData(grid, 8, 12);
205
+ expect(imageData.width).toBe(16); // 2 cols * 8
206
+ expect(imageData.height).toBe(24); // 2 rows * 12
207
+ });
208
+ it('should fill cells with brightness values', () => {
209
+ const grid = [[128]]; // Single cell
210
+ const imageData = gridToImageData(grid, 2, 2);
211
+ // Check first pixel (should be brightness 128)
212
+ expect(imageData.data[0]).toBe(128); // R
213
+ expect(imageData.data[1]).toBe(128); // G
214
+ expect(imageData.data[2]).toBe(128); // B
215
+ expect(imageData.data[3]).toBe(255); // A (full opacity)
216
+ });
217
+ it('should handle empty grid', () => {
218
+ const grid = [];
219
+ const imageData = gridToImageData(grid, 8, 12);
220
+ expect(imageData.width).toBe(0);
221
+ expect(imageData.height).toBe(0);
222
+ });
223
+ });
@@ -37,7 +37,15 @@ export declare class GossamerRenderer {
37
37
  private animationId;
38
38
  private lastFrameTime;
39
39
  private isRunning;
40
+ private charAtlas;
41
+ private atlasCharacters;
40
42
  constructor(canvas: HTMLCanvasElement, config?: Partial<Omit<RenderConfig, 'canvas'>>);
43
+ /**
44
+ * Build character texture atlas for fast rendering
45
+ * Pre-renders all characters to an offscreen canvas, then uses drawImage
46
+ * instead of fillText for 5-10x faster rendering
47
+ */
48
+ private buildCharacterAtlas;
41
49
  /**
42
50
  * Set up the canvas with optimal rendering settings
43
51
  */
@@ -85,6 +93,25 @@ export declare class GossamerRenderer {
85
93
  x: number;
86
94
  y: number;
87
95
  }>): void;
96
+ /**
97
+ * PERFORMANCE: Render from BrightnessBuffer using texture atlas
98
+ *
99
+ * Uses pre-rendered character sprites instead of fillText calls.
100
+ * 5-10x faster than renderFromBrightnessGrid for large canvases.
101
+ *
102
+ * @param buffer - BrightnessBuffer from fillBrightnessBuffer
103
+ */
104
+ renderFromBuffer(buffer: {
105
+ data: Uint8Array;
106
+ cols: number;
107
+ rows: number;
108
+ }): void;
109
+ /**
110
+ * PERFORMANCE: Render brightness grid using atlas (legacy grid format)
111
+ *
112
+ * @param grid - 2D array of brightness values
113
+ */
114
+ renderGridFast(grid: number[][]): void;
88
115
  /**
89
116
  * Calculate average brightness for a cell region
90
117
  */
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,kBAAkB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACjE;AAeD;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,SAAS,CAAkB;gBAEvB,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAM;IAgBzF;;OAEG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI;IAKjE;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAO3C;;OAEG;IACH,aAAa,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAOlD;;OAEG;IACH,YAAY,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAU9C;;OAEG;IACH,KAAK,IAAI,IAAI;IAWb;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAoBvC;;OAEG;IACH,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI;IAmBhD;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI;IAW1F;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;IACH,cAAc,CACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,SAAS,GAAG,MAAM,EAAE,EAAE,EACrE,GAAG,GAAE,MAAW,GACf,IAAI;IAgCP;;OAEG;IACH,aAAa,IAAI,IAAI;IAQrB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,kBAAkB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACjE;AAeD;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,eAAe,CAAc;gBAEzB,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAM;IAiBzF;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IA6C3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI;IAiBjE;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAO3C;;OAEG;IACH,aAAa,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAOlD;;OAEG;IACH,YAAY,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAU9C;;OAEG;IACH,KAAK,IAAI,IAAI;IAWb;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAoBvC;;OAEG;IACH,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI;IAmBhD;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI;IAW1F;;;;;;;OAOG;IACH,gBAAgB,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAgDhF;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI;IAiCtC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;IACH,cAAc,CACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,SAAS,GAAG,MAAM,EAAE,EAAE,EACrE,GAAG,GAAE,MAAW,GACf,IAAI;IAgCP;;OAEG;IACH,aAAa,IAAI,IAAI;IAQrB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,362 @@
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
+ // Define locally to avoid circular dependency with index.ts
8
+ const DEFAULT_CHARACTERS = ' .:-=+*#%@';
9
+ function calculateBrightness(r, g, b) {
10
+ return 0.21 * r + 0.72 * g + 0.07 * b;
11
+ }
12
+ /**
13
+ * Default render configuration
14
+ */
15
+ const DEFAULT_RENDER_CONFIG = {
16
+ characters: DEFAULT_CHARACTERS,
17
+ cellWidth: 8,
18
+ cellHeight: 12,
19
+ color: '#ffffff',
20
+ backgroundColor: '',
21
+ fontFamily: 'monospace',
22
+ brightnessFunction: calculateBrightness,
23
+ };
24
+ /**
25
+ * Core ASCII renderer class
26
+ *
27
+ * Handles all canvas rendering operations for ASCII effects.
28
+ * Supports both static rendering and animation loops.
29
+ */
30
+ export class GossamerRenderer {
31
+ constructor(canvas, config = {}) {
32
+ this.animationId = null;
33
+ this.lastFrameTime = 0;
34
+ this.isRunning = false;
35
+ // Performance: Character texture atlas
36
+ this.charAtlas = null;
37
+ this.atlasCharacters = '';
38
+ const context = canvas.getContext('2d');
39
+ if (!context) {
40
+ throw new Error('Failed to get 2D rendering context');
41
+ }
42
+ this.ctx = context;
43
+ this.config = {
44
+ canvas,
45
+ ...DEFAULT_RENDER_CONFIG,
46
+ ...config,
47
+ };
48
+ this.setupCanvas();
49
+ this.buildCharacterAtlas();
50
+ }
51
+ /**
52
+ * Build character texture atlas for fast rendering
53
+ * Pre-renders all characters to an offscreen canvas, then uses drawImage
54
+ * instead of fillText for 5-10x faster rendering
55
+ */
56
+ buildCharacterAtlas() {
57
+ const { characters, cellWidth, cellHeight, color, fontFamily } = this.config;
58
+ // Skip if atlas already built with same characters
59
+ if (this.atlasCharacters === characters && this.charAtlas) {
60
+ return;
61
+ }
62
+ // Create offscreen canvas (use OffscreenCanvas if available for better perf)
63
+ const atlasWidth = characters.length * cellWidth;
64
+ const atlasHeight = cellHeight;
65
+ if (typeof OffscreenCanvas !== 'undefined') {
66
+ this.charAtlas = new OffscreenCanvas(atlasWidth, atlasHeight);
67
+ }
68
+ else {
69
+ this.charAtlas = document.createElement('canvas');
70
+ this.charAtlas.width = atlasWidth;
71
+ this.charAtlas.height = atlasHeight;
72
+ }
73
+ const ctx = this.charAtlas.getContext('2d');
74
+ if (!ctx) {
75
+ this.charAtlas = null;
76
+ return;
77
+ }
78
+ // Clear with transparent background
79
+ ctx.clearRect(0, 0, atlasWidth, atlasHeight);
80
+ // Render each character
81
+ ctx.fillStyle = color;
82
+ ctx.font = `${cellHeight}px ${fontFamily}`;
83
+ ctx.textBaseline = 'top';
84
+ for (let i = 0; i < characters.length; i++) {
85
+ const char = characters[i];
86
+ if (char !== ' ') {
87
+ ctx.fillText(char, i * cellWidth, 0);
88
+ }
89
+ }
90
+ this.atlasCharacters = characters;
91
+ }
92
+ /**
93
+ * Set up the canvas with optimal rendering settings
94
+ */
95
+ setupCanvas() {
96
+ const { fontFamily, cellHeight } = this.config;
97
+ // Set font for consistent character sizing
98
+ this.ctx.font = `${cellHeight}px ${fontFamily}`;
99
+ this.ctx.textBaseline = 'top';
100
+ // Enable image smoothing for better quality
101
+ this.ctx.imageSmoothingEnabled = true;
102
+ this.ctx.imageSmoothingQuality = 'high';
103
+ }
104
+ /**
105
+ * Update the renderer configuration
106
+ */
107
+ updateConfig(config) {
108
+ const needsAtlasRebuild = config.characters !== undefined ||
109
+ config.color !== undefined ||
110
+ config.cellWidth !== undefined ||
111
+ config.cellHeight !== undefined ||
112
+ config.fontFamily !== undefined;
113
+ this.config = { ...this.config, ...config };
114
+ this.setupCanvas();
115
+ if (needsAtlasRebuild) {
116
+ this.atlasCharacters = ''; // Force rebuild
117
+ this.buildCharacterAtlas();
118
+ }
119
+ }
120
+ /**
121
+ * Resize the canvas to match new dimensions
122
+ */
123
+ resize(width, height) {
124
+ const { canvas } = this.config;
125
+ canvas.width = width;
126
+ canvas.height = height;
127
+ this.setupCanvas();
128
+ }
129
+ /**
130
+ * Get the current canvas dimensions
131
+ */
132
+ getDimensions() {
133
+ return {
134
+ width: this.config.canvas.width,
135
+ height: this.config.canvas.height,
136
+ };
137
+ }
138
+ /**
139
+ * Calculate the number of cells that fit in the canvas
140
+ */
141
+ getCellCount() {
142
+ const { width, height } = this.getDimensions();
143
+ const { cellWidth, cellHeight } = this.config;
144
+ return {
145
+ cols: Math.ceil(width / cellWidth),
146
+ rows: Math.ceil(height / cellHeight),
147
+ };
148
+ }
149
+ /**
150
+ * Clear the canvas
151
+ */
152
+ clear() {
153
+ const { canvas, backgroundColor } = this.config;
154
+ if (backgroundColor) {
155
+ this.ctx.fillStyle = backgroundColor;
156
+ this.ctx.fillRect(0, 0, canvas.width, canvas.height);
157
+ }
158
+ else {
159
+ this.ctx.clearRect(0, 0, canvas.width, canvas.height);
160
+ }
161
+ }
162
+ /**
163
+ * Render a single frame from image data
164
+ */
165
+ renderFrame(imageData) {
166
+ const { canvas, characters, cellWidth, cellHeight, color, brightnessFunction } = this.config;
167
+ const { width, data } = imageData;
168
+ this.clear();
169
+ this.ctx.fillStyle = color;
170
+ for (let y = 0; y < canvas.height; y += cellHeight) {
171
+ for (let x = 0; x < canvas.width; x += cellWidth) {
172
+ const brightness = this.getCellBrightness(data, x, y, width, cellWidth, cellHeight, brightnessFunction);
173
+ const charIndex = Math.floor((brightness / 255) * (characters.length - 1));
174
+ const char = characters[Math.min(charIndex, characters.length - 1)];
175
+ if (char !== ' ') {
176
+ this.ctx.fillText(char, x, y);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ /**
182
+ * Render ASCII from a brightness grid (for pattern-based rendering)
183
+ */
184
+ renderFromBrightnessGrid(grid) {
185
+ const { characters, cellWidth, cellHeight, color } = this.config;
186
+ this.clear();
187
+ this.ctx.fillStyle = color;
188
+ for (let row = 0; row < grid.length; row++) {
189
+ for (let col = 0; col < grid[row].length; col++) {
190
+ const brightness = grid[row][col];
191
+ const charIndex = Math.floor((brightness / 255) * (characters.length - 1));
192
+ const char = characters[Math.min(charIndex, characters.length - 1)];
193
+ if (char !== ' ') {
194
+ this.ctx.fillText(char, col * cellWidth, row * cellHeight);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Render ASCII with per-cell colors (for colored image rendering)
201
+ */
202
+ renderWithColors(data) {
203
+ this.clear();
204
+ for (const { char, color, x, y } of data) {
205
+ if (char !== ' ') {
206
+ this.ctx.fillStyle = color;
207
+ this.ctx.fillText(char, x, y);
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * PERFORMANCE: Render from BrightnessBuffer using texture atlas
213
+ *
214
+ * Uses pre-rendered character sprites instead of fillText calls.
215
+ * 5-10x faster than renderFromBrightnessGrid for large canvases.
216
+ *
217
+ * @param buffer - BrightnessBuffer from fillBrightnessBuffer
218
+ */
219
+ renderFromBuffer(buffer) {
220
+ const { characters, cellWidth, cellHeight } = this.config;
221
+ this.clear();
222
+ // Fall back to fillText if atlas not available
223
+ if (!this.charAtlas) {
224
+ this.ctx.fillStyle = this.config.color;
225
+ const charLen = characters.length - 1;
226
+ let idx = 0;
227
+ for (let row = 0; row < buffer.rows; row++) {
228
+ for (let col = 0; col < buffer.cols; col++) {
229
+ const brightness = buffer.data[idx++];
230
+ const charIndex = (brightness / 255 * charLen) | 0;
231
+ const char = characters[Math.min(charIndex, charLen)];
232
+ if (char !== ' ') {
233
+ this.ctx.fillText(char, col * cellWidth, row * cellHeight);
234
+ }
235
+ }
236
+ }
237
+ return;
238
+ }
239
+ // Use atlas for fast rendering via drawImage
240
+ const charLen = characters.length - 1;
241
+ let idx = 0;
242
+ for (let row = 0; row < buffer.rows; row++) {
243
+ const y = row * cellHeight;
244
+ for (let col = 0; col < buffer.cols; col++) {
245
+ const brightness = buffer.data[idx++];
246
+ const charIndex = (brightness / 255 * charLen) | 0;
247
+ // Skip space characters (index 0 in most charsets)
248
+ if (charIndex === 0 && characters[0] === ' ') {
249
+ continue;
250
+ }
251
+ // Draw from atlas: source is the character's position in the atlas
252
+ this.ctx.drawImage(this.charAtlas, charIndex * cellWidth, 0, cellWidth, cellHeight, // source
253
+ col * cellWidth, y, cellWidth, cellHeight // destination
254
+ );
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * PERFORMANCE: Render brightness grid using atlas (legacy grid format)
260
+ *
261
+ * @param grid - 2D array of brightness values
262
+ */
263
+ renderGridFast(grid) {
264
+ const { characters, cellWidth, cellHeight } = this.config;
265
+ this.clear();
266
+ if (!this.charAtlas) {
267
+ // Fallback to standard method
268
+ this.renderFromBrightnessGrid(grid);
269
+ return;
270
+ }
271
+ const charLen = characters.length - 1;
272
+ for (let row = 0; row < grid.length; row++) {
273
+ const y = row * cellHeight;
274
+ const rowData = grid[row];
275
+ for (let col = 0; col < rowData.length; col++) {
276
+ const brightness = rowData[col];
277
+ const charIndex = (brightness / 255 * charLen) | 0;
278
+ if (charIndex === 0 && characters[0] === ' ') {
279
+ continue;
280
+ }
281
+ this.ctx.drawImage(this.charAtlas, charIndex * cellWidth, 0, cellWidth, cellHeight, col * cellWidth, y, cellWidth, cellHeight);
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Calculate average brightness for a cell region
287
+ */
288
+ getCellBrightness(data, startX, startY, imageWidth, cellWidth, cellHeight, brightnessFunction) {
289
+ let total = 0;
290
+ let count = 0;
291
+ for (let cy = 0; cy < cellHeight; cy++) {
292
+ for (let cx = 0; cx < cellWidth; cx++) {
293
+ const px = ((startY + cy) * imageWidth + (startX + cx)) * 4;
294
+ if (px >= 0 && px + 2 < data.length) {
295
+ total += brightnessFunction(data[px], data[px + 1], data[px + 2]);
296
+ count++;
297
+ }
298
+ }
299
+ }
300
+ return count > 0 ? total / count : 0;
301
+ }
302
+ /**
303
+ * Start an animation loop with FPS limiting
304
+ */
305
+ startAnimation(updateFn, fps = 30) {
306
+ if (this.isRunning) {
307
+ this.stopAnimation();
308
+ }
309
+ this.isRunning = true;
310
+ const frameInterval = 1000 / fps;
311
+ this.lastFrameTime = performance.now();
312
+ const animate = (currentTime) => {
313
+ if (!this.isRunning)
314
+ return;
315
+ const deltaTime = currentTime - this.lastFrameTime;
316
+ if (deltaTime >= frameInterval) {
317
+ const result = updateFn(currentTime, deltaTime);
318
+ if (result instanceof ImageData) {
319
+ this.renderFrame(result);
320
+ }
321
+ else {
322
+ this.renderFromBrightnessGrid(result);
323
+ }
324
+ this.lastFrameTime = currentTime - (deltaTime % frameInterval);
325
+ }
326
+ this.animationId = requestAnimationFrame(animate);
327
+ };
328
+ this.animationId = requestAnimationFrame(animate);
329
+ }
330
+ /**
331
+ * Stop the animation loop
332
+ */
333
+ stopAnimation() {
334
+ this.isRunning = false;
335
+ if (this.animationId !== null) {
336
+ cancelAnimationFrame(this.animationId);
337
+ this.animationId = null;
338
+ }
339
+ }
340
+ /**
341
+ * Check if animation is currently running
342
+ */
343
+ isAnimating() {
344
+ return this.isRunning;
345
+ }
346
+ /**
347
+ * Pause animation (can be resumed)
348
+ */
349
+ pause() {
350
+ this.isRunning = false;
351
+ if (this.animationId !== null) {
352
+ cancelAnimationFrame(this.animationId);
353
+ this.animationId = null;
354
+ }
355
+ }
356
+ /**
357
+ * Clean up and destroy the renderer
358
+ */
359
+ destroy() {
360
+ this.stopAnimation();
361
+ }
362
+ }
@@ -1 +1,56 @@
1
- export { SvelteComponentTyped as default } from 'svelte';
1
+ export type BorderStyle = 'dots' | 'dashes' | 'stars' | 'corners' | 'simple' | 'double';
2
+ export interface GossamerBorderProps {
3
+ /** Border style preset */
4
+ style?: BorderStyle;
5
+ /** Custom characters for border (overrides style) */
6
+ characters?: {
7
+ horizontal?: string;
8
+ vertical?: string;
9
+ topLeft?: string;
10
+ topRight?: string;
11
+ bottomLeft?: string;
12
+ bottomRight?: string;
13
+ };
14
+ /** Border color */
15
+ color?: string;
16
+ /** Border thickness in characters */
17
+ thickness?: number;
18
+ /** Character size in pixels */
19
+ charSize?: number;
20
+ /** Enable animation */
21
+ animated?: boolean;
22
+ /** Animation speed */
23
+ speed?: number;
24
+ /** Padding inside the border */
25
+ padding?: number;
26
+ /** Additional CSS class */
27
+ class?: string;
28
+ }
29
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
30
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
31
+ $$bindings?: Bindings;
32
+ } & Exports;
33
+ (internal: unknown, props: Props & {
34
+ $$events?: Events;
35
+ $$slots?: Slots;
36
+ }): Exports & {
37
+ $set?: any;
38
+ $on?: any;
39
+ };
40
+ z_$$bindings?: Bindings;
41
+ }
42
+ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
43
+ default: any;
44
+ } ? Props extends Record<string, never> ? any : {
45
+ children?: any;
46
+ } : {});
47
+ declare const GossamerBorder: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<GossamerBorderProps, {
48
+ default: {};
49
+ }>, {
50
+ [evt: string]: CustomEvent<any>;
51
+ }, {
52
+ default: {};
53
+ }, {}, "">;
54
+ type GossamerBorder = InstanceType<typeof GossamerBorder>;
55
+ export default GossamerBorder;
56
+ //# sourceMappingURL=GossamerBorder.svelte.d.ts.map