@autumnsgrove/gossamer 0.1.1 → 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.
- package/LICENSE +21 -0
- package/dist/animation.js +165 -0
- package/dist/animation.test.js +204 -0
- package/dist/characters.js +176 -0
- package/dist/characters.test.js +115 -0
- package/dist/colors.js +199 -0
- package/dist/index.js +79 -1850
- package/dist/index.test.js +92 -0
- package/dist/patterns.js +539 -0
- package/dist/patterns.test.js +223 -0
- package/dist/renderer.js +362 -0
- package/dist/svelte/GossamerBorder.svelte.d.ts +56 -1
- package/dist/svelte/GossamerBorder.svelte.d.ts.map +1 -0
- package/dist/svelte/GossamerClouds.svelte.d.ts +31 -1
- package/dist/svelte/GossamerClouds.svelte.d.ts.map +1 -0
- package/dist/svelte/GossamerImage.svelte.d.ts +28 -1
- package/dist/svelte/GossamerImage.svelte.d.ts.map +1 -0
- package/dist/svelte/GossamerOverlay.svelte.d.ts +32 -1
- package/dist/svelte/GossamerOverlay.svelte.d.ts.map +1 -0
- package/dist/svelte/GossamerText.svelte.d.ts +29 -1
- package/dist/svelte/GossamerText.svelte.d.ts.map +1 -0
- package/dist/svelte/index.js +31 -3646
- package/dist/svelte/presets.d.ts +4 -2
- package/dist/svelte/presets.js +161 -0
- package/dist/utils/canvas.js +139 -0
- package/dist/utils/image.js +195 -0
- package/dist/utils/performance.js +205 -0
- package/package.json +18 -22
- package/dist/index.js.map +0 -1
- package/dist/style.css +0 -124
- package/dist/svelte/index.js.map +0 -1
- package/src/animation.test.ts +0 -254
- package/src/animation.ts +0 -243
- package/src/characters.test.ts +0 -148
- package/src/characters.ts +0 -219
- package/src/colors.ts +0 -234
- package/src/index.test.ts +0 -115
- package/src/index.ts +0 -234
- package/src/patterns.test.ts +0 -273
- package/src/patterns.ts +0 -760
- package/src/renderer.ts +0 -470
- package/src/svelte/index.ts +0 -75
- package/src/svelte/presets.ts +0 -174
- package/src/utils/canvas.ts +0 -210
- package/src/utils/image.ts +0 -275
- package/src/utils/performance.ts +0 -282
- /package/{src → dist}/svelte/GossamerBorder.svelte +0 -0
- /package/{src → dist}/svelte/GossamerClouds.svelte +0 -0
- /package/{src → dist}/svelte/GossamerImage.svelte +0 -0
- /package/{src → dist}/svelte/GossamerOverlay.svelte +0 -0
- /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
|
+
});
|
package/dist/renderer.js
ADDED
|
@@ -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
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GossamerBorder.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/GossamerBorder.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsRH,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AACD,KAAK,gCAAgC,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,GACvD,CAAC,KAAK,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GACzB,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACnC,GAAG,GACH;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAClB,EAAE,CAAC,CAAC;AAId,QAAA,MAAM,cAAc;;;;;;UAAqF,CAAC;AACxF,KAAK,cAAc,GAAG,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,eAAe,cAAc,CAAC"}
|
|
@@ -1 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PatternType } from '../index';
|
|
2
|
+
export interface GossamerCloudsProps {
|
|
3
|
+
/** Pattern type for generation */
|
|
4
|
+
pattern?: PatternType;
|
|
5
|
+
/** Character set (light to dark) */
|
|
6
|
+
characters?: string;
|
|
7
|
+
/** Foreground color */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Overall opacity (0-1) */
|
|
10
|
+
opacity?: number;
|
|
11
|
+
/** Enable animation */
|
|
12
|
+
animated?: boolean;
|
|
13
|
+
/** Animation speed multiplier */
|
|
14
|
+
speed?: number;
|
|
15
|
+
/** Pattern frequency (scale) */
|
|
16
|
+
frequency?: number;
|
|
17
|
+
/** Pattern amplitude (intensity) */
|
|
18
|
+
amplitude?: number;
|
|
19
|
+
/** Cell size in pixels */
|
|
20
|
+
cellSize?: number;
|
|
21
|
+
/** Target FPS for animation */
|
|
22
|
+
fps?: number;
|
|
23
|
+
/** Use a preset configuration */
|
|
24
|
+
preset?: string;
|
|
25
|
+
/** Additional CSS class */
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
declare const GossamerClouds: import("svelte").Component<GossamerCloudsProps, {}, "">;
|
|
29
|
+
type GossamerClouds = ReturnType<typeof GossamerClouds>;
|
|
30
|
+
export default GossamerClouds;
|
|
31
|
+
//# sourceMappingURL=GossamerClouds.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GossamerClouds.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/GossamerClouds.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AAE3D,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkOH,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|