@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.
- package/dist/animation.d.ts +80 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/characters.d.ts +49 -0
- package/dist/characters.d.ts.map +1 -0
- package/dist/index.d.ts +37 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1284 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns.d.ts +100 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/renderer.d.ts +113 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/style.css +124 -0
- package/dist/svelte/GossamerBorder.svelte.d.ts +1 -0
- package/dist/svelte/GossamerClouds.svelte.d.ts +1 -0
- package/dist/svelte/GossamerImage.svelte.d.ts +1 -0
- package/dist/svelte/GossamerOverlay.svelte.d.ts +1 -0
- package/dist/svelte/GossamerText.svelte.d.ts +1 -0
- package/dist/svelte/index.d.ts +20 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3651 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/svelte/presets.d.ts +38 -0
- package/dist/svelte/presets.d.ts.map +1 -0
- package/dist/utils/canvas.d.ts +73 -0
- package/dist/utils/canvas.d.ts.map +1 -0
- package/dist/utils/image.d.ts +74 -0
- package/dist/utils/image.d.ts.map +1 -0
- package/dist/utils/performance.d.ts +86 -0
- package/dist/utils/performance.d.ts.map +1 -0
- package/package.json +23 -5
- package/src/animation.test.ts +254 -0
- package/src/animation.ts +243 -0
- package/src/characters.test.ts +148 -0
- package/src/characters.ts +164 -0
- package/src/index.test.ts +115 -0
- package/src/index.ts +133 -11
- package/src/patterns.test.ts +273 -0
- package/src/patterns.ts +316 -0
- package/src/renderer.ts +309 -0
- package/src/svelte/GossamerBorder.svelte +326 -0
- package/src/svelte/GossamerClouds.svelte +269 -0
- package/src/svelte/GossamerImage.svelte +266 -0
- package/src/svelte/GossamerOverlay.svelte +232 -0
- package/src/svelte/GossamerText.svelte +239 -0
- package/src/svelte/index.ts +75 -0
- package/src/svelte/presets.ts +174 -0
- package/src/utils/canvas.ts +210 -0
- package/src/utils/image.ts +275 -0
- package/src/utils/performance.ts +282 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { PatternType } from '../index';
|
|
3
|
+
|
|
4
|
+
export interface GossamerTextProps {
|
|
5
|
+
/** Text content to display */
|
|
6
|
+
text: string;
|
|
7
|
+
/** Character set for effect (light to dark) */
|
|
8
|
+
characters?: string;
|
|
9
|
+
/** Text color */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Font size in pixels */
|
|
12
|
+
fontSize?: number;
|
|
13
|
+
/** Font family */
|
|
14
|
+
fontFamily?: string;
|
|
15
|
+
/** Enable animation effect */
|
|
16
|
+
animated?: boolean;
|
|
17
|
+
/** Animation pattern */
|
|
18
|
+
pattern?: PatternType;
|
|
19
|
+
/** Animation speed */
|
|
20
|
+
speed?: number;
|
|
21
|
+
/** Effect intensity */
|
|
22
|
+
intensity?: number;
|
|
23
|
+
/** Target FPS */
|
|
24
|
+
fps?: number;
|
|
25
|
+
/** Additional CSS class */
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { onMount } from 'svelte';
|
|
32
|
+
import {
|
|
33
|
+
perlinNoise2D,
|
|
34
|
+
createVisibilityObserver,
|
|
35
|
+
onReducedMotionChange,
|
|
36
|
+
CHARACTER_SETS,
|
|
37
|
+
} from '../index';
|
|
38
|
+
|
|
39
|
+
// Props with defaults
|
|
40
|
+
let {
|
|
41
|
+
text,
|
|
42
|
+
characters = CHARACTER_SETS.minimal.characters,
|
|
43
|
+
color = 'currentColor',
|
|
44
|
+
fontSize = 48,
|
|
45
|
+
fontFamily = 'monospace',
|
|
46
|
+
animated = false,
|
|
47
|
+
pattern = 'perlin',
|
|
48
|
+
speed = 0.5,
|
|
49
|
+
intensity = 0.3,
|
|
50
|
+
fps = 30,
|
|
51
|
+
class: className = '',
|
|
52
|
+
}: GossamerTextProps = $props();
|
|
53
|
+
|
|
54
|
+
// State
|
|
55
|
+
let canvas: HTMLCanvasElement;
|
|
56
|
+
let container: HTMLDivElement;
|
|
57
|
+
let isVisible = true;
|
|
58
|
+
let reducedMotion = false;
|
|
59
|
+
let animationId: number | null = null;
|
|
60
|
+
let textMetrics: { width: number; height: number } = { width: 0, height: 0 };
|
|
61
|
+
|
|
62
|
+
const shouldAnimate = $derived(animated && isVisible && !reducedMotion);
|
|
63
|
+
|
|
64
|
+
function measureText(): void {
|
|
65
|
+
if (!canvas) return;
|
|
66
|
+
|
|
67
|
+
const ctx = canvas.getContext('2d');
|
|
68
|
+
if (!ctx) return;
|
|
69
|
+
|
|
70
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
71
|
+
const metrics = ctx.measureText(text);
|
|
72
|
+
|
|
73
|
+
textMetrics = {
|
|
74
|
+
width: Math.ceil(metrics.width) + 20,
|
|
75
|
+
height: fontSize + 20,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
canvas.width = textMetrics.width;
|
|
79
|
+
canvas.height = textMetrics.height;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderText(time: number = 0): void {
|
|
83
|
+
if (!canvas) return;
|
|
84
|
+
|
|
85
|
+
const ctx = canvas.getContext('2d');
|
|
86
|
+
if (!ctx) return;
|
|
87
|
+
|
|
88
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
89
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
90
|
+
ctx.textBaseline = 'top';
|
|
91
|
+
ctx.fillStyle = color;
|
|
92
|
+
|
|
93
|
+
// Render each character with potential effect
|
|
94
|
+
let x = 10;
|
|
95
|
+
const y = 10;
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < text.length; i++) {
|
|
98
|
+
const char = text[i];
|
|
99
|
+
const charWidth = ctx.measureText(char).width;
|
|
100
|
+
|
|
101
|
+
if (animated && time > 0) {
|
|
102
|
+
// Apply noise-based effect to character
|
|
103
|
+
const noise = perlinNoise2D(
|
|
104
|
+
i * 0.5 + time * speed * 0.001,
|
|
105
|
+
time * speed * 0.0005
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Slight position offset based on noise
|
|
109
|
+
const offsetY = noise * intensity * 5;
|
|
110
|
+
|
|
111
|
+
// Slight opacity variation
|
|
112
|
+
ctx.globalAlpha = 0.7 + (noise + 1) * 0.15;
|
|
113
|
+
|
|
114
|
+
ctx.fillText(char, x, y + offsetY);
|
|
115
|
+
ctx.globalAlpha = 1;
|
|
116
|
+
} else {
|
|
117
|
+
ctx.fillText(char, x, y);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
x += charWidth;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let startTime = 0;
|
|
125
|
+
|
|
126
|
+
function animate(currentTime: number): void {
|
|
127
|
+
if (!shouldAnimate) return;
|
|
128
|
+
|
|
129
|
+
const elapsed = currentTime - startTime;
|
|
130
|
+
renderText(elapsed);
|
|
131
|
+
animationId = requestAnimationFrame(animate);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function startAnimation(): void {
|
|
135
|
+
if (animationId !== null) return;
|
|
136
|
+
startTime = performance.now();
|
|
137
|
+
animationId = requestAnimationFrame(animate);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stopAnimation(): void {
|
|
141
|
+
if (animationId !== null) {
|
|
142
|
+
cancelAnimationFrame(animationId);
|
|
143
|
+
animationId = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Lifecycle
|
|
148
|
+
onMount(() => {
|
|
149
|
+
measureText();
|
|
150
|
+
renderText();
|
|
151
|
+
|
|
152
|
+
const cleanupMotion = onReducedMotionChange((prefers) => {
|
|
153
|
+
reducedMotion = prefers;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const cleanupVisibility = createVisibilityObserver(
|
|
157
|
+
container,
|
|
158
|
+
(visible) => {
|
|
159
|
+
isVisible = visible;
|
|
160
|
+
if (visible && shouldAnimate) {
|
|
161
|
+
startAnimation();
|
|
162
|
+
} else {
|
|
163
|
+
stopAnimation();
|
|
164
|
+
renderText();
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
0.1
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (shouldAnimate) {
|
|
171
|
+
startAnimation();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
cleanupMotion();
|
|
176
|
+
cleanupVisibility();
|
|
177
|
+
stopAnimation();
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// React to text changes
|
|
182
|
+
$effect(() => {
|
|
183
|
+
if (text) {
|
|
184
|
+
measureText();
|
|
185
|
+
if (shouldAnimate) {
|
|
186
|
+
// Animation will handle rendering
|
|
187
|
+
} else {
|
|
188
|
+
renderText();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// React to animation state changes
|
|
194
|
+
$effect(() => {
|
|
195
|
+
if (shouldAnimate) {
|
|
196
|
+
startAnimation();
|
|
197
|
+
} else {
|
|
198
|
+
stopAnimation();
|
|
199
|
+
renderText();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<div
|
|
205
|
+
bind:this={container}
|
|
206
|
+
class="gossamer-text {className}"
|
|
207
|
+
style:width={textMetrics.width ? `${textMetrics.width}px` : 'auto'}
|
|
208
|
+
style:height={textMetrics.height ? `${textMetrics.height}px` : 'auto'}
|
|
209
|
+
>
|
|
210
|
+
<canvas
|
|
211
|
+
bind:this={canvas}
|
|
212
|
+
aria-hidden="true"
|
|
213
|
+
class="gossamer-canvas"
|
|
214
|
+
></canvas>
|
|
215
|
+
<span class="gossamer-text-sr">{text}</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<style>
|
|
219
|
+
.gossamer-text {
|
|
220
|
+
position: relative;
|
|
221
|
+
display: inline-block;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.gossamer-canvas {
|
|
225
|
+
display: block;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.gossamer-text-sr {
|
|
229
|
+
position: absolute;
|
|
230
|
+
width: 1px;
|
|
231
|
+
height: 1px;
|
|
232
|
+
padding: 0;
|
|
233
|
+
margin: -1px;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
clip: rect(0, 0, 0, 0);
|
|
236
|
+
white-space: nowrap;
|
|
237
|
+
border: 0;
|
|
238
|
+
}
|
|
239
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gossamer - Svelte 5 Components
|
|
3
|
+
*
|
|
4
|
+
* ASCII visual effects components for Svelte applications.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Components
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export { default as GossamerClouds } from './GossamerClouds.svelte';
|
|
14
|
+
export { default as GossamerImage } from './GossamerImage.svelte';
|
|
15
|
+
export { default as GossamerText } from './GossamerText.svelte';
|
|
16
|
+
export { default as GossamerOverlay } from './GossamerOverlay.svelte';
|
|
17
|
+
export { default as GossamerBorder } from './GossamerBorder.svelte';
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Component Types
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export type { GossamerCloudsProps } from './GossamerClouds.svelte';
|
|
24
|
+
export type { GossamerImageProps } from './GossamerImage.svelte';
|
|
25
|
+
export type { GossamerTextProps } from './GossamerText.svelte';
|
|
26
|
+
export type { GossamerOverlayProps, BlendMode } from './GossamerOverlay.svelte';
|
|
27
|
+
export type { GossamerBorderProps, BorderStyle } from './GossamerBorder.svelte';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Presets
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
PRESETS,
|
|
35
|
+
grovePresets,
|
|
36
|
+
seasonalPresets,
|
|
37
|
+
ambientPresets,
|
|
38
|
+
getPreset,
|
|
39
|
+
getPresetNames,
|
|
40
|
+
getPresetsByCategory,
|
|
41
|
+
} from './presets';
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Re-exports from Core
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
// Core types
|
|
49
|
+
type GossamerConfig,
|
|
50
|
+
type PresetConfig,
|
|
51
|
+
type PatternConfig,
|
|
52
|
+
type PatternType,
|
|
53
|
+
type CharacterSet,
|
|
54
|
+
|
|
55
|
+
// Constants
|
|
56
|
+
DEFAULT_CHARACTERS,
|
|
57
|
+
DEFAULT_CONFIG,
|
|
58
|
+
CHARACTER_SETS,
|
|
59
|
+
|
|
60
|
+
// Core functions
|
|
61
|
+
calculateBrightness,
|
|
62
|
+
brightnessToChar,
|
|
63
|
+
|
|
64
|
+
// Character utilities
|
|
65
|
+
getCharacterSet,
|
|
66
|
+
getCharacters,
|
|
67
|
+
getCharacterSetNames,
|
|
68
|
+
invertCharacters,
|
|
69
|
+
|
|
70
|
+
// Performance utilities
|
|
71
|
+
prefersReducedMotion,
|
|
72
|
+
|
|
73
|
+
// Version
|
|
74
|
+
VERSION,
|
|
75
|
+
} from '../index';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset configurations for Gossamer effects
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PresetConfig } from '../index';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Grove-themed presets
|
|
9
|
+
* Organic, nature-inspired effects
|
|
10
|
+
*/
|
|
11
|
+
export const grovePresets: Record<string, PresetConfig> = {
|
|
12
|
+
'grove-mist': {
|
|
13
|
+
name: 'Grove Mist',
|
|
14
|
+
description: 'Soft fog effect drifting through the trees',
|
|
15
|
+
characters: ' ·∙•◦',
|
|
16
|
+
pattern: 'perlin',
|
|
17
|
+
frequency: 0.03,
|
|
18
|
+
amplitude: 0.8,
|
|
19
|
+
speed: 0.3,
|
|
20
|
+
opacity: 0.2,
|
|
21
|
+
},
|
|
22
|
+
'grove-fireflies': {
|
|
23
|
+
name: 'Grove Fireflies',
|
|
24
|
+
description: 'Twinkling points of light in the darkness',
|
|
25
|
+
characters: ' ·*✦✧',
|
|
26
|
+
pattern: 'static',
|
|
27
|
+
frequency: 0.01,
|
|
28
|
+
amplitude: 1.2,
|
|
29
|
+
speed: 0.8,
|
|
30
|
+
opacity: 0.3,
|
|
31
|
+
},
|
|
32
|
+
'grove-rain': {
|
|
33
|
+
name: 'Grove Rain',
|
|
34
|
+
description: 'Gentle rain falling through the canopy',
|
|
35
|
+
characters: ' │\\|/',
|
|
36
|
+
pattern: 'waves',
|
|
37
|
+
frequency: 0.05,
|
|
38
|
+
amplitude: 1.0,
|
|
39
|
+
speed: 1.5,
|
|
40
|
+
opacity: 0.15,
|
|
41
|
+
},
|
|
42
|
+
'grove-dew': {
|
|
43
|
+
name: 'Grove Dew',
|
|
44
|
+
description: 'Morning dew glistening on spider silk',
|
|
45
|
+
characters: ' ·∘∙●',
|
|
46
|
+
pattern: 'fbm',
|
|
47
|
+
frequency: 0.04,
|
|
48
|
+
amplitude: 0.7,
|
|
49
|
+
speed: 0.1,
|
|
50
|
+
opacity: 0.15,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Seasonal presets
|
|
56
|
+
* Effects themed around the four seasons
|
|
57
|
+
*/
|
|
58
|
+
export const seasonalPresets: Record<string, PresetConfig> = {
|
|
59
|
+
'winter-snow': {
|
|
60
|
+
name: 'Winter Snow',
|
|
61
|
+
description: 'Gentle snowfall on a quiet night',
|
|
62
|
+
characters: ' ·∙*❄',
|
|
63
|
+
pattern: 'perlin',
|
|
64
|
+
frequency: 0.04,
|
|
65
|
+
amplitude: 0.9,
|
|
66
|
+
speed: 0.5,
|
|
67
|
+
opacity: 0.25,
|
|
68
|
+
},
|
|
69
|
+
'autumn-leaves': {
|
|
70
|
+
name: 'Autumn Leaves',
|
|
71
|
+
description: 'Scattered leaves drifting on the wind',
|
|
72
|
+
characters: ' 🍂·∙',
|
|
73
|
+
pattern: 'perlin',
|
|
74
|
+
frequency: 0.06,
|
|
75
|
+
amplitude: 1.1,
|
|
76
|
+
speed: 0.4,
|
|
77
|
+
opacity: 0.2,
|
|
78
|
+
},
|
|
79
|
+
'spring-petals': {
|
|
80
|
+
name: 'Spring Petals',
|
|
81
|
+
description: 'Cherry blossom petals floating on the breeze',
|
|
82
|
+
characters: ' ·✿❀',
|
|
83
|
+
pattern: 'waves',
|
|
84
|
+
frequency: 0.05,
|
|
85
|
+
amplitude: 0.8,
|
|
86
|
+
speed: 0.6,
|
|
87
|
+
opacity: 0.2,
|
|
88
|
+
},
|
|
89
|
+
'summer-heat': {
|
|
90
|
+
name: 'Summer Heat',
|
|
91
|
+
description: 'Heat shimmer rising from sun-warmed ground',
|
|
92
|
+
characters: ' ~≈∿',
|
|
93
|
+
pattern: 'waves',
|
|
94
|
+
frequency: 0.08,
|
|
95
|
+
amplitude: 1.3,
|
|
96
|
+
speed: 1.0,
|
|
97
|
+
opacity: 0.1,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Ambient presets
|
|
103
|
+
* Subtle background textures
|
|
104
|
+
*/
|
|
105
|
+
export const ambientPresets: Record<string, PresetConfig> = {
|
|
106
|
+
'ambient-static': {
|
|
107
|
+
name: 'Ambient Static',
|
|
108
|
+
description: 'Gentle static noise texture',
|
|
109
|
+
characters: ' .:',
|
|
110
|
+
pattern: 'static',
|
|
111
|
+
frequency: 0.1,
|
|
112
|
+
amplitude: 0.5,
|
|
113
|
+
speed: 0.2,
|
|
114
|
+
opacity: 0.08,
|
|
115
|
+
},
|
|
116
|
+
'ambient-waves': {
|
|
117
|
+
name: 'Ambient Waves',
|
|
118
|
+
description: 'Soft flowing wave pattern',
|
|
119
|
+
characters: ' ·~',
|
|
120
|
+
pattern: 'waves',
|
|
121
|
+
frequency: 0.02,
|
|
122
|
+
amplitude: 0.6,
|
|
123
|
+
speed: 0.3,
|
|
124
|
+
opacity: 0.1,
|
|
125
|
+
},
|
|
126
|
+
'ambient-clouds': {
|
|
127
|
+
name: 'Ambient Clouds',
|
|
128
|
+
description: 'Drifting cloud-like patterns',
|
|
129
|
+
characters: ' .:-',
|
|
130
|
+
pattern: 'fbm',
|
|
131
|
+
frequency: 0.02,
|
|
132
|
+
amplitude: 0.7,
|
|
133
|
+
speed: 0.15,
|
|
134
|
+
opacity: 0.12,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* All presets combined for easy access
|
|
140
|
+
*/
|
|
141
|
+
export const PRESETS: Record<string, PresetConfig> = {
|
|
142
|
+
...grovePresets,
|
|
143
|
+
...seasonalPresets,
|
|
144
|
+
...ambientPresets,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get a preset by name
|
|
149
|
+
*/
|
|
150
|
+
export function getPreset(name: string): PresetConfig | undefined {
|
|
151
|
+
return PRESETS[name];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* List all available preset names
|
|
156
|
+
*/
|
|
157
|
+
export function getPresetNames(): string[] {
|
|
158
|
+
return Object.keys(PRESETS);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* List preset names by category
|
|
163
|
+
*/
|
|
164
|
+
export function getPresetsByCategory(): {
|
|
165
|
+
grove: string[];
|
|
166
|
+
seasonal: string[];
|
|
167
|
+
ambient: string[];
|
|
168
|
+
} {
|
|
169
|
+
return {
|
|
170
|
+
grove: Object.keys(grovePresets),
|
|
171
|
+
seasonal: Object.keys(seasonalPresets),
|
|
172
|
+
ambient: Object.keys(ambientPresets),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for canvas creation, setup, and manipulation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for canvas creation
|
|
9
|
+
*/
|
|
10
|
+
export interface CanvasOptions {
|
|
11
|
+
/** Canvas width in pixels */
|
|
12
|
+
width?: number;
|
|
13
|
+
/** Canvas height in pixels */
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Whether to use high DPI scaling */
|
|
16
|
+
highDPI?: boolean;
|
|
17
|
+
/** CSS class to add to canvas */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Inline styles to apply */
|
|
20
|
+
style?: Partial<CSSStyleDeclaration>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a canvas element with optimal settings
|
|
25
|
+
*/
|
|
26
|
+
export function createCanvas(options: CanvasOptions = {}): HTMLCanvasElement {
|
|
27
|
+
const { width = 300, height = 150, highDPI = true, className, style } = options;
|
|
28
|
+
|
|
29
|
+
const canvas = document.createElement('canvas');
|
|
30
|
+
|
|
31
|
+
if (highDPI) {
|
|
32
|
+
const dpr = window.devicePixelRatio || 1;
|
|
33
|
+
canvas.width = width * dpr;
|
|
34
|
+
canvas.height = height * dpr;
|
|
35
|
+
canvas.style.width = `${width}px`;
|
|
36
|
+
canvas.style.height = `${height}px`;
|
|
37
|
+
|
|
38
|
+
const ctx = canvas.getContext('2d');
|
|
39
|
+
if (ctx) {
|
|
40
|
+
ctx.scale(dpr, dpr);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
canvas.width = width;
|
|
44
|
+
canvas.height = height;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (className) {
|
|
48
|
+
canvas.className = className;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (style) {
|
|
52
|
+
Object.assign(canvas.style, style);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return canvas;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the device pixel ratio
|
|
60
|
+
*/
|
|
61
|
+
export function getDevicePixelRatio(): number {
|
|
62
|
+
return typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resize canvas to match container dimensions
|
|
67
|
+
*/
|
|
68
|
+
export function resizeCanvasToContainer(
|
|
69
|
+
canvas: HTMLCanvasElement,
|
|
70
|
+
container: HTMLElement,
|
|
71
|
+
highDPI: boolean = true
|
|
72
|
+
): { width: number; height: number } {
|
|
73
|
+
const rect = container.getBoundingClientRect();
|
|
74
|
+
const dpr = highDPI ? getDevicePixelRatio() : 1;
|
|
75
|
+
|
|
76
|
+
const width = rect.width;
|
|
77
|
+
const height = rect.height;
|
|
78
|
+
|
|
79
|
+
canvas.width = width * dpr;
|
|
80
|
+
canvas.height = height * dpr;
|
|
81
|
+
canvas.style.width = `${width}px`;
|
|
82
|
+
canvas.style.height = `${height}px`;
|
|
83
|
+
|
|
84
|
+
if (highDPI) {
|
|
85
|
+
const ctx = canvas.getContext('2d');
|
|
86
|
+
if (ctx) {
|
|
87
|
+
ctx.scale(dpr, dpr);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { width, height };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create an offscreen canvas for buffer rendering
|
|
96
|
+
*/
|
|
97
|
+
export function createOffscreenCanvas(width: number, height: number): HTMLCanvasElement | OffscreenCanvas {
|
|
98
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
99
|
+
return new OffscreenCanvas(width, height);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fallback for environments without OffscreenCanvas
|
|
103
|
+
const canvas = document.createElement('canvas');
|
|
104
|
+
canvas.width = width;
|
|
105
|
+
canvas.height = height;
|
|
106
|
+
return canvas;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear a canvas
|
|
111
|
+
*/
|
|
112
|
+
export function clearCanvas(
|
|
113
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
|
114
|
+
width: number,
|
|
115
|
+
height: number,
|
|
116
|
+
backgroundColor?: string
|
|
117
|
+
): void {
|
|
118
|
+
if (backgroundColor) {
|
|
119
|
+
ctx.fillStyle = backgroundColor;
|
|
120
|
+
ctx.fillRect(0, 0, width, height);
|
|
121
|
+
} else {
|
|
122
|
+
ctx.clearRect(0, 0, width, height);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get image data from a canvas region
|
|
128
|
+
*/
|
|
129
|
+
export function getImageData(
|
|
130
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
|
131
|
+
x: number = 0,
|
|
132
|
+
y: number = 0,
|
|
133
|
+
width?: number,
|
|
134
|
+
height?: number
|
|
135
|
+
): ImageData {
|
|
136
|
+
const canvas = ctx.canvas;
|
|
137
|
+
const w = width ?? canvas.width;
|
|
138
|
+
const h = height ?? canvas.height;
|
|
139
|
+
|
|
140
|
+
return ctx.getImageData(x, y, w, h);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set optimal rendering context settings
|
|
145
|
+
*/
|
|
146
|
+
export function optimizeContext(ctx: CanvasRenderingContext2D): void {
|
|
147
|
+
// Disable image smoothing for crisp ASCII rendering
|
|
148
|
+
ctx.imageSmoothingEnabled = false;
|
|
149
|
+
|
|
150
|
+
// Use source-over for standard compositing
|
|
151
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set text rendering options for ASCII display
|
|
156
|
+
*/
|
|
157
|
+
export function setupTextRendering(
|
|
158
|
+
ctx: CanvasRenderingContext2D,
|
|
159
|
+
fontSize: number,
|
|
160
|
+
fontFamily: string = 'monospace',
|
|
161
|
+
color: string = '#ffffff'
|
|
162
|
+
): void {
|
|
163
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
164
|
+
ctx.textBaseline = 'top';
|
|
165
|
+
ctx.textAlign = 'left';
|
|
166
|
+
ctx.fillStyle = color;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Measure text width for a given font configuration
|
|
171
|
+
*/
|
|
172
|
+
export function measureTextWidth(
|
|
173
|
+
ctx: CanvasRenderingContext2D,
|
|
174
|
+
text: string,
|
|
175
|
+
fontSize: number,
|
|
176
|
+
fontFamily: string = 'monospace'
|
|
177
|
+
): number {
|
|
178
|
+
const originalFont = ctx.font;
|
|
179
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
180
|
+
const metrics = ctx.measureText(text);
|
|
181
|
+
ctx.font = originalFont;
|
|
182
|
+
return metrics.width;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Calculate optimal cell size for a given canvas and desired columns
|
|
187
|
+
*/
|
|
188
|
+
export function calculateCellSize(
|
|
189
|
+
canvasWidth: number,
|
|
190
|
+
canvasHeight: number,
|
|
191
|
+
targetCols: number
|
|
192
|
+
): { cellWidth: number; cellHeight: number; cols: number; rows: number } {
|
|
193
|
+
const cellWidth = Math.floor(canvasWidth / targetCols);
|
|
194
|
+
// Use a typical monospace aspect ratio of ~0.6
|
|
195
|
+
const cellHeight = Math.floor(cellWidth * 1.5);
|
|
196
|
+
const cols = Math.floor(canvasWidth / cellWidth);
|
|
197
|
+
const rows = Math.floor(canvasHeight / cellHeight);
|
|
198
|
+
|
|
199
|
+
return { cellWidth, cellHeight, cols, rows };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Apply a CSS blend mode to canvas compositing
|
|
204
|
+
*/
|
|
205
|
+
export function setBlendMode(
|
|
206
|
+
ctx: CanvasRenderingContext2D,
|
|
207
|
+
mode: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'soft-light' | 'hard-light' | 'difference' | 'exclusion'
|
|
208
|
+
): void {
|
|
209
|
+
ctx.globalCompositeOperation = mode === 'normal' ? 'source-over' : mode;
|
|
210
|
+
}
|