@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AutumnsGrove
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Gossamer Animation Utilities
3
+ *
4
+ * FPS limiting, animation loop management, and timing utilities.
5
+ */
6
+ /**
7
+ * Create a managed animation loop with FPS limiting
8
+ */
9
+ export function createAnimationLoop(options) {
10
+ const { fps = 30, onStart, onStop, onFrame } = options;
11
+ const state = {
12
+ isRunning: false,
13
+ frameId: null,
14
+ lastFrameTime: 0,
15
+ frameInterval: 1000 / fps,
16
+ elapsedTime: 0,
17
+ frameCount: 0,
18
+ };
19
+ let pausedTime = 0;
20
+ let isPaused = false;
21
+ function animate(currentTime) {
22
+ if (!state.isRunning || isPaused)
23
+ return;
24
+ const deltaTime = currentTime - state.lastFrameTime;
25
+ if (deltaTime >= state.frameInterval) {
26
+ state.elapsedTime += deltaTime;
27
+ state.frameCount++;
28
+ const continueAnimation = onFrame(currentTime, deltaTime, state.frameCount);
29
+ if (continueAnimation === false) {
30
+ stop();
31
+ return;
32
+ }
33
+ // Adjust for frame timing drift
34
+ state.lastFrameTime = currentTime - (deltaTime % state.frameInterval);
35
+ }
36
+ state.frameId = requestAnimationFrame(animate);
37
+ }
38
+ function start() {
39
+ if (state.isRunning)
40
+ return;
41
+ state.isRunning = true;
42
+ state.lastFrameTime = performance.now();
43
+ state.elapsedTime = 0;
44
+ state.frameCount = 0;
45
+ isPaused = false;
46
+ onStart?.();
47
+ state.frameId = requestAnimationFrame(animate);
48
+ }
49
+ function stop() {
50
+ state.isRunning = false;
51
+ isPaused = false;
52
+ if (state.frameId !== null) {
53
+ cancelAnimationFrame(state.frameId);
54
+ state.frameId = null;
55
+ }
56
+ onStop?.();
57
+ }
58
+ function pause() {
59
+ if (!state.isRunning || isPaused)
60
+ return;
61
+ isPaused = true;
62
+ pausedTime = performance.now();
63
+ if (state.frameId !== null) {
64
+ cancelAnimationFrame(state.frameId);
65
+ state.frameId = null;
66
+ }
67
+ }
68
+ function resume() {
69
+ if (!state.isRunning || !isPaused)
70
+ return;
71
+ isPaused = false;
72
+ // Adjust lastFrameTime to account for paused duration
73
+ state.lastFrameTime += performance.now() - pausedTime;
74
+ state.frameId = requestAnimationFrame(animate);
75
+ }
76
+ function getState() {
77
+ return { ...state };
78
+ }
79
+ return { start, stop, pause, resume, getState };
80
+ }
81
+ /**
82
+ * Simple throttle function for limiting function execution
83
+ */
84
+ export function throttle(fn, limit) {
85
+ let lastCall = 0;
86
+ let timeout = null;
87
+ return (...args) => {
88
+ const now = Date.now();
89
+ if (now - lastCall >= limit) {
90
+ lastCall = now;
91
+ fn(...args);
92
+ }
93
+ else if (!timeout) {
94
+ timeout = setTimeout(() => {
95
+ lastCall = Date.now();
96
+ timeout = null;
97
+ fn(...args);
98
+ }, limit - (now - lastCall));
99
+ }
100
+ };
101
+ }
102
+ /**
103
+ * Debounce function for delaying execution until activity stops
104
+ */
105
+ export function debounce(fn, delay) {
106
+ let timeout = null;
107
+ return (...args) => {
108
+ if (timeout) {
109
+ clearTimeout(timeout);
110
+ }
111
+ timeout = setTimeout(() => {
112
+ timeout = null;
113
+ fn(...args);
114
+ }, delay);
115
+ };
116
+ }
117
+ /**
118
+ * Calculate actual FPS from frame times
119
+ */
120
+ export function calculateFPS(frameTimes, sampleSize = 60) {
121
+ if (frameTimes.length < 2)
122
+ return 0;
123
+ const samples = frameTimes.slice(-sampleSize);
124
+ const totalTime = samples[samples.length - 1] - samples[0];
125
+ const frameCount = samples.length - 1;
126
+ if (totalTime <= 0)
127
+ return 0;
128
+ return (frameCount / totalTime) * 1000;
129
+ }
130
+ /**
131
+ * Easing functions for smooth animations
132
+ */
133
+ export const easings = {
134
+ /** Linear - no easing */
135
+ linear: (t) => t,
136
+ /** Ease in - slow start */
137
+ easeIn: (t) => t * t,
138
+ /** Ease out - slow end */
139
+ easeOut: (t) => t * (2 - t),
140
+ /** Ease in/out - slow start and end */
141
+ easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
142
+ /** Sine ease in */
143
+ sineIn: (t) => 1 - Math.cos((t * Math.PI) / 2),
144
+ /** Sine ease out */
145
+ sineOut: (t) => Math.sin((t * Math.PI) / 2),
146
+ /** Sine ease in/out */
147
+ sineInOut: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
148
+ /** Bounce at end */
149
+ bounceOut: (t) => {
150
+ const n1 = 7.5625;
151
+ const d1 = 2.75;
152
+ if (t < 1 / d1) {
153
+ return n1 * t * t;
154
+ }
155
+ else if (t < 2 / d1) {
156
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
157
+ }
158
+ else if (t < 2.5 / d1) {
159
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
160
+ }
161
+ else {
162
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
163
+ }
164
+ },
165
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Tests for animation utilities
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { throttle, debounce, calculateFPS, easings, createAnimationLoop } from './animation';
6
+ describe('throttle', () => {
7
+ beforeEach(() => {
8
+ vi.useFakeTimers();
9
+ });
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+ it('should call function immediately on first call', () => {
14
+ const fn = vi.fn();
15
+ const throttled = throttle(fn, 100);
16
+ throttled();
17
+ expect(fn).toHaveBeenCalledTimes(1);
18
+ });
19
+ it('should throttle subsequent calls within limit', () => {
20
+ const fn = vi.fn();
21
+ const throttled = throttle(fn, 100);
22
+ throttled();
23
+ throttled();
24
+ throttled();
25
+ expect(fn).toHaveBeenCalledTimes(1);
26
+ });
27
+ it('should call function again after limit expires', () => {
28
+ const fn = vi.fn();
29
+ const throttled = throttle(fn, 100);
30
+ throttled();
31
+ expect(fn).toHaveBeenCalledTimes(1);
32
+ vi.advanceTimersByTime(150);
33
+ throttled();
34
+ expect(fn).toHaveBeenCalledTimes(2);
35
+ });
36
+ it('should pass arguments to throttled function', () => {
37
+ const fn = vi.fn();
38
+ const throttled = throttle(fn, 100);
39
+ throttled('arg1', 'arg2');
40
+ expect(fn).toHaveBeenCalledWith('arg1', 'arg2');
41
+ });
42
+ });
43
+ describe('debounce', () => {
44
+ beforeEach(() => {
45
+ vi.useFakeTimers();
46
+ });
47
+ afterEach(() => {
48
+ vi.useRealTimers();
49
+ });
50
+ it('should not call function immediately', () => {
51
+ const fn = vi.fn();
52
+ const debounced = debounce(fn, 100);
53
+ debounced();
54
+ expect(fn).not.toHaveBeenCalled();
55
+ });
56
+ it('should call function after delay', () => {
57
+ const fn = vi.fn();
58
+ const debounced = debounce(fn, 100);
59
+ debounced();
60
+ vi.advanceTimersByTime(100);
61
+ expect(fn).toHaveBeenCalledTimes(1);
62
+ });
63
+ it('should reset timer on subsequent calls', () => {
64
+ const fn = vi.fn();
65
+ const debounced = debounce(fn, 100);
66
+ debounced();
67
+ vi.advanceTimersByTime(50);
68
+ debounced();
69
+ vi.advanceTimersByTime(50);
70
+ expect(fn).not.toHaveBeenCalled();
71
+ vi.advanceTimersByTime(50);
72
+ expect(fn).toHaveBeenCalledTimes(1);
73
+ });
74
+ it('should pass arguments to debounced function', () => {
75
+ const fn = vi.fn();
76
+ const debounced = debounce(fn, 100);
77
+ debounced('test', 123);
78
+ vi.advanceTimersByTime(100);
79
+ expect(fn).toHaveBeenCalledWith('test', 123);
80
+ });
81
+ });
82
+ describe('calculateFPS', () => {
83
+ it('should return 0 for empty array', () => {
84
+ expect(calculateFPS([])).toBe(0);
85
+ });
86
+ it('should return 0 for single frame time', () => {
87
+ expect(calculateFPS([1000])).toBe(0);
88
+ });
89
+ it('should calculate FPS correctly', () => {
90
+ // 60 frames over 1 second = 60 FPS
91
+ const frameTimes = Array.from({ length: 61 }, (_, i) => i * (1000 / 60));
92
+ const fps = calculateFPS(frameTimes);
93
+ expect(fps).toBeCloseTo(60, 0);
94
+ });
95
+ it('should handle 30 FPS', () => {
96
+ // 30 frames over 1 second = 30 FPS
97
+ const frameTimes = Array.from({ length: 31 }, (_, i) => i * (1000 / 30));
98
+ const fps = calculateFPS(frameTimes);
99
+ expect(fps).toBeCloseTo(30, 0);
100
+ });
101
+ it('should use sample size parameter', () => {
102
+ // Create 100 frame times
103
+ const frameTimes = Array.from({ length: 100 }, (_, i) => i * 16.67);
104
+ // Should only use last 10 samples
105
+ const fps = calculateFPS(frameTimes, 10);
106
+ expect(fps).toBeCloseTo(60, 0);
107
+ });
108
+ });
109
+ describe('easings', () => {
110
+ describe('linear', () => {
111
+ it('should return input unchanged', () => {
112
+ expect(easings.linear(0)).toBe(0);
113
+ expect(easings.linear(0.5)).toBe(0.5);
114
+ expect(easings.linear(1)).toBe(1);
115
+ });
116
+ });
117
+ describe('easeIn', () => {
118
+ it('should start slow', () => {
119
+ expect(easings.easeIn(0)).toBe(0);
120
+ expect(easings.easeIn(0.5)).toBe(0.25);
121
+ expect(easings.easeIn(1)).toBe(1);
122
+ });
123
+ });
124
+ describe('easeOut', () => {
125
+ it('should end slow', () => {
126
+ expect(easings.easeOut(0)).toBe(0);
127
+ expect(easings.easeOut(0.5)).toBe(0.75);
128
+ expect(easings.easeOut(1)).toBe(1);
129
+ });
130
+ });
131
+ describe('easeInOut', () => {
132
+ it('should start and end slow', () => {
133
+ expect(easings.easeInOut(0)).toBe(0);
134
+ expect(easings.easeInOut(0.5)).toBe(0.5);
135
+ expect(easings.easeInOut(1)).toBe(1);
136
+ });
137
+ it('should be symmetric around 0.5', () => {
138
+ const val1 = easings.easeInOut(0.25);
139
+ const val2 = 1 - easings.easeInOut(0.75);
140
+ expect(val1).toBeCloseTo(val2, 5);
141
+ });
142
+ });
143
+ describe('sineIn', () => {
144
+ it('should return 0 at start and 1 at end', () => {
145
+ expect(easings.sineIn(0)).toBe(0);
146
+ expect(easings.sineIn(1)).toBeCloseTo(1, 5);
147
+ });
148
+ });
149
+ describe('sineOut', () => {
150
+ it('should return 0 at start and 1 at end', () => {
151
+ expect(easings.sineOut(0)).toBe(0);
152
+ expect(easings.sineOut(1)).toBeCloseTo(1, 5);
153
+ });
154
+ });
155
+ describe('sineInOut', () => {
156
+ it('should return 0 at start, 0.5 at middle, 1 at end', () => {
157
+ expect(easings.sineInOut(0)).toBeCloseTo(0, 5);
158
+ expect(easings.sineInOut(0.5)).toBeCloseTo(0.5, 5);
159
+ expect(easings.sineInOut(1)).toBeCloseTo(1, 5);
160
+ });
161
+ });
162
+ describe('bounceOut', () => {
163
+ it('should return 0 at start and 1 at end', () => {
164
+ expect(easings.bounceOut(0)).toBe(0);
165
+ expect(easings.bounceOut(1)).toBeCloseTo(1, 5);
166
+ });
167
+ it('should overshoot intermediate values', () => {
168
+ // bounceOut creates bouncing effect with values close to 1
169
+ const val = easings.bounceOut(0.9);
170
+ expect(val).toBeGreaterThan(0.9);
171
+ });
172
+ });
173
+ });
174
+ describe('createAnimationLoop', () => {
175
+ it('should return control functions', () => {
176
+ const loop = createAnimationLoop({
177
+ onFrame: () => { },
178
+ });
179
+ expect(typeof loop.start).toBe('function');
180
+ expect(typeof loop.stop).toBe('function');
181
+ expect(typeof loop.pause).toBe('function');
182
+ expect(typeof loop.resume).toBe('function');
183
+ expect(typeof loop.getState).toBe('function');
184
+ });
185
+ it('should initialize with correct default state', () => {
186
+ const loop = createAnimationLoop({
187
+ fps: 60,
188
+ onFrame: () => { },
189
+ });
190
+ const state = loop.getState();
191
+ expect(state.isRunning).toBe(false);
192
+ expect(state.frameId).toBeNull();
193
+ expect(state.frameInterval).toBeCloseTo(1000 / 60, 1);
194
+ expect(state.elapsedTime).toBe(0);
195
+ expect(state.frameCount).toBe(0);
196
+ });
197
+ it('should use default FPS of 30', () => {
198
+ const loop = createAnimationLoop({
199
+ onFrame: () => { },
200
+ });
201
+ const state = loop.getState();
202
+ expect(state.frameInterval).toBeCloseTo(1000 / 30, 1);
203
+ });
204
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"characters.d.ts","sourceRoot":"","sources":["../src/characters.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAoFvD,CAAC;AAEF;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAE/C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,MAAW,EACxB,OAAO,GAAE,MAAM,EAAO,GACrB,YAAY,CAOd;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAIhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D"}
1
+ {"version":3,"file":"characters.d.ts","sourceRoot":"","sources":["../src/characters.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA2IvD,CAAC;AAEF;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAE/C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,MAAW,EACxB,OAAO,GAAE,MAAM,EAAO,GACrB,YAAY,CAOd;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAIhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -0,0 +1,176 @@
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
+ * Standard character sets for ASCII rendering
9
+ */
10
+ export const CHARACTER_SETS = {
11
+ standard: {
12
+ name: 'Standard',
13
+ description: 'Classic ASCII art character set',
14
+ characters: ' .:-=+*#%@',
15
+ bestFor: ['general', 'images', 'patterns'],
16
+ },
17
+ dense: {
18
+ name: 'Dense',
19
+ description: 'High detail character set with many gradations',
20
+ characters: " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$",
21
+ bestFor: ['detailed images', 'portraits', 'high contrast'],
22
+ },
23
+ minimal: {
24
+ name: 'Minimal',
25
+ description: 'Clean and simple with few characters',
26
+ characters: ' .:*#',
27
+ bestFor: ['backgrounds', 'subtle effects', 'clean aesthetic'],
28
+ },
29
+ grove: {
30
+ name: 'Grove',
31
+ description: 'Organic, soft characters inspired by nature',
32
+ characters: ' ·∙•◦○◉●',
33
+ bestFor: ['organic patterns', 'soft backgrounds', 'nature themes'],
34
+ },
35
+ dots: {
36
+ name: 'Dots',
37
+ description: 'Braille-like dot patterns',
38
+ characters: ' ⋅∘∙●',
39
+ bestFor: ['stipple effects', 'pointillism', 'dotted textures'],
40
+ },
41
+ blocks: {
42
+ name: 'Blocks',
43
+ description: 'Block-based characters for sharp edges',
44
+ characters: ' ░▒▓█',
45
+ bestFor: ['retro effects', 'pixel art', 'bold patterns'],
46
+ },
47
+ lines: {
48
+ name: 'Lines',
49
+ description: 'Line-based characters for directional effects',
50
+ characters: ' -─═╌│┃',
51
+ bestFor: ['rain effects', 'streaks', 'motion blur'],
52
+ },
53
+ stars: {
54
+ name: 'Stars',
55
+ description: 'Star and sparkle characters',
56
+ characters: ' ·✧✦✫✬✯★',
57
+ bestFor: ['sparkle effects', 'night sky', 'magical themes'],
58
+ },
59
+ nature: {
60
+ name: 'Nature',
61
+ description: 'Nature-themed decorative characters',
62
+ characters: ' .~≈∿⌇☘',
63
+ bestFor: ['decorative', 'themed effects', 'organic patterns'],
64
+ },
65
+ weather: {
66
+ name: 'Weather',
67
+ description: 'Weather-related symbols',
68
+ characters: ' ·.:*❄❅❆',
69
+ bestFor: ['snow effects', 'weather simulations', 'seasonal themes'],
70
+ },
71
+ binary: {
72
+ name: 'Binary',
73
+ description: 'Digital-style binary characters',
74
+ characters: ' 01',
75
+ bestFor: ['digital effects', 'matrix style', 'tech themes'],
76
+ },
77
+ math: {
78
+ name: 'Math',
79
+ description: 'Mathematical symbols',
80
+ characters: ' +-×÷=≠≈∞',
81
+ bestFor: ['abstract patterns', 'tech themes', 'decorative'],
82
+ },
83
+ // ==========================================================================
84
+ // GLASS-OPTIMIZED CHARACTER SETS
85
+ // Designed for subtle overlays on Glass components
86
+ // More characters = more visible gradations
87
+ // ==========================================================================
88
+ 'glass-dots': {
89
+ name: 'Glass Dots',
90
+ description: 'Soft dot gradations for glass overlays',
91
+ characters: ' ·∘∙○•●',
92
+ bestFor: ['glass overlays', 'subtle backgrounds', 'mist effects'],
93
+ },
94
+ 'glass-mist': {
95
+ name: 'Glass Mist',
96
+ description: 'Ethereal mist effect for glass',
97
+ characters: ' .·∙•◦○◉●',
98
+ bestFor: ['glass overlays', 'fog effects', 'ambient backgrounds'],
99
+ },
100
+ 'glass-dust': {
101
+ name: 'Glass Dust',
102
+ description: 'Floating dust particles',
103
+ characters: ' ˙·∘°•◦○',
104
+ bestFor: ['glass overlays', 'particle effects', 'light scatter'],
105
+ },
106
+ 'glass-soft': {
107
+ name: 'Glass Soft',
108
+ description: 'Soft block gradations for glass',
109
+ characters: ' ·░▒▓',
110
+ bestFor: ['glass overlays', 'soft gradients', 'frosted effect'],
111
+ },
112
+ 'glass-sparkle': {
113
+ name: 'Glass Sparkle',
114
+ description: 'Subtle sparkles for glass',
115
+ characters: ' ·.✧✦✫★',
116
+ bestFor: ['glass overlays', 'highlight effects', 'magical themes'],
117
+ },
118
+ 'glass-wave': {
119
+ name: 'Glass Wave',
120
+ description: 'Flowing wave patterns for glass',
121
+ characters: ' .~∼≈≋',
122
+ bestFor: ['glass overlays', 'water effects', 'flowing motion'],
123
+ },
124
+ 'glass-organic': {
125
+ name: 'Glass Organic',
126
+ description: 'Natural, organic feel for glass',
127
+ characters: ' .·:;∘○◦•●',
128
+ bestFor: ['glass overlays', 'nature themes', 'grove aesthetic'],
129
+ },
130
+ };
131
+ /**
132
+ * Get a character set by name
133
+ */
134
+ export function getCharacterSet(name) {
135
+ return CHARACTER_SETS[name];
136
+ }
137
+ /**
138
+ * Get just the characters string from a named set
139
+ */
140
+ export function getCharacters(name) {
141
+ return CHARACTER_SETS[name]?.characters || CHARACTER_SETS.standard.characters;
142
+ }
143
+ /**
144
+ * List all available character set names
145
+ */
146
+ export function getCharacterSetNames() {
147
+ return Object.keys(CHARACTER_SETS);
148
+ }
149
+ /**
150
+ * Create a custom character set
151
+ */
152
+ export function createCharacterSet(name, characters, description = '', bestFor = []) {
153
+ return {
154
+ name,
155
+ description,
156
+ characters,
157
+ bestFor,
158
+ };
159
+ }
160
+ /**
161
+ * Validate that a character string is suitable for ASCII rendering
162
+ * (should start with space and have at least 2 characters)
163
+ */
164
+ export function validateCharacterSet(characters) {
165
+ if (characters.length < 2)
166
+ return false;
167
+ if (characters[0] !== ' ')
168
+ return false;
169
+ return true;
170
+ }
171
+ /**
172
+ * Reverse a character set (for inverted brightness mapping)
173
+ */
174
+ export function invertCharacters(characters) {
175
+ return characters.split('').reverse().join('');
176
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tests for character set utilities
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { CHARACTER_SETS, getCharacterSet, getCharacters, getCharacterSetNames, createCharacterSet, validateCharacterSet, invertCharacters, } from './characters';
6
+ describe('CHARACTER_SETS', () => {
7
+ it('should contain standard character set', () => {
8
+ expect(CHARACTER_SETS.standard).toBeDefined();
9
+ expect(CHARACTER_SETS.standard.characters).toBe(' .:-=+*#%@');
10
+ });
11
+ it('should have all expected character sets', () => {
12
+ const expectedSets = [
13
+ 'standard',
14
+ 'dense',
15
+ 'minimal',
16
+ 'grove',
17
+ 'dots',
18
+ 'blocks',
19
+ 'lines',
20
+ 'stars',
21
+ 'nature',
22
+ 'weather',
23
+ 'binary',
24
+ 'math',
25
+ ];
26
+ for (const setName of expectedSets) {
27
+ expect(CHARACTER_SETS[setName]).toBeDefined();
28
+ }
29
+ });
30
+ it('should have all character sets start with space', () => {
31
+ for (const [name, set] of Object.entries(CHARACTER_SETS)) {
32
+ expect(set.characters[0]).toBe(' ');
33
+ }
34
+ });
35
+ });
36
+ describe('getCharacterSet', () => {
37
+ it('should return character set by name', () => {
38
+ const result = getCharacterSet('standard');
39
+ expect(result).toBeDefined();
40
+ expect(result?.name).toBe('Standard');
41
+ expect(result?.characters).toBe(' .:-=+*#%@');
42
+ });
43
+ it('should return undefined for unknown set', () => {
44
+ const result = getCharacterSet('nonexistent');
45
+ expect(result).toBeUndefined();
46
+ });
47
+ });
48
+ describe('getCharacters', () => {
49
+ it('should return characters string for valid set', () => {
50
+ expect(getCharacters('minimal')).toBe(' .:*#');
51
+ expect(getCharacters('blocks')).toBe(' ░▒▓█');
52
+ });
53
+ it('should return standard characters for unknown set', () => {
54
+ expect(getCharacters('nonexistent')).toBe(' .:-=+*#%@');
55
+ });
56
+ });
57
+ describe('getCharacterSetNames', () => {
58
+ it('should return array of all set names', () => {
59
+ const names = getCharacterSetNames();
60
+ expect(Array.isArray(names)).toBe(true);
61
+ expect(names).toContain('standard');
62
+ expect(names).toContain('minimal');
63
+ expect(names).toContain('blocks');
64
+ });
65
+ it('should return same count as CHARACTER_SETS keys', () => {
66
+ const names = getCharacterSetNames();
67
+ expect(names.length).toBe(Object.keys(CHARACTER_SETS).length);
68
+ });
69
+ });
70
+ describe('createCharacterSet', () => {
71
+ it('should create character set with all properties', () => {
72
+ const result = createCharacterSet('custom', ' abc', 'A custom set', ['testing']);
73
+ expect(result.name).toBe('custom');
74
+ expect(result.characters).toBe(' abc');
75
+ expect(result.description).toBe('A custom set');
76
+ expect(result.bestFor).toEqual(['testing']);
77
+ });
78
+ it('should use defaults for optional parameters', () => {
79
+ const result = createCharacterSet('minimal', ' xy');
80
+ expect(result.name).toBe('minimal');
81
+ expect(result.characters).toBe(' xy');
82
+ expect(result.description).toBe('');
83
+ expect(result.bestFor).toEqual([]);
84
+ });
85
+ });
86
+ describe('validateCharacterSet', () => {
87
+ it('should return true for valid character sets', () => {
88
+ expect(validateCharacterSet(' .:-=+*#%@')).toBe(true);
89
+ expect(validateCharacterSet(' ab')).toBe(true);
90
+ expect(validateCharacterSet(' ░▒▓█')).toBe(true);
91
+ });
92
+ it('should return false for too short sets', () => {
93
+ expect(validateCharacterSet('')).toBe(false);
94
+ expect(validateCharacterSet(' ')).toBe(false);
95
+ });
96
+ it('should return false for sets not starting with space', () => {
97
+ expect(validateCharacterSet('abc')).toBe(false);
98
+ expect(validateCharacterSet('.:-=')).toBe(false);
99
+ });
100
+ });
101
+ describe('invertCharacters', () => {
102
+ it('should reverse character string', () => {
103
+ expect(invertCharacters(' abc')).toBe('cba ');
104
+ expect(invertCharacters(' .:-=')).toBe('=-:. ');
105
+ });
106
+ it('should handle single character', () => {
107
+ expect(invertCharacters(' ')).toBe(' ');
108
+ });
109
+ it('should be reversible', () => {
110
+ const original = ' .:-=+*#%@';
111
+ const inverted = invertCharacters(original);
112
+ const restored = invertCharacters(inverted);
113
+ expect(restored).toBe(original);
114
+ });
115
+ });