@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,243 @@
1
+ /**
2
+ * Gossamer Animation Utilities
3
+ *
4
+ * FPS limiting, animation loop management, and timing utilities.
5
+ */
6
+
7
+ /**
8
+ * Animation state for tracking loop execution
9
+ */
10
+ export interface AnimationState {
11
+ /** Whether animation is currently running */
12
+ isRunning: boolean;
13
+ /** Current animation frame ID */
14
+ frameId: number | null;
15
+ /** Timestamp of last frame */
16
+ lastFrameTime: number;
17
+ /** Frame interval in ms (derived from FPS) */
18
+ frameInterval: number;
19
+ /** Total elapsed time in ms */
20
+ elapsedTime: number;
21
+ /** Current frame count */
22
+ frameCount: number;
23
+ }
24
+
25
+ /**
26
+ * Options for creating an animation loop
27
+ */
28
+ export interface AnimationOptions {
29
+ /** Target frames per second (default: 30) */
30
+ fps?: number;
31
+ /** Callback when animation starts */
32
+ onStart?: () => void;
33
+ /** Callback when animation stops */
34
+ onStop?: () => void;
35
+ /** Callback for each frame - return false to stop */
36
+ onFrame: (time: number, deltaTime: number, frameCount: number) => boolean | void;
37
+ }
38
+
39
+ /**
40
+ * Create a managed animation loop with FPS limiting
41
+ */
42
+ export function createAnimationLoop(options: AnimationOptions): {
43
+ start: () => void;
44
+ stop: () => void;
45
+ pause: () => void;
46
+ resume: () => void;
47
+ getState: () => AnimationState;
48
+ } {
49
+ const { fps = 30, onStart, onStop, onFrame } = options;
50
+
51
+ const state: AnimationState = {
52
+ isRunning: false,
53
+ frameId: null,
54
+ lastFrameTime: 0,
55
+ frameInterval: 1000 / fps,
56
+ elapsedTime: 0,
57
+ frameCount: 0,
58
+ };
59
+
60
+ let pausedTime = 0;
61
+ let isPaused = false;
62
+
63
+ function animate(currentTime: number): void {
64
+ if (!state.isRunning || isPaused) return;
65
+
66
+ const deltaTime = currentTime - state.lastFrameTime;
67
+
68
+ if (deltaTime >= state.frameInterval) {
69
+ state.elapsedTime += deltaTime;
70
+ state.frameCount++;
71
+
72
+ const continueAnimation = onFrame(currentTime, deltaTime, state.frameCount);
73
+
74
+ if (continueAnimation === false) {
75
+ stop();
76
+ return;
77
+ }
78
+
79
+ // Adjust for frame timing drift
80
+ state.lastFrameTime = currentTime - (deltaTime % state.frameInterval);
81
+ }
82
+
83
+ state.frameId = requestAnimationFrame(animate);
84
+ }
85
+
86
+ function start(): void {
87
+ if (state.isRunning) return;
88
+
89
+ state.isRunning = true;
90
+ state.lastFrameTime = performance.now();
91
+ state.elapsedTime = 0;
92
+ state.frameCount = 0;
93
+ isPaused = false;
94
+
95
+ onStart?.();
96
+ state.frameId = requestAnimationFrame(animate);
97
+ }
98
+
99
+ function stop(): void {
100
+ state.isRunning = false;
101
+ isPaused = false;
102
+
103
+ if (state.frameId !== null) {
104
+ cancelAnimationFrame(state.frameId);
105
+ state.frameId = null;
106
+ }
107
+
108
+ onStop?.();
109
+ }
110
+
111
+ function pause(): void {
112
+ if (!state.isRunning || isPaused) return;
113
+
114
+ isPaused = true;
115
+ pausedTime = performance.now();
116
+
117
+ if (state.frameId !== null) {
118
+ cancelAnimationFrame(state.frameId);
119
+ state.frameId = null;
120
+ }
121
+ }
122
+
123
+ function resume(): void {
124
+ if (!state.isRunning || !isPaused) return;
125
+
126
+ isPaused = false;
127
+ // Adjust lastFrameTime to account for paused duration
128
+ state.lastFrameTime += performance.now() - pausedTime;
129
+ state.frameId = requestAnimationFrame(animate);
130
+ }
131
+
132
+ function getState(): AnimationState {
133
+ return { ...state };
134
+ }
135
+
136
+ return { start, stop, pause, resume, getState };
137
+ }
138
+
139
+ /**
140
+ * Simple throttle function for limiting function execution
141
+ */
142
+ export function throttle<T extends (...args: unknown[]) => unknown>(
143
+ fn: T,
144
+ limit: number
145
+ ): (...args: Parameters<T>) => void {
146
+ let lastCall = 0;
147
+ let timeout: ReturnType<typeof setTimeout> | null = null;
148
+
149
+ return (...args: Parameters<T>): void => {
150
+ const now = Date.now();
151
+
152
+ if (now - lastCall >= limit) {
153
+ lastCall = now;
154
+ fn(...args);
155
+ } else if (!timeout) {
156
+ timeout = setTimeout(() => {
157
+ lastCall = Date.now();
158
+ timeout = null;
159
+ fn(...args);
160
+ }, limit - (now - lastCall));
161
+ }
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Debounce function for delaying execution until activity stops
167
+ */
168
+ export function debounce<T extends (...args: unknown[]) => unknown>(
169
+ fn: T,
170
+ delay: number
171
+ ): (...args: Parameters<T>) => void {
172
+ let timeout: ReturnType<typeof setTimeout> | null = null;
173
+
174
+ return (...args: Parameters<T>): void => {
175
+ if (timeout) {
176
+ clearTimeout(timeout);
177
+ }
178
+
179
+ timeout = setTimeout(() => {
180
+ timeout = null;
181
+ fn(...args);
182
+ }, delay);
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Calculate actual FPS from frame times
188
+ */
189
+ export function calculateFPS(frameTimes: number[], sampleSize: number = 60): number {
190
+ if (frameTimes.length < 2) return 0;
191
+
192
+ const samples = frameTimes.slice(-sampleSize);
193
+ const totalTime = samples[samples.length - 1] - samples[0];
194
+ const frameCount = samples.length - 1;
195
+
196
+ if (totalTime <= 0) return 0;
197
+
198
+ return (frameCount / totalTime) * 1000;
199
+ }
200
+
201
+ /**
202
+ * Easing functions for smooth animations
203
+ */
204
+ export const easings = {
205
+ /** Linear - no easing */
206
+ linear: (t: number): number => t,
207
+
208
+ /** Ease in - slow start */
209
+ easeIn: (t: number): number => t * t,
210
+
211
+ /** Ease out - slow end */
212
+ easeOut: (t: number): number => t * (2 - t),
213
+
214
+ /** Ease in/out - slow start and end */
215
+ easeInOut: (t: number): number => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
216
+
217
+ /** Sine ease in */
218
+ sineIn: (t: number): number => 1 - Math.cos((t * Math.PI) / 2),
219
+
220
+ /** Sine ease out */
221
+ sineOut: (t: number): number => Math.sin((t * Math.PI) / 2),
222
+
223
+ /** Sine ease in/out */
224
+ sineInOut: (t: number): number => -(Math.cos(Math.PI * t) - 1) / 2,
225
+
226
+ /** Bounce at end */
227
+ bounceOut: (t: number): number => {
228
+ const n1 = 7.5625;
229
+ const d1 = 2.75;
230
+
231
+ if (t < 1 / d1) {
232
+ return n1 * t * t;
233
+ } else if (t < 2 / d1) {
234
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
235
+ } else if (t < 2.5 / d1) {
236
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
237
+ } else {
238
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
239
+ }
240
+ },
241
+ };
242
+
243
+ export type EasingFunction = (t: number) => number;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Tests for character set utilities
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ CHARACTER_SETS,
7
+ getCharacterSet,
8
+ getCharacters,
9
+ getCharacterSetNames,
10
+ createCharacterSet,
11
+ validateCharacterSet,
12
+ invertCharacters,
13
+ } from './characters';
14
+
15
+ describe('CHARACTER_SETS', () => {
16
+ it('should contain standard character set', () => {
17
+ expect(CHARACTER_SETS.standard).toBeDefined();
18
+ expect(CHARACTER_SETS.standard.characters).toBe(' .:-=+*#%@');
19
+ });
20
+
21
+ it('should have all expected character sets', () => {
22
+ const expectedSets = [
23
+ 'standard',
24
+ 'dense',
25
+ 'minimal',
26
+ 'grove',
27
+ 'dots',
28
+ 'blocks',
29
+ 'lines',
30
+ 'stars',
31
+ 'nature',
32
+ 'weather',
33
+ 'binary',
34
+ 'math',
35
+ ];
36
+
37
+ for (const setName of expectedSets) {
38
+ expect(CHARACTER_SETS[setName]).toBeDefined();
39
+ }
40
+ });
41
+
42
+ it('should have all character sets start with space', () => {
43
+ for (const [name, set] of Object.entries(CHARACTER_SETS)) {
44
+ expect(set.characters[0]).toBe(' ');
45
+ }
46
+ });
47
+ });
48
+
49
+ describe('getCharacterSet', () => {
50
+ it('should return character set by name', () => {
51
+ const result = getCharacterSet('standard');
52
+ expect(result).toBeDefined();
53
+ expect(result?.name).toBe('Standard');
54
+ expect(result?.characters).toBe(' .:-=+*#%@');
55
+ });
56
+
57
+ it('should return undefined for unknown set', () => {
58
+ const result = getCharacterSet('nonexistent');
59
+ expect(result).toBeUndefined();
60
+ });
61
+ });
62
+
63
+ describe('getCharacters', () => {
64
+ it('should return characters string for valid set', () => {
65
+ expect(getCharacters('minimal')).toBe(' .:*#');
66
+ expect(getCharacters('blocks')).toBe(' ░▒▓█');
67
+ });
68
+
69
+ it('should return standard characters for unknown set', () => {
70
+ expect(getCharacters('nonexistent')).toBe(' .:-=+*#%@');
71
+ });
72
+ });
73
+
74
+ describe('getCharacterSetNames', () => {
75
+ it('should return array of all set names', () => {
76
+ const names = getCharacterSetNames();
77
+ expect(Array.isArray(names)).toBe(true);
78
+ expect(names).toContain('standard');
79
+ expect(names).toContain('minimal');
80
+ expect(names).toContain('blocks');
81
+ });
82
+
83
+ it('should return same count as CHARACTER_SETS keys', () => {
84
+ const names = getCharacterSetNames();
85
+ expect(names.length).toBe(Object.keys(CHARACTER_SETS).length);
86
+ });
87
+ });
88
+
89
+ describe('createCharacterSet', () => {
90
+ it('should create character set with all properties', () => {
91
+ const result = createCharacterSet(
92
+ 'custom',
93
+ ' abc',
94
+ 'A custom set',
95
+ ['testing']
96
+ );
97
+
98
+ expect(result.name).toBe('custom');
99
+ expect(result.characters).toBe(' abc');
100
+ expect(result.description).toBe('A custom set');
101
+ expect(result.bestFor).toEqual(['testing']);
102
+ });
103
+
104
+ it('should use defaults for optional parameters', () => {
105
+ const result = createCharacterSet('minimal', ' xy');
106
+
107
+ expect(result.name).toBe('minimal');
108
+ expect(result.characters).toBe(' xy');
109
+ expect(result.description).toBe('');
110
+ expect(result.bestFor).toEqual([]);
111
+ });
112
+ });
113
+
114
+ describe('validateCharacterSet', () => {
115
+ it('should return true for valid character sets', () => {
116
+ expect(validateCharacterSet(' .:-=+*#%@')).toBe(true);
117
+ expect(validateCharacterSet(' ab')).toBe(true);
118
+ expect(validateCharacterSet(' ░▒▓█')).toBe(true);
119
+ });
120
+
121
+ it('should return false for too short sets', () => {
122
+ expect(validateCharacterSet('')).toBe(false);
123
+ expect(validateCharacterSet(' ')).toBe(false);
124
+ });
125
+
126
+ it('should return false for sets not starting with space', () => {
127
+ expect(validateCharacterSet('abc')).toBe(false);
128
+ expect(validateCharacterSet('.:-=')).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe('invertCharacters', () => {
133
+ it('should reverse character string', () => {
134
+ expect(invertCharacters(' abc')).toBe('cba ');
135
+ expect(invertCharacters(' .:-=')).toBe('=-:. ');
136
+ });
137
+
138
+ it('should handle single character', () => {
139
+ expect(invertCharacters(' ')).toBe(' ');
140
+ });
141
+
142
+ it('should be reversible', () => {
143
+ const original = ' .:-=+*#%@';
144
+ const inverted = invertCharacters(original);
145
+ const restored = invertCharacters(inverted);
146
+ expect(restored).toBe(original);
147
+ });
148
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Gossamer Character Sets
3
+ *
4
+ * Predefined character sets for ASCII rendering, ordered from light to dark.
5
+ * Character density determines which character represents each brightness level.
6
+ */
7
+
8
+ /**
9
+ * Character set configuration
10
+ */
11
+ export interface CharacterSet {
12
+ /** Unique identifier for the character set */
13
+ name: string;
14
+ /** Description of the set's aesthetic */
15
+ description: string;
16
+ /** Characters ordered from lightest (space) to darkest */
17
+ characters: string;
18
+ /** Recommended use cases */
19
+ bestFor: string[];
20
+ }
21
+
22
+ /**
23
+ * Standard character sets for ASCII rendering
24
+ */
25
+ export const CHARACTER_SETS: Record<string, CharacterSet> = {
26
+ standard: {
27
+ name: 'Standard',
28
+ description: 'Classic ASCII art character set',
29
+ characters: ' .:-=+*#%@',
30
+ bestFor: ['general', 'images', 'patterns'],
31
+ },
32
+
33
+ dense: {
34
+ name: 'Dense',
35
+ description: 'High detail character set with many gradations',
36
+ characters: " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$",
37
+ bestFor: ['detailed images', 'portraits', 'high contrast'],
38
+ },
39
+
40
+ minimal: {
41
+ name: 'Minimal',
42
+ description: 'Clean and simple with few characters',
43
+ characters: ' .:*#',
44
+ bestFor: ['backgrounds', 'subtle effects', 'clean aesthetic'],
45
+ },
46
+
47
+ grove: {
48
+ name: 'Grove',
49
+ description: 'Organic, soft characters inspired by nature',
50
+ characters: ' ·∙•◦○◉●',
51
+ bestFor: ['organic patterns', 'soft backgrounds', 'nature themes'],
52
+ },
53
+
54
+ dots: {
55
+ name: 'Dots',
56
+ description: 'Braille-like dot patterns',
57
+ characters: ' ⋅∘∙●',
58
+ bestFor: ['stipple effects', 'pointillism', 'dotted textures'],
59
+ },
60
+
61
+ blocks: {
62
+ name: 'Blocks',
63
+ description: 'Block-based characters for sharp edges',
64
+ characters: ' ░▒▓█',
65
+ bestFor: ['retro effects', 'pixel art', 'bold patterns'],
66
+ },
67
+
68
+ lines: {
69
+ name: 'Lines',
70
+ description: 'Line-based characters for directional effects',
71
+ characters: ' -─═╌│┃',
72
+ bestFor: ['rain effects', 'streaks', 'motion blur'],
73
+ },
74
+
75
+ stars: {
76
+ name: 'Stars',
77
+ description: 'Star and sparkle characters',
78
+ characters: ' ·✧✦✫✬✯★',
79
+ bestFor: ['sparkle effects', 'night sky', 'magical themes'],
80
+ },
81
+
82
+ nature: {
83
+ name: 'Nature',
84
+ description: 'Nature-themed decorative characters',
85
+ characters: ' .~≈∿⌇☘',
86
+ bestFor: ['decorative', 'themed effects', 'organic patterns'],
87
+ },
88
+
89
+ weather: {
90
+ name: 'Weather',
91
+ description: 'Weather-related symbols',
92
+ characters: ' ·.:*❄❅❆',
93
+ bestFor: ['snow effects', 'weather simulations', 'seasonal themes'],
94
+ },
95
+
96
+ binary: {
97
+ name: 'Binary',
98
+ description: 'Digital-style binary characters',
99
+ characters: ' 01',
100
+ bestFor: ['digital effects', 'matrix style', 'tech themes'],
101
+ },
102
+
103
+ math: {
104
+ name: 'Math',
105
+ description: 'Mathematical symbols',
106
+ characters: ' +-×÷=≠≈∞',
107
+ bestFor: ['abstract patterns', 'tech themes', 'decorative'],
108
+ },
109
+ };
110
+
111
+ /**
112
+ * Get a character set by name
113
+ */
114
+ export function getCharacterSet(name: string): CharacterSet | undefined {
115
+ return CHARACTER_SETS[name];
116
+ }
117
+
118
+ /**
119
+ * Get just the characters string from a named set
120
+ */
121
+ export function getCharacters(name: string): string {
122
+ return CHARACTER_SETS[name]?.characters || CHARACTER_SETS.standard.characters;
123
+ }
124
+
125
+ /**
126
+ * List all available character set names
127
+ */
128
+ export function getCharacterSetNames(): string[] {
129
+ return Object.keys(CHARACTER_SETS);
130
+ }
131
+
132
+ /**
133
+ * Create a custom character set
134
+ */
135
+ export function createCharacterSet(
136
+ name: string,
137
+ characters: string,
138
+ description: string = '',
139
+ bestFor: string[] = []
140
+ ): CharacterSet {
141
+ return {
142
+ name,
143
+ description,
144
+ characters,
145
+ bestFor,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Validate that a character string is suitable for ASCII rendering
151
+ * (should start with space and have at least 2 characters)
152
+ */
153
+ export function validateCharacterSet(characters: string): boolean {
154
+ if (characters.length < 2) return false;
155
+ if (characters[0] !== ' ') return false;
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Reverse a character set (for inverted brightness mapping)
161
+ */
162
+ export function invertCharacters(characters: string): string {
163
+ return characters.split('').reverse().join('');
164
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tests for core index functions
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ calculateBrightness,
7
+ brightnessToChar,
8
+ DEFAULT_CHARACTERS,
9
+ DEFAULT_CONFIG,
10
+ VERSION,
11
+ } from './index';
12
+
13
+ describe('calculateBrightness', () => {
14
+ it('should return 0 for black', () => {
15
+ expect(calculateBrightness(0, 0, 0)).toBe(0);
16
+ });
17
+
18
+ it('should return 255 for white', () => {
19
+ expect(calculateBrightness(255, 255, 255)).toBeCloseTo(255, 5);
20
+ });
21
+
22
+ it('should weight green highest', () => {
23
+ // Green contributes 0.72, so pure green should be brighter than pure red
24
+ const greenBrightness = calculateBrightness(0, 255, 0);
25
+ const redBrightness = calculateBrightness(255, 0, 0);
26
+ const blueBrightness = calculateBrightness(0, 0, 255);
27
+
28
+ expect(greenBrightness).toBeGreaterThan(redBrightness);
29
+ expect(greenBrightness).toBeGreaterThan(blueBrightness);
30
+ expect(redBrightness).toBeGreaterThan(blueBrightness);
31
+ });
32
+
33
+ it('should calculate correct luminance values', () => {
34
+ // Pure red: 0.21 * 255 = 53.55
35
+ expect(calculateBrightness(255, 0, 0)).toBeCloseTo(53.55, 1);
36
+ // Pure green: 0.72 * 255 = 183.6
37
+ expect(calculateBrightness(0, 255, 0)).toBeCloseTo(183.6, 1);
38
+ // Pure blue: 0.07 * 255 = 17.85
39
+ expect(calculateBrightness(0, 0, 255)).toBeCloseTo(17.85, 1);
40
+ });
41
+
42
+ it('should handle mid-gray', () => {
43
+ const gray = calculateBrightness(128, 128, 128);
44
+ // 128 * (0.21 + 0.72 + 0.07) = 128
45
+ expect(gray).toBe(128);
46
+ });
47
+ });
48
+
49
+ describe('brightnessToChar', () => {
50
+ it('should return space for brightness 0', () => {
51
+ expect(brightnessToChar(0)).toBe(' ');
52
+ });
53
+
54
+ it('should return last character for brightness 255', () => {
55
+ expect(brightnessToChar(255)).toBe('@');
56
+ });
57
+
58
+ it('should map mid-brightness to middle character', () => {
59
+ // With ' .:-=+*#%@' (10 chars), brightness 127-128 should be around index 4-5
60
+ const char = brightnessToChar(127);
61
+ expect(['=', '+'].includes(char)).toBe(true);
62
+ });
63
+
64
+ it('should use custom character set', () => {
65
+ const customSet = ' abc';
66
+ expect(brightnessToChar(0, customSet)).toBe(' ');
67
+ expect(brightnessToChar(255, customSet)).toBe('c');
68
+ });
69
+
70
+ it('should handle two-character set', () => {
71
+ const binarySet = ' #';
72
+ expect(brightnessToChar(0, binarySet)).toBe(' ');
73
+ expect(brightnessToChar(127, binarySet)).toBe(' ');
74
+ expect(brightnessToChar(255, binarySet)).toBe('#');
75
+ });
76
+
77
+ it('should clamp to valid range', () => {
78
+ // Even with extreme values, should not crash
79
+ expect(() => brightnessToChar(-10)).not.toThrow();
80
+ expect(() => brightnessToChar(300)).not.toThrow();
81
+ });
82
+ });
83
+
84
+ describe('DEFAULT_CHARACTERS', () => {
85
+ it('should be standard ASCII art character set', () => {
86
+ expect(DEFAULT_CHARACTERS).toBe(' .:-=+*#%@');
87
+ });
88
+
89
+ it('should start with space', () => {
90
+ expect(DEFAULT_CHARACTERS[0]).toBe(' ');
91
+ });
92
+
93
+ it('should have 10 characters', () => {
94
+ expect(DEFAULT_CHARACTERS.length).toBe(10);
95
+ });
96
+ });
97
+
98
+ describe('DEFAULT_CONFIG', () => {
99
+ it('should have all required properties', () => {
100
+ expect(DEFAULT_CONFIG.characters).toBe(DEFAULT_CHARACTERS);
101
+ expect(DEFAULT_CONFIG.cellWidth).toBe(8);
102
+ expect(DEFAULT_CONFIG.cellHeight).toBe(12);
103
+ expect(DEFAULT_CONFIG.color).toBe('#ffffff');
104
+ expect(DEFAULT_CONFIG.backgroundColor).toBe('');
105
+ expect(DEFAULT_CONFIG.fontFamily).toBe('monospace');
106
+ expect(DEFAULT_CONFIG.animate).toBe(false);
107
+ expect(DEFAULT_CONFIG.fps).toBe(30);
108
+ });
109
+ });
110
+
111
+ describe('VERSION', () => {
112
+ it('should be a semver string', () => {
113
+ expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/);
114
+ });
115
+ });