@asciirender/asciir 1.0.2
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/README.md +313 -0
- package/dist/asciir.js +387 -0
- package/dist/asciir.umd.cjs +4 -0
- package/dist/index.d.ts +240 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinay
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# ASCIIR
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://img.shields.io/npm/v/asciir?color=blue&label=npm" alt="npm version" />
|
|
5
|
+
<img src="https://img.shields.io/npm/dm/asciir?color=green" alt="npm downloads" />
|
|
6
|
+
<img src="https://img.shields.io/bundlephobia/minzip/asciir?color=orange" alt="bundle size" />
|
|
7
|
+
<img src="https://img.shields.io/github/license/YOUR_USERNAME/asciir?color=purple" alt="license" />
|
|
8
|
+
<img src="https://img.shields.io/npm/types/asciir?color=blue" alt="TypeScript" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<strong>A powerful React component for converting images to beautiful ASCII art</strong>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#installation">Installation</a> •
|
|
17
|
+
<a href="#quick-start">Quick Start</a> •
|
|
18
|
+
<a href="#api-reference">API</a> •
|
|
19
|
+
<a href="#examples">Examples</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Features
|
|
25
|
+
|
|
26
|
+
- 🎨 **Multiple Color Modes** - Monochrome, Original colors, or custom palettes
|
|
27
|
+
- 📝 **Custom Character Sets** - Use preset or custom character ramps
|
|
28
|
+
- 🎛️ **Image Filters** - Brightness, contrast, and saturation controls
|
|
29
|
+
- 🔲 **Dithering** - Floyd-Steinberg dithering for better gradients
|
|
30
|
+
- 📦 **Multiple Output Formats** - HTML, SVG, or Canvas
|
|
31
|
+
- 💾 **Export Options** - Download as PNG, SVG, or TXT
|
|
32
|
+
- 🎯 **TypeScript** - Full type definitions included
|
|
33
|
+
- ⚡ **Lightweight** - ~5KB gzipped, zero runtime dependencies
|
|
34
|
+
|
|
35
|
+
## 📦 Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install asciir
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
yarn add asciir
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm add asciir
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 🚀 Quick Start
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { ASCIIR } from 'asciir';
|
|
53
|
+
|
|
54
|
+
function App() {
|
|
55
|
+
return (
|
|
56
|
+
<ASCIIR
|
|
57
|
+
src="/path/to/image.jpg"
|
|
58
|
+
config={{
|
|
59
|
+
resolutionWidth: 100,
|
|
60
|
+
colorMode: 'original'
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 📖 API Reference
|
|
68
|
+
|
|
69
|
+
### `<ASCIIR />` Component
|
|
70
|
+
|
|
71
|
+
The main component for rendering ASCII art.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { ASCIIR, useASCIIRender } from 'asciir';
|
|
75
|
+
|
|
76
|
+
function App() {
|
|
77
|
+
const asciiRef = useASCIIRender();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<ASCIIR
|
|
82
|
+
ref={asciiRef}
|
|
83
|
+
src={imageSource}
|
|
84
|
+
config={config}
|
|
85
|
+
output="html"
|
|
86
|
+
onGenerate={(result) => console.log(result)}
|
|
87
|
+
onError={(error) => console.error(error)}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<button onClick={() => asciiRef.current?.downloadPNG()}>
|
|
91
|
+
Download PNG
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Props
|
|
99
|
+
|
|
100
|
+
| Prop | Type | Default | Description |
|
|
101
|
+
|------|------|---------|-------------|
|
|
102
|
+
| `src` | `string \| File \| Blob \| HTMLImageElement` | Required | Image source |
|
|
103
|
+
| `config` | `Partial<ASCIIRenderConfig>` | `{}` | Configuration overrides |
|
|
104
|
+
| `output` | `'html' \| 'svg' \| 'canvas'` | `'html'` | Output format |
|
|
105
|
+
| `onGenerate` | `(result: ProcessingResult) => void` | - | Callback when ASCII is generated |
|
|
106
|
+
| `onError` | `(error: Error) => void` | - | Error callback |
|
|
107
|
+
| `className` | `string` | - | Container CSS class |
|
|
108
|
+
| `style` | `CSSProperties` | - | Container inline styles |
|
|
109
|
+
| `autoFit` | `boolean` | `false` | Auto-fit to container width |
|
|
110
|
+
|
|
111
|
+
### Configuration Options
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
interface ASCIIRenderConfig {
|
|
115
|
+
resolutionWidth: number; // Width in characters (default: 150)
|
|
116
|
+
characterSet: string; // Character ramp (default: " .:-=+*#%@")
|
|
117
|
+
inverted: boolean; // Invert character ramp (default: false)
|
|
118
|
+
contrastStretch: boolean; // Auto contrast (default: true)
|
|
119
|
+
fontColor: string; // Text color for mono mode (default: "#FFFFFF")
|
|
120
|
+
backgroundColor: string; // Background color (default: "#242424")
|
|
121
|
+
lineHeight: number; // Line height multiplier (default: 1.0)
|
|
122
|
+
scaleRatio: number; // Vertical scale ratio (default: 0.55)
|
|
123
|
+
fontSize: number; // Font size in px (default: 12)
|
|
124
|
+
transparentBackground: boolean; // Transparent bg (default: false)
|
|
125
|
+
dithering: boolean; // Enable dithering (default: false)
|
|
126
|
+
colorMode: 'mono' | 'original' | 'palette'; // Color mode (default: 'mono')
|
|
127
|
+
fontFamily: string; // Font family (default: "'Fira Code', monospace")
|
|
128
|
+
exportScale: number; // PNG export scale (default: 2)
|
|
129
|
+
fillTransparency: boolean; // Fill transparent areas (default: true)
|
|
130
|
+
brightness: number; // Brightness 0-3 (default: 1.0)
|
|
131
|
+
contrast: number; // Contrast 0-3 (default: 1.0)
|
|
132
|
+
saturation: number; // Saturation 0-3 (default: 1.0)
|
|
133
|
+
colorPalette: string[]; // Color palette for palette mode
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Ref Methods
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
interface ASCIIRenderRef {
|
|
141
|
+
getResult(): ProcessingResult | null;
|
|
142
|
+
downloadPNG(filename?: string): void;
|
|
143
|
+
downloadSVG(filename?: string): void;
|
|
144
|
+
downloadTXT(filename?: string): void;
|
|
145
|
+
copyToClipboard(): Promise<void>;
|
|
146
|
+
getCanvas(scale?: number): HTMLCanvasElement | null;
|
|
147
|
+
getSVGString(): string | null;
|
|
148
|
+
refresh(): void;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Presets
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import {
|
|
156
|
+
CHAR_SETS, // Character set presets
|
|
157
|
+
FONTS, // Font presets
|
|
158
|
+
PALETTE_PRESETS // Color palette presets
|
|
159
|
+
} from 'asciir';
|
|
160
|
+
|
|
161
|
+
// Character sets
|
|
162
|
+
CHAR_SETS.default // " .:-=+*#%@"
|
|
163
|
+
CHAR_SETS.standard_short // "@%#*+=-:. "
|
|
164
|
+
CHAR_SETS.blocks // "█▓▒░ "
|
|
165
|
+
CHAR_SETS.binary // "01 "
|
|
166
|
+
CHAR_SETS.matrix // Matrix/Katakana style
|
|
167
|
+
|
|
168
|
+
// Fonts
|
|
169
|
+
FONTS.fira // 'Fira Code', monospace
|
|
170
|
+
FONTS.vt323 // 'VT323', monospace (retro)
|
|
171
|
+
FONTS.roboto // 'Roboto Mono', monospace
|
|
172
|
+
|
|
173
|
+
// Color palettes
|
|
174
|
+
PALETTE_PRESETS.cga // CGA colors
|
|
175
|
+
PALETTE_PRESETS.gameboy // GameBoy green
|
|
176
|
+
PALETTE_PRESETS.vaporwave // Vaporwave aesthetic
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Utility Functions
|
|
180
|
+
|
|
181
|
+
For advanced usage, you can use the core functions directly:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import {
|
|
185
|
+
generateAscii,
|
|
186
|
+
generateCanvasFromAscii,
|
|
187
|
+
generateSVG,
|
|
188
|
+
generateHTML,
|
|
189
|
+
loadImage
|
|
190
|
+
} from 'asciir';
|
|
191
|
+
|
|
192
|
+
// Load and process an image
|
|
193
|
+
const img = await loadImage('/path/to/image.jpg');
|
|
194
|
+
const result = generateAscii(img, config);
|
|
195
|
+
|
|
196
|
+
// Generate outputs
|
|
197
|
+
const svg = generateSVG(result, config);
|
|
198
|
+
const html = generateHTML(result, config);
|
|
199
|
+
const canvas = generateCanvasFromAscii(result, config, 2);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 📋 Examples
|
|
203
|
+
|
|
204
|
+
### Basic Usage
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<ASCIIR src="/image.jpg" />
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Colored ASCII Art
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<ASCIIR
|
|
214
|
+
src="/image.jpg"
|
|
215
|
+
config={{
|
|
216
|
+
colorMode: 'original',
|
|
217
|
+
resolutionWidth: 120
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Custom Palette
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { ASCIIR, PALETTE_PRESETS } from 'asciir';
|
|
226
|
+
|
|
227
|
+
<ASCIIR
|
|
228
|
+
src="/image.jpg"
|
|
229
|
+
config={{
|
|
230
|
+
colorMode: 'palette',
|
|
231
|
+
colorPalette: PALETTE_PRESETS.vaporwave
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### With Downloads
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { ASCIIR, useASCIIRender } from 'asciir';
|
|
240
|
+
|
|
241
|
+
function App() {
|
|
242
|
+
const ref = useASCIIRender();
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<>
|
|
246
|
+
<ASCIIR ref={ref} src="/image.jpg" />
|
|
247
|
+
<button onClick={() => ref.current?.downloadPNG('my-art.png')}>
|
|
248
|
+
Save as PNG
|
|
249
|
+
</button>
|
|
250
|
+
</>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### File Upload
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { useState } from 'react';
|
|
259
|
+
import { ASCIIR } from 'asciir';
|
|
260
|
+
|
|
261
|
+
function App() {
|
|
262
|
+
const [file, setFile] = useState<File | null>(null);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
<input
|
|
267
|
+
type="file"
|
|
268
|
+
accept="image/*"
|
|
269
|
+
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
|
270
|
+
/>
|
|
271
|
+
{file && <ASCIIR src={file} />}
|
|
272
|
+
</>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 🛠️ Development
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# Clone the repository
|
|
281
|
+
git clone https://github.com/YOUR_USERNAME/asciir.git
|
|
282
|
+
cd asciir
|
|
283
|
+
|
|
284
|
+
# Install dependencies
|
|
285
|
+
npm install
|
|
286
|
+
|
|
287
|
+
# Run the demo app
|
|
288
|
+
npm run dev
|
|
289
|
+
|
|
290
|
+
# Build the library
|
|
291
|
+
npm run build:lib
|
|
292
|
+
|
|
293
|
+
# Type checking
|
|
294
|
+
npm run typecheck
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## 📦 Building for Production
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Build library only (for npm publish)
|
|
301
|
+
npm run build:lib
|
|
302
|
+
|
|
303
|
+
# Build demo application
|
|
304
|
+
npm run build:demo
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 📝 License
|
|
308
|
+
|
|
309
|
+
MIT © Vinay
|
|
310
|
+
|
|
311
|
+
## 🌟 Show Your Support
|
|
312
|
+
|
|
313
|
+
Give a ⭐️ if this project helped you!
|
package/dist/asciir.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { jsx as I } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as j, useState as k, useRef as U, useMemo as E, useEffect as H, useCallback as B, useImperativeHandle as O } from "react";
|
|
3
|
+
const _ = {
|
|
4
|
+
/** Default character set */
|
|
5
|
+
default: " .:-=+*#%@",
|
|
6
|
+
/** Standard short character set */
|
|
7
|
+
standard_short: "@%#*+=-:. ",
|
|
8
|
+
/** Standard long character set with more detail */
|
|
9
|
+
standard_long: "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ",
|
|
10
|
+
/** Unicode block shades */
|
|
11
|
+
blocks: "█▓▒░ ",
|
|
12
|
+
/** Binary style */
|
|
13
|
+
binary: "01 ",
|
|
14
|
+
/** Minimal contrast */
|
|
15
|
+
minimal: "#. ",
|
|
16
|
+
/** Matrix/Katakana style */
|
|
17
|
+
matrix: "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ123457890:・.=*+-<>¦|"
|
|
18
|
+
}, C = {
|
|
19
|
+
fira: "'Fira Code', monospace",
|
|
20
|
+
vt323: "'VT323', monospace",
|
|
21
|
+
roboto: "'Roboto Mono', monospace",
|
|
22
|
+
source: "'Source Code Pro', monospace",
|
|
23
|
+
courier: "'Courier New', monospace"
|
|
24
|
+
}, Z = {
|
|
25
|
+
[C.fira]: 0.6,
|
|
26
|
+
[C.vt323]: 0.5,
|
|
27
|
+
[C.roboto]: 0.6,
|
|
28
|
+
[C.source]: 0.6,
|
|
29
|
+
[C.courier]: 0.6
|
|
30
|
+
}, z = {
|
|
31
|
+
/** Classic terminal green */
|
|
32
|
+
retro_term: ["#000000", "#00ff00", "#008800"],
|
|
33
|
+
/** Game Boy 4-color palette */
|
|
34
|
+
gameboy: ["#0f380f", "#306230", "#8bac0f", "#9bbc0f"],
|
|
35
|
+
/** CGA 16-color palette (subset) */
|
|
36
|
+
cga: ["#000000", "#555555", "#ffffff", "#ff5555", "#ff55ff", "#55ffff"],
|
|
37
|
+
/** Vaporwave aesthetic */
|
|
38
|
+
vaporwave: ["#ff71ce", "#01cdfe", "#05ffa1", "#b967ff", "#fffb96", "#000000"],
|
|
39
|
+
/** Grayscale gradient */
|
|
40
|
+
grayscale: ["#000000", "#333333", "#666666", "#999999", "#cccccc", "#ffffff"],
|
|
41
|
+
/** AMOLED high contrast */
|
|
42
|
+
amoled: ["#000000", "#ffffff"]
|
|
43
|
+
}, N = {
|
|
44
|
+
resolutionWidth: 150,
|
|
45
|
+
characterSet: _.default,
|
|
46
|
+
inverted: !1,
|
|
47
|
+
contrastStretch: !0,
|
|
48
|
+
fontColor: "#FFFFFF",
|
|
49
|
+
backgroundColor: "#242424",
|
|
50
|
+
lineHeight: 1,
|
|
51
|
+
scaleRatio: 0.55,
|
|
52
|
+
autoScaleHeight: !1,
|
|
53
|
+
fontSize: 12,
|
|
54
|
+
transparentBackground: !1,
|
|
55
|
+
dithering: !1,
|
|
56
|
+
colorMode: "mono",
|
|
57
|
+
fontFamily: C.fira,
|
|
58
|
+
exportScale: 2,
|
|
59
|
+
fillTransparency: !0,
|
|
60
|
+
brightness: 1,
|
|
61
|
+
contrast: 1,
|
|
62
|
+
saturation: 1,
|
|
63
|
+
colorPalette: [...z.cga]
|
|
64
|
+
}, V = (e) => new Promise((t, p) => {
|
|
65
|
+
const n = new FileReader();
|
|
66
|
+
n.onload = () => t(n.result), n.onerror = p, n.readAsDataURL(e);
|
|
67
|
+
}), D = (e) => new Promise((t, p) => {
|
|
68
|
+
const n = new Image();
|
|
69
|
+
n.crossOrigin = "anonymous", n.onload = () => t(n), n.onerror = p, n.src = e;
|
|
70
|
+
}), W = (e) => new Promise((t, p) => {
|
|
71
|
+
const n = URL.createObjectURL(e), a = new Image();
|
|
72
|
+
a.onload = () => {
|
|
73
|
+
URL.revokeObjectURL(n), t(a);
|
|
74
|
+
}, a.onerror = (f) => {
|
|
75
|
+
URL.revokeObjectURL(n), p(f);
|
|
76
|
+
}, a.src = n;
|
|
77
|
+
}), P = (e) => {
|
|
78
|
+
const t = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);
|
|
79
|
+
return t ? {
|
|
80
|
+
r: parseInt(t[1], 16),
|
|
81
|
+
g: parseInt(t[2], 16),
|
|
82
|
+
b: parseInt(t[3], 16)
|
|
83
|
+
} : { r: 0, g: 0, b: 0 };
|
|
84
|
+
}, L = (e) => Math.max(0, Math.min(255, e)), q = (e, t) => {
|
|
85
|
+
const { brightness: p, contrast: n, saturation: a } = t;
|
|
86
|
+
if (p === 1 && n === 1 && a === 1) return;
|
|
87
|
+
const f = 128 * (1 - n);
|
|
88
|
+
for (let s = 0; s < e.length; s += 4) {
|
|
89
|
+
let m = e[s], h = e[s + 1], l = e[s + 2];
|
|
90
|
+
m *= p, h *= p, l *= p, m = m * n + f, h = h * n + f, l = l * n + f;
|
|
91
|
+
const u = 0.299 * m + 0.587 * h + 0.114 * l;
|
|
92
|
+
m = u + a * (m - u), h = u + a * (h - u), l = u + a * (l - u), e[s] = L(m), e[s + 1] = L(h), e[s + 2] = L(l);
|
|
93
|
+
}
|
|
94
|
+
}, G = (e, t, p, n) => {
|
|
95
|
+
let a = 1 / 0, f = n[0];
|
|
96
|
+
for (const s of n) {
|
|
97
|
+
const m = (e - s.r) ** 2 + (t - s.g) ** 2 + (p - s.b) ** 2;
|
|
98
|
+
m < a && (a = m, f = s);
|
|
99
|
+
}
|
|
100
|
+
return f;
|
|
101
|
+
}, A = (e, t) => {
|
|
102
|
+
const p = document.createElement("canvas"), n = p.getContext("2d", { willReadFrequently: !0 });
|
|
103
|
+
if (!n)
|
|
104
|
+
throw new Error("Canvas context not supported");
|
|
105
|
+
const a = e.height / e.width, f = t.resolutionWidth, s = Math.floor(a * f * t.scaleRatio);
|
|
106
|
+
p.width = f, p.height = s, n.drawImage(e, 0, 0, f, s);
|
|
107
|
+
const h = n.getImageData(0, 0, f, s).data, l = P(t.backgroundColor), u = t.colorMode === "palette" ? t.colorPalette.map(P) : [];
|
|
108
|
+
for (let o = 0; o < h.length; o += 4) {
|
|
109
|
+
const d = h[o + 3];
|
|
110
|
+
if (t.fillTransparency && d < 255) {
|
|
111
|
+
const r = d / 255;
|
|
112
|
+
h[o] = Math.round(h[o] * r + l.r * (1 - r)), h[o + 1] = Math.round(h[o + 1] * r + l.g * (1 - r)), h[o + 2] = Math.round(h[o + 2] * r + l.b * (1 - r)), h[o + 3] = 255;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
q(h, t);
|
|
116
|
+
const c = new Float32Array(f * s), v = new Uint8ClampedArray(h.length);
|
|
117
|
+
for (let o = 0; o < h.length; o += 4) {
|
|
118
|
+
let d = h[o], r = h[o + 1], i = h[o + 2];
|
|
119
|
+
if (t.colorMode === "palette" && u.length > 0) {
|
|
120
|
+
const g = G(d, r, i, u);
|
|
121
|
+
d = g.r, r = g.g, i = g.b;
|
|
122
|
+
}
|
|
123
|
+
v[o] = d, v[o + 1] = r, v[o + 2] = i, v[o + 3] = 255, c[o / 4] = d * 0.299 + r * 0.587 + i * 0.114;
|
|
124
|
+
}
|
|
125
|
+
if (t.contrastStretch) {
|
|
126
|
+
let o = 255, d = 0;
|
|
127
|
+
for (let i = 0; i < c.length; i++)
|
|
128
|
+
c[i] < o && (o = c[i]), c[i] > d && (d = c[i]);
|
|
129
|
+
const r = d - o;
|
|
130
|
+
if (r > 0)
|
|
131
|
+
for (let i = 0; i < c.length; i++)
|
|
132
|
+
c[i] = (c[i] - o) / r * 255;
|
|
133
|
+
else
|
|
134
|
+
c.fill(0);
|
|
135
|
+
}
|
|
136
|
+
let x = t.characterSet.split("");
|
|
137
|
+
t.inverted && (x = x.reverse());
|
|
138
|
+
const w = x.length - 1;
|
|
139
|
+
if (t.dithering)
|
|
140
|
+
for (let o = 0; o < s; o++)
|
|
141
|
+
for (let d = 0; d < f; d++) {
|
|
142
|
+
const r = o * f + d, i = c[r], g = 255 / Math.max(1, w), $ = Math.round(i / g) * g, M = i - $;
|
|
143
|
+
d + 1 < f && (c[o * f + (d + 1)] += M * 7 / 16), d - 1 >= 0 && o + 1 < s && (c[(o + 1) * f + (d - 1)] += M * 3 / 16), o + 1 < s && (c[(o + 1) * f + d] += M * 5 / 16), d + 1 < f && o + 1 < s && (c[(o + 1) * f + (d + 1)] += M * 1 / 16);
|
|
144
|
+
}
|
|
145
|
+
const R = [], b = w / 255;
|
|
146
|
+
for (let o = 0; o < c.length; o++) {
|
|
147
|
+
let d = c[o];
|
|
148
|
+
d = Math.max(0, Math.min(255, d));
|
|
149
|
+
const r = Math.floor(d * b + 0.5);
|
|
150
|
+
R.push(x[Math.max(0, Math.min(r, w))]);
|
|
151
|
+
}
|
|
152
|
+
const y = [];
|
|
153
|
+
for (let o = 0; o < R.length; o += f)
|
|
154
|
+
y.push(R.slice(o, o + f).join(""));
|
|
155
|
+
return {
|
|
156
|
+
text: y.join(`
|
|
157
|
+
`),
|
|
158
|
+
lines: y,
|
|
159
|
+
width: f,
|
|
160
|
+
height: s,
|
|
161
|
+
colorData: t.colorMode === "original" || t.colorMode === "palette" ? v : void 0
|
|
162
|
+
};
|
|
163
|
+
}, T = (e, t, p) => {
|
|
164
|
+
const n = document.createElement("canvas"), a = n.getContext("2d");
|
|
165
|
+
if (!a) throw new Error("Canvas context not supported");
|
|
166
|
+
const f = p || 1, s = t.fontSize * f, m = t.fontFamily;
|
|
167
|
+
a.font = `${s}px ${m}`, a.textBaseline = "top";
|
|
168
|
+
const l = a.measureText("M").width, c = s * t.lineHeight, v = Math.ceil(l * e.width), x = Math.ceil(c * e.height);
|
|
169
|
+
n.width = v, n.height = x, a.font = `${s}px ${m}`, a.textBaseline = "top", t.transparentBackground ? a.clearRect(0, 0, v, x) : (a.fillStyle = t.backgroundColor, a.fillRect(0, 0, v, x));
|
|
170
|
+
const { lines: w, colorData: R } = e;
|
|
171
|
+
if ((t.colorMode === "original" || t.colorMode === "palette") && R)
|
|
172
|
+
for (let b = 0; b < e.height; b++) {
|
|
173
|
+
const y = w[b], o = b * c;
|
|
174
|
+
for (let d = 0; d < e.width; d++) {
|
|
175
|
+
const r = y[d], i = (b * e.width + d) * 4, g = R[i], S = R[i + 1], $ = R[i + 2];
|
|
176
|
+
a.fillStyle = `rgb(${g},${S},${$})`, a.fillText(r, d * l, o);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
a.fillStyle = t.fontColor;
|
|
181
|
+
for (let b = 0; b < w.length; b++)
|
|
182
|
+
a.fillText(w[b], 0, b * c);
|
|
183
|
+
}
|
|
184
|
+
return n;
|
|
185
|
+
}, F = (e, t) => {
|
|
186
|
+
const { lines: p, colorData: n } = e, a = t.fontSize, f = a * 0.6, s = a * t.lineHeight, m = f * e.width, h = s * e.height, l = (c) => c.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
187
|
+
let u = `<svg xmlns="http://www.w3.org/2000/svg" width="${m}" height="${h}" viewBox="0 0 ${m} ${h}" shape-rendering="crispEdges">`;
|
|
188
|
+
if (t.transparentBackground || (u += `<rect width="100%" height="100%" fill="${t.backgroundColor}"/>`), u += `<style>text { font-family: ${t.fontFamily}, monospace; font-size: ${a}px; white-space: pre; text-rendering: geometricPrecision; }</style>`, (t.colorMode === "original" || t.colorMode === "palette") && n)
|
|
189
|
+
for (let c = 0; c < e.height; c++) {
|
|
190
|
+
let x = `<text x="0" y="${c * s + a}">`;
|
|
191
|
+
for (let w = 0; w < e.width; w++) {
|
|
192
|
+
const R = p[c][w], b = (c * e.width + w) * 4, y = n[b], o = n[b + 1], d = n[b + 2], r = "#" + ((1 << 24) + (y << 16) + (o << 8) + d).toString(16).slice(1);
|
|
193
|
+
x += `<tspan fill="${r}">${l(R)}</tspan>`;
|
|
194
|
+
}
|
|
195
|
+
x += "</text>", u += x;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const c = t.fontColor;
|
|
199
|
+
p.forEach((v, x) => {
|
|
200
|
+
const w = x * s + a;
|
|
201
|
+
u += `<text x="0" y="${w}" fill="${c}">${l(v)}</text>`;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return u += "</svg>", u;
|
|
205
|
+
}, X = (e, t) => {
|
|
206
|
+
const { lines: p, colorData: n } = e, a = t.fontSize, f = t.lineHeight;
|
|
207
|
+
if ((t.colorMode === "original" || t.colorMode === "palette") && n) {
|
|
208
|
+
let s = `<pre style="font-family: ${t.fontFamily}; font-size: ${a}px; line-height: ${f}; margin: 0; ${t.transparentBackground ? "" : `background-color: ${t.backgroundColor};`} display: inline-block;">`;
|
|
209
|
+
for (let m = 0; m < e.height; m++) {
|
|
210
|
+
for (let h = 0; h < e.width; h++) {
|
|
211
|
+
const l = p[m][h], u = (m * e.width + h) * 4, c = n[u], v = n[u + 1], x = n[u + 2];
|
|
212
|
+
s += `<span style="color: rgb(${c},${v},${x})">${l === "<" ? "<" : l === ">" ? ">" : l === "&" ? "&" : l}</span>`;
|
|
213
|
+
}
|
|
214
|
+
m < e.height - 1 && (s += `
|
|
215
|
+
`);
|
|
216
|
+
}
|
|
217
|
+
return s += "</pre>", s;
|
|
218
|
+
} else {
|
|
219
|
+
const s = p.join(`
|
|
220
|
+
`).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
221
|
+
return `<pre style="font-family: ${t.fontFamily}; font-size: ${a}px; line-height: ${f}; margin: 0; color: ${t.fontColor}; ${t.transparentBackground ? "" : `background-color: ${t.backgroundColor};`} display: inline-block;">${s}</pre>`;
|
|
222
|
+
}
|
|
223
|
+
}, J = j(
|
|
224
|
+
({
|
|
225
|
+
src: e,
|
|
226
|
+
config: t,
|
|
227
|
+
output: p = "html",
|
|
228
|
+
onGenerate: n,
|
|
229
|
+
onError: a,
|
|
230
|
+
className: f,
|
|
231
|
+
style: s,
|
|
232
|
+
autoFit: m = !1
|
|
233
|
+
}, h) => {
|
|
234
|
+
const [l, u] = k(null), [c, v] = k(null), [x, w] = k(!1), R = U(null), b = U(null), y = E(
|
|
235
|
+
() => ({ ...N, ...t }),
|
|
236
|
+
[t]
|
|
237
|
+
);
|
|
238
|
+
H(() => {
|
|
239
|
+
let r = !1;
|
|
240
|
+
return (async () => {
|
|
241
|
+
w(!0);
|
|
242
|
+
try {
|
|
243
|
+
let g;
|
|
244
|
+
if (e instanceof HTMLImageElement)
|
|
245
|
+
g = e;
|
|
246
|
+
else if (typeof e == "object" && e !== null && "name" in e && "type" in e) {
|
|
247
|
+
const S = await V(e);
|
|
248
|
+
g = await D(S);
|
|
249
|
+
} else if (e instanceof Blob)
|
|
250
|
+
g = await W(e);
|
|
251
|
+
else if (typeof e == "string")
|
|
252
|
+
g = await D(e);
|
|
253
|
+
else
|
|
254
|
+
throw new Error("Invalid src type");
|
|
255
|
+
r || v(g);
|
|
256
|
+
} catch (g) {
|
|
257
|
+
if (!r) {
|
|
258
|
+
const S = g instanceof Error ? g : new Error(String(g));
|
|
259
|
+
a?.(S), v(null);
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
r || w(!1);
|
|
263
|
+
}
|
|
264
|
+
})(), () => {
|
|
265
|
+
r = !0;
|
|
266
|
+
};
|
|
267
|
+
}, [e, a]), H(() => {
|
|
268
|
+
if (!c) {
|
|
269
|
+
u(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const r = setTimeout(() => {
|
|
273
|
+
try {
|
|
274
|
+
const i = A(c, y);
|
|
275
|
+
u(i), n?.(i);
|
|
276
|
+
} catch (i) {
|
|
277
|
+
const g = i instanceof Error ? i : new Error(String(i));
|
|
278
|
+
a?.(g);
|
|
279
|
+
}
|
|
280
|
+
}, 200);
|
|
281
|
+
return () => clearTimeout(r);
|
|
282
|
+
}, [c, y, n, a]);
|
|
283
|
+
const o = B(() => {
|
|
284
|
+
if (c)
|
|
285
|
+
try {
|
|
286
|
+
const r = A(c, y);
|
|
287
|
+
u(r), n?.(r);
|
|
288
|
+
} catch (r) {
|
|
289
|
+
const i = r instanceof Error ? r : new Error(String(r));
|
|
290
|
+
a?.(i);
|
|
291
|
+
}
|
|
292
|
+
}, [c, y, n, a]);
|
|
293
|
+
O(
|
|
294
|
+
h,
|
|
295
|
+
() => ({
|
|
296
|
+
getResult: () => l,
|
|
297
|
+
downloadPNG: (r = `ascii-art-${Date.now()}.png`) => {
|
|
298
|
+
if (!l) return;
|
|
299
|
+
const i = T(l, y, y.exportScale), g = document.createElement("a");
|
|
300
|
+
g.download = r, g.href = i.toDataURL("image/png"), g.click();
|
|
301
|
+
},
|
|
302
|
+
downloadSVG: (r = `ascii-art-${Date.now()}.svg`) => {
|
|
303
|
+
if (!l) return;
|
|
304
|
+
const i = F(l, y), g = new Blob([i], { type: "image/svg+xml" }), S = URL.createObjectURL(g), $ = document.createElement("a");
|
|
305
|
+
$.download = r, $.href = S, $.click(), URL.revokeObjectURL(S);
|
|
306
|
+
},
|
|
307
|
+
downloadTXT: (r = `ascii-art-${Date.now()}.txt`) => {
|
|
308
|
+
if (!l) return;
|
|
309
|
+
const i = new Blob([l.text], { type: "text/plain" }), g = URL.createObjectURL(i), S = document.createElement("a");
|
|
310
|
+
S.download = r, S.href = g, S.click(), URL.revokeObjectURL(g);
|
|
311
|
+
},
|
|
312
|
+
copyToClipboard: async () => {
|
|
313
|
+
l && await navigator.clipboard.writeText(l.text);
|
|
314
|
+
},
|
|
315
|
+
getCanvas: (r) => l ? T(l, y, r ?? y.exportScale) : null,
|
|
316
|
+
getSVGString: () => l ? F(l, y) : null,
|
|
317
|
+
refresh: o
|
|
318
|
+
}),
|
|
319
|
+
[l, y, o]
|
|
320
|
+
);
|
|
321
|
+
const d = E(() => {
|
|
322
|
+
if (!l) return null;
|
|
323
|
+
switch (p) {
|
|
324
|
+
case "svg":
|
|
325
|
+
return /* @__PURE__ */ I(
|
|
326
|
+
"div",
|
|
327
|
+
{
|
|
328
|
+
dangerouslySetInnerHTML: { __html: F(l, y) },
|
|
329
|
+
style: { display: "inline-block" }
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
case "canvas":
|
|
333
|
+
const r = T(l, y);
|
|
334
|
+
return b.current = r, /* @__PURE__ */ I(
|
|
335
|
+
"img",
|
|
336
|
+
{
|
|
337
|
+
src: r.toDataURL(),
|
|
338
|
+
alt: "ASCII Art",
|
|
339
|
+
style: { display: "block", maxWidth: m ? "100%" : void 0 }
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
case "html":
|
|
343
|
+
default:
|
|
344
|
+
return /* @__PURE__ */ I(
|
|
345
|
+
"div",
|
|
346
|
+
{
|
|
347
|
+
dangerouslySetInnerHTML: { __html: X(l, y) },
|
|
348
|
+
style: { display: "inline-block" }
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}, [l, y, p, m]);
|
|
353
|
+
return /* @__PURE__ */ I(
|
|
354
|
+
"div",
|
|
355
|
+
{
|
|
356
|
+
ref: R,
|
|
357
|
+
className: f,
|
|
358
|
+
style: {
|
|
359
|
+
display: "inline-block",
|
|
360
|
+
overflow: m ? "auto" : void 0,
|
|
361
|
+
...s
|
|
362
|
+
},
|
|
363
|
+
children: x ? /* @__PURE__ */ I("div", { style: { padding: "1rem", opacity: 0.5 }, children: "Loading..." }) : l ? d : /* @__PURE__ */ I("div", { style: { padding: "1rem", opacity: 0.5 }, children: "No image loaded" })
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
J.displayName = "ASCIIRender";
|
|
369
|
+
const K = () => U(null);
|
|
370
|
+
export {
|
|
371
|
+
J as ASCIIR,
|
|
372
|
+
J as ASCIIRender,
|
|
373
|
+
_ as CHAR_SETS,
|
|
374
|
+
N as DEFAULT_CONFIG,
|
|
375
|
+
C as FONTS,
|
|
376
|
+
Z as FONT_RATIOS,
|
|
377
|
+
z as PALETTE_PRESETS,
|
|
378
|
+
J as default,
|
|
379
|
+
A as generateAscii,
|
|
380
|
+
T as generateCanvasFromAscii,
|
|
381
|
+
X as generateHTML,
|
|
382
|
+
F as generateSVG,
|
|
383
|
+
D as loadImage,
|
|
384
|
+
W as loadImageFromBlob,
|
|
385
|
+
V as readFileAsDataURL,
|
|
386
|
+
K as useASCIIRender
|
|
387
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(function(y,C){typeof exports=="object"&&typeof module<"u"?C(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],C):(y=typeof globalThis<"u"?globalThis:y||self,C(y.ASCIIR={},y.jsxRuntime,y.React))})(this,(function(y,C,R){"use strict";const P={default:" .:-=+*#%@",standard_short:"@%#*+=-:. ",standard_long:"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ",blocks:"█▓▒░ ",binary:"01 ",minimal:"#. ",matrix:"ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ123457890:・.=*+-<>¦|"},M={fira:"'Fira Code', monospace",vt323:"'VT323', monospace",roboto:"'Roboto Mono', monospace",source:"'Source Code Pro', monospace",courier:"'Courier New', monospace"},N={[M.fira]:.6,[M.vt323]:.5,[M.roboto]:.6,[M.source]:.6,[M.courier]:.6},D={retro_term:["#000000","#00ff00","#008800"],gameboy:["#0f380f","#306230","#8bac0f","#9bbc0f"],cga:["#000000","#555555","#ffffff","#ff5555","#ff55ff","#55ffff"],vaporwave:["#ff71ce","#01cdfe","#05ffa1","#b967ff","#fffb96","#000000"],grayscale:["#000000","#333333","#666666","#999999","#cccccc","#ffffff"],amoled:["#000000","#ffffff"]},j={resolutionWidth:150,characterSet:P.default,inverted:!1,contrastStretch:!0,fontColor:"#FFFFFF",backgroundColor:"#242424",lineHeight:1,scaleRatio:.55,autoScaleHeight:!1,fontSize:12,transparentBackground:!1,dithering:!1,colorMode:"mono",fontFamily:M.fira,exportScale:2,fillTransparency:!0,brightness:1,contrast:1,saturation:1,colorPalette:[...D.cga]},O=t=>new Promise((e,u)=>{const o=new FileReader;o.onload=()=>e(o.result),o.onerror=u,o.readAsDataURL(t)}),A=t=>new Promise((e,u)=>{const o=new Image;o.crossOrigin="anonymous",o.onload=()=>e(o),o.onerror=u,o.src=t}),_=t=>new Promise((e,u)=>{const o=URL.createObjectURL(t),r=new Image;r.onload=()=>{URL.revokeObjectURL(o),e(r)},r.onerror=f=>{URL.revokeObjectURL(o),u(f)},r.src=o}),B=t=>{const e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:{r:0,g:0,b:0}},U=t=>Math.max(0,Math.min(255,t)),V=(t,e)=>{const{brightness:u,contrast:o,saturation:r}=e;if(u===1&&o===1&&r===1)return;const f=128*(1-o);for(let s=0;s<t.length;s+=4){let m=t[s],h=t[s+1],l=t[s+2];m*=u,h*=u,l*=u,m=m*o+f,h=h*o+f,l=l*o+f;const p=.299*m+.587*h+.114*l;m=p+r*(m-p),h=p+r*(h-p),l=p+r*(l-p),t[s]=U(m),t[s+1]=U(h),t[s+2]=U(l)}},q=(t,e,u,o)=>{let r=1/0,f=o[0];for(const s of o){const m=(t-s.r)**2+(e-s.g)**2+(u-s.b)**2;m<r&&(r=m,f=s)}return f},H=(t,e)=>{const u=document.createElement("canvas"),o=u.getContext("2d",{willReadFrequently:!0});if(!o)throw new Error("Canvas context not supported");const r=t.height/t.width,f=e.resolutionWidth,s=Math.floor(r*f*e.scaleRatio);u.width=f,u.height=s,o.drawImage(t,0,0,f,s);const h=o.getImageData(0,0,f,s).data,l=B(e.backgroundColor),p=e.colorMode==="palette"?e.colorPalette.map(B):[];for(let n=0;n<h.length;n+=4){const d=h[n+3];if(e.fillTransparency&&d<255){const a=d/255;h[n]=Math.round(h[n]*a+l.r*(1-a)),h[n+1]=Math.round(h[n+1]*a+l.g*(1-a)),h[n+2]=Math.round(h[n+2]*a+l.b*(1-a)),h[n+3]=255}}V(h,e);const i=new Float32Array(f*s),v=new Uint8ClampedArray(h.length);for(let n=0;n<h.length;n+=4){let d=h[n],a=h[n+1],c=h[n+2];if(e.colorMode==="palette"&&p.length>0){const g=q(d,a,c,p);d=g.r,a=g.g,c=g.b}v[n]=d,v[n+1]=a,v[n+2]=c,v[n+3]=255,i[n/4]=d*.299+a*.587+c*.114}if(e.contrastStretch){let n=255,d=0;for(let c=0;c<i.length;c++)i[c]<n&&(n=i[c]),i[c]>d&&(d=i[c]);const a=d-n;if(a>0)for(let c=0;c<i.length;c++)i[c]=(i[c]-n)/a*255;else i.fill(0)}let w=e.characterSet.split("");e.inverted&&(w=w.reverse());const S=w.length-1;if(e.dithering)for(let n=0;n<s;n++)for(let d=0;d<f;d++){const a=n*f+d,c=i[a],g=255/Math.max(1,S),T=Math.round(c/g)*g,E=c-T;d+1<f&&(i[n*f+(d+1)]+=E*7/16),d-1>=0&&n+1<s&&(i[(n+1)*f+(d-1)]+=E*3/16),n+1<s&&(i[(n+1)*f+d]+=E*5/16),d+1<f&&n+1<s&&(i[(n+1)*f+(d+1)]+=E*1/16)}const $=[],x=S/255;for(let n=0;n<i.length;n++){let d=i[n];d=Math.max(0,Math.min(255,d));const a=Math.floor(d*x+.5);$.push(w[Math.max(0,Math.min(a,S))])}const b=[];for(let n=0;n<$.length;n+=f)b.push($.slice(n,n+f).join(""));return{text:b.join(`
|
|
2
|
+
`),lines:b,width:f,height:s,colorData:e.colorMode==="original"||e.colorMode==="palette"?v:void 0}},L=(t,e,u)=>{const o=document.createElement("canvas"),r=o.getContext("2d");if(!r)throw new Error("Canvas context not supported");const f=u||1,s=e.fontSize*f,m=e.fontFamily;r.font=`${s}px ${m}`,r.textBaseline="top";const l=r.measureText("M").width,i=s*e.lineHeight,v=Math.ceil(l*t.width),w=Math.ceil(i*t.height);o.width=v,o.height=w,r.font=`${s}px ${m}`,r.textBaseline="top",e.transparentBackground?r.clearRect(0,0,v,w):(r.fillStyle=e.backgroundColor,r.fillRect(0,0,v,w));const{lines:S,colorData:$}=t;if((e.colorMode==="original"||e.colorMode==="palette")&&$)for(let x=0;x<t.height;x++){const b=S[x],n=x*i;for(let d=0;d<t.width;d++){const a=b[d],c=(x*t.width+d)*4,g=$[c],I=$[c+1],T=$[c+2];r.fillStyle=`rgb(${g},${I},${T})`,r.fillText(a,d*l,n)}}else{r.fillStyle=e.fontColor;for(let x=0;x<S.length;x++)r.fillText(S[x],0,x*i)}return o},k=(t,e)=>{const{lines:u,colorData:o}=t,r=e.fontSize,f=r*.6,s=r*e.lineHeight,m=f*t.width,h=s*t.height,l=i=>i.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");let p=`<svg xmlns="http://www.w3.org/2000/svg" width="${m}" height="${h}" viewBox="0 0 ${m} ${h}" shape-rendering="crispEdges">`;if(e.transparentBackground||(p+=`<rect width="100%" height="100%" fill="${e.backgroundColor}"/>`),p+=`<style>text { font-family: ${e.fontFamily}, monospace; font-size: ${r}px; white-space: pre; text-rendering: geometricPrecision; }</style>`,(e.colorMode==="original"||e.colorMode==="palette")&&o)for(let i=0;i<t.height;i++){let w=`<text x="0" y="${i*s+r}">`;for(let S=0;S<t.width;S++){const $=u[i][S],x=(i*t.width+S)*4,b=o[x],n=o[x+1],d=o[x+2],a="#"+((1<<24)+(b<<16)+(n<<8)+d).toString(16).slice(1);w+=`<tspan fill="${a}">${l($)}</tspan>`}w+="</text>",p+=w}else{const i=e.fontColor;u.forEach((v,w)=>{const S=w*s+r;p+=`<text x="0" y="${S}" fill="${i}">${l(v)}</text>`})}return p+="</svg>",p},z=(t,e)=>{const{lines:u,colorData:o}=t,r=e.fontSize,f=e.lineHeight;if((e.colorMode==="original"||e.colorMode==="palette")&&o){let s=`<pre style="font-family: ${e.fontFamily}; font-size: ${r}px; line-height: ${f}; margin: 0; ${e.transparentBackground?"":`background-color: ${e.backgroundColor};`} display: inline-block;">`;for(let m=0;m<t.height;m++){for(let h=0;h<t.width;h++){const l=u[m][h],p=(m*t.width+h)*4,i=o[p],v=o[p+1],w=o[p+2];s+=`<span style="color: rgb(${i},${v},${w})">${l==="<"?"<":l===">"?">":l==="&"?"&":l}</span>`}m<t.height-1&&(s+=`
|
|
3
|
+
`)}return s+="</pre>",s}else{const s=u.join(`
|
|
4
|
+
`).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");return`<pre style="font-family: ${e.fontFamily}; font-size: ${r}px; line-height: ${f}; margin: 0; color: ${e.fontColor}; ${e.transparentBackground?"":`background-color: ${e.backgroundColor};`} display: inline-block;">${s}</pre>`}},F=R.forwardRef(({src:t,config:e,output:u="html",onGenerate:o,onError:r,className:f,style:s,autoFit:m=!1},h)=>{const[l,p]=R.useState(null),[i,v]=R.useState(null),[w,S]=R.useState(!1),$=R.useRef(null),x=R.useRef(null),b=R.useMemo(()=>({...j,...e}),[e]);R.useEffect(()=>{let a=!1;return(async()=>{S(!0);try{let g;if(t instanceof HTMLImageElement)g=t;else if(typeof t=="object"&&t!==null&&"name"in t&&"type"in t){const I=await O(t);g=await A(I)}else if(t instanceof Blob)g=await _(t);else if(typeof t=="string")g=await A(t);else throw new Error("Invalid src type");a||v(g)}catch(g){if(!a){const I=g instanceof Error?g:new Error(String(g));r?.(I),v(null)}}finally{a||S(!1)}})(),()=>{a=!0}},[t,r]),R.useEffect(()=>{if(!i){p(null);return}const a=setTimeout(()=>{try{const c=H(i,b);p(c),o?.(c)}catch(c){const g=c instanceof Error?c:new Error(String(c));r?.(g)}},200);return()=>clearTimeout(a)},[i,b,o,r]);const n=R.useCallback(()=>{if(i)try{const a=H(i,b);p(a),o?.(a)}catch(a){const c=a instanceof Error?a:new Error(String(a));r?.(c)}},[i,b,o,r]);R.useImperativeHandle(h,()=>({getResult:()=>l,downloadPNG:(a=`ascii-art-${Date.now()}.png`)=>{if(!l)return;const c=L(l,b,b.exportScale),g=document.createElement("a");g.download=a,g.href=c.toDataURL("image/png"),g.click()},downloadSVG:(a=`ascii-art-${Date.now()}.svg`)=>{if(!l)return;const c=k(l,b),g=new Blob([c],{type:"image/svg+xml"}),I=URL.createObjectURL(g),T=document.createElement("a");T.download=a,T.href=I,T.click(),URL.revokeObjectURL(I)},downloadTXT:(a=`ascii-art-${Date.now()}.txt`)=>{if(!l)return;const c=new Blob([l.text],{type:"text/plain"}),g=URL.createObjectURL(c),I=document.createElement("a");I.download=a,I.href=g,I.click(),URL.revokeObjectURL(g)},copyToClipboard:async()=>{l&&await navigator.clipboard.writeText(l.text)},getCanvas:a=>l?L(l,b,a??b.exportScale):null,getSVGString:()=>l?k(l,b):null,refresh:n}),[l,b,n]);const d=R.useMemo(()=>{if(!l)return null;switch(u){case"svg":return C.jsx("div",{dangerouslySetInnerHTML:{__html:k(l,b)},style:{display:"inline-block"}});case"canvas":const a=L(l,b);return x.current=a,C.jsx("img",{src:a.toDataURL(),alt:"ASCII Art",style:{display:"block",maxWidth:m?"100%":void 0}});case"html":default:return C.jsx("div",{dangerouslySetInnerHTML:{__html:z(l,b)},style:{display:"inline-block"}})}},[l,b,u,m]);return C.jsx("div",{ref:$,className:f,style:{display:"inline-block",overflow:m?"auto":void 0,...s},children:w?C.jsx("div",{style:{padding:"1rem",opacity:.5},children:"Loading..."}):l?d:C.jsx("div",{style:{padding:"1rem",opacity:.5},children:"No image loaded"})})});F.displayName="ASCIIRender";const W=()=>R.useRef(null);y.ASCIIR=F,y.ASCIIRender=F,y.CHAR_SETS=P,y.DEFAULT_CONFIG=j,y.FONTS=M,y.FONT_RATIOS=N,y.PALETTE_PRESETS=D,y.default=F,y.generateAscii=H,y.generateCanvasFromAscii=L,y.generateHTML=z,y.generateSVG=k,y.loadImage=A,y.loadImageFromBlob=_,y.readFileAsDataURL=O,y.useASCIIRender=W,Object.defineProperties(y,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { default as default_2 } from 'react';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ASCIIRender - A React component for converting images to ASCII art
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { ASCIIRender } from 'asciirender';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <ASCIIRender
|
|
14
|
+
* src="/path/to/image.jpg"
|
|
15
|
+
* config={{ resolutionWidth: 100, colorMode: 'original' }}
|
|
16
|
+
* />
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const ASCIIRender: default_2.ForwardRefExoticComponent<ASCIIRenderProps & default_2.RefAttributes<ASCIIRenderRef>>;
|
|
22
|
+
export { ASCIIRender as ASCIIR }
|
|
23
|
+
export { ASCIIRender }
|
|
24
|
+
export default ASCIIRender;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration options for ASCII art generation
|
|
28
|
+
*/
|
|
29
|
+
export declare interface ASCIIRenderConfig {
|
|
30
|
+
/** Width of output in characters (columns). Default: 150 */
|
|
31
|
+
resolutionWidth: number;
|
|
32
|
+
/** Character set for ASCII mapping (dark to light). Default: " .:-=+*#%@" */
|
|
33
|
+
characterSet: string;
|
|
34
|
+
/** Whether to invert the character ramp. Default: false */
|
|
35
|
+
inverted: boolean;
|
|
36
|
+
/** Apply auto contrast stretching. Default: true */
|
|
37
|
+
contrastStretch: boolean;
|
|
38
|
+
/** Font/text color for mono mode. Default: "#FFFFFF" */
|
|
39
|
+
fontColor: string;
|
|
40
|
+
/** Background color. Default: "#242424" */
|
|
41
|
+
backgroundColor: string;
|
|
42
|
+
/** Line height multiplier. Default: 1.0 */
|
|
43
|
+
lineHeight: number;
|
|
44
|
+
/** Vertical scale ratio (to correct aspect ratio). Default: 0.55 */
|
|
45
|
+
scaleRatio: number;
|
|
46
|
+
/** Auto-scale ratio based on font height. Default: false */
|
|
47
|
+
autoScaleHeight: boolean;
|
|
48
|
+
/** Font size in pixels. Default: 12 */
|
|
49
|
+
fontSize: number;
|
|
50
|
+
/** Whether background should be transparent. Default: false */
|
|
51
|
+
transparentBackground: boolean;
|
|
52
|
+
/** Apply Floyd-Steinberg dithering. Default: false */
|
|
53
|
+
dithering: boolean;
|
|
54
|
+
/** Color mode: 'mono', 'original', or 'palette'. Default: 'mono' */
|
|
55
|
+
colorMode: 'mono' | 'original' | 'palette';
|
|
56
|
+
/** Font family for rendering. Default: "'Fira Code', monospace" */
|
|
57
|
+
fontFamily: string;
|
|
58
|
+
/** Scale factor for PNG export. Default: 2 */
|
|
59
|
+
exportScale: number;
|
|
60
|
+
/** Fill transparent areas with background color. Default: true */
|
|
61
|
+
fillTransparency: boolean;
|
|
62
|
+
/** Brightness adjustment (0-3). Default: 1.0 */
|
|
63
|
+
brightness: number;
|
|
64
|
+
/** Contrast adjustment (0-3). Default: 1.0 */
|
|
65
|
+
contrast: number;
|
|
66
|
+
/** Saturation adjustment (0-3). Default: 1.0 */
|
|
67
|
+
saturation: number;
|
|
68
|
+
/** Color palette for palette mode. Default: CGA colors */
|
|
69
|
+
colorPalette: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Props for the ASCIIRender component
|
|
74
|
+
*/
|
|
75
|
+
export declare interface ASCIIRenderProps {
|
|
76
|
+
/** Image source - can be URL, File, Blob, or HTMLImageElement */
|
|
77
|
+
src: string | File | Blob | HTMLImageElement;
|
|
78
|
+
/** Partial config overrides (all fields optional) */
|
|
79
|
+
config?: Partial<ASCIIRenderConfig>;
|
|
80
|
+
/** Output format: 'html' (default), 'svg', or 'canvas' */
|
|
81
|
+
output?: 'html' | 'svg' | 'canvas';
|
|
82
|
+
/** Callback when ASCII art is generated */
|
|
83
|
+
onGenerate?: (result: ProcessingResult) => void;
|
|
84
|
+
/** Callback on error */
|
|
85
|
+
onError?: (error: Error) => void;
|
|
86
|
+
/** Additional className for the container */
|
|
87
|
+
className?: string;
|
|
88
|
+
/** Additional style for the container */
|
|
89
|
+
style?: React.CSSProperties;
|
|
90
|
+
/** Whether to auto-fit to container width */
|
|
91
|
+
autoFit?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Ref handle for imperative access to ASCIIRender
|
|
96
|
+
*/
|
|
97
|
+
export declare interface ASCIIRenderRef {
|
|
98
|
+
/** Get the current processing result */
|
|
99
|
+
getResult: () => ProcessingResult | null;
|
|
100
|
+
/** Download as PNG */
|
|
101
|
+
downloadPNG: (filename?: string) => void;
|
|
102
|
+
/** Download as SVG */
|
|
103
|
+
downloadSVG: (filename?: string) => void;
|
|
104
|
+
/** Download as TXT */
|
|
105
|
+
downloadTXT: (filename?: string) => void;
|
|
106
|
+
/** Copy text to clipboard */
|
|
107
|
+
copyToClipboard: () => Promise<void>;
|
|
108
|
+
/** Get the generated canvas element */
|
|
109
|
+
getCanvas: (scale?: number) => HTMLCanvasElement | null;
|
|
110
|
+
/** Get the SVG string */
|
|
111
|
+
getSVGString: () => string | null;
|
|
112
|
+
/** Re-process the image with current config */
|
|
113
|
+
refresh: () => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Predefined character sets for ASCII art generation
|
|
118
|
+
*/
|
|
119
|
+
export declare const CHAR_SETS: {
|
|
120
|
+
/** Default character set */
|
|
121
|
+
readonly default: " .:-=+*#%@";
|
|
122
|
+
/** Standard short character set */
|
|
123
|
+
readonly standard_short: "@%#*+=-:. ";
|
|
124
|
+
/** Standard long character set with more detail */
|
|
125
|
+
readonly standard_long: "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
|
|
126
|
+
/** Unicode block shades */
|
|
127
|
+
readonly blocks: "█▓▒░ ";
|
|
128
|
+
/** Binary style */
|
|
129
|
+
readonly binary: "01 ";
|
|
130
|
+
/** Minimal contrast */
|
|
131
|
+
readonly minimal: "#. ";
|
|
132
|
+
/** Matrix/Katakana style */
|
|
133
|
+
readonly matrix: "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ123457890:・.=*+-<>¦|";
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Default configuration for ASCII art generation
|
|
138
|
+
*/
|
|
139
|
+
export declare const DEFAULT_CONFIG: ASCIIRenderConfig;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Approximate width/height ratio for each font
|
|
143
|
+
*/
|
|
144
|
+
export declare const FONT_RATIOS: Record<string, number>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Predefined monospace font families
|
|
148
|
+
*/
|
|
149
|
+
export declare const FONTS: {
|
|
150
|
+
readonly fira: "'Fira Code', monospace";
|
|
151
|
+
readonly vt323: "'VT323', monospace";
|
|
152
|
+
readonly roboto: "'Roboto Mono', monospace";
|
|
153
|
+
readonly source: "'Source Code Pro', monospace";
|
|
154
|
+
readonly courier: "'Courier New', monospace";
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate ASCII art from an HTMLImageElement
|
|
159
|
+
*/
|
|
160
|
+
export declare const generateAscii: (image: HTMLImageElement, config: ASCIIRenderConfig) => ProcessingResult;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate a canvas from ASCII result
|
|
164
|
+
*/
|
|
165
|
+
export declare const generateCanvasFromAscii: (result: ProcessingResult, config: ASCIIRenderConfig, customScale?: number) => HTMLCanvasElement;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate HTML string from ASCII result
|
|
169
|
+
*/
|
|
170
|
+
export declare const generateHTML: (result: ProcessingResult, config: ASCIIRenderConfig) => string;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate SVG string from ASCII result
|
|
174
|
+
*/
|
|
175
|
+
export declare const generateSVG: (result: ProcessingResult, config: ASCIIRenderConfig) => string;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load an image from a URL
|
|
179
|
+
*/
|
|
180
|
+
export declare const loadImage: (src: string) => Promise<HTMLImageElement>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Load an image from a Blob
|
|
184
|
+
*/
|
|
185
|
+
export declare const loadImageFromBlob: (blob: Blob) => Promise<HTMLImageElement>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Predefined color palettes for palette mode
|
|
189
|
+
*/
|
|
190
|
+
export declare const PALETTE_PRESETS: {
|
|
191
|
+
/** Classic terminal green */
|
|
192
|
+
readonly retro_term: readonly ["#000000", "#00ff00", "#008800"];
|
|
193
|
+
/** Game Boy 4-color palette */
|
|
194
|
+
readonly gameboy: readonly ["#0f380f", "#306230", "#8bac0f", "#9bbc0f"];
|
|
195
|
+
/** CGA 16-color palette (subset) */
|
|
196
|
+
readonly cga: readonly ["#000000", "#555555", "#ffffff", "#ff5555", "#ff55ff", "#55ffff"];
|
|
197
|
+
/** Vaporwave aesthetic */
|
|
198
|
+
readonly vaporwave: readonly ["#ff71ce", "#01cdfe", "#05ffa1", "#b967ff", "#fffb96", "#000000"];
|
|
199
|
+
/** Grayscale gradient */
|
|
200
|
+
readonly grayscale: readonly ["#000000", "#333333", "#666666", "#999999", "#cccccc", "#ffffff"];
|
|
201
|
+
/** AMOLED high contrast */
|
|
202
|
+
readonly amoled: readonly ["#000000", "#ffffff"];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Result of ASCII art processing
|
|
207
|
+
*/
|
|
208
|
+
export declare interface ProcessingResult {
|
|
209
|
+
/** Full ASCII text with newlines */
|
|
210
|
+
text: string;
|
|
211
|
+
/** Array of lines */
|
|
212
|
+
lines: string[];
|
|
213
|
+
/** Width in characters */
|
|
214
|
+
width: number;
|
|
215
|
+
/** Height in lines */
|
|
216
|
+
height: number;
|
|
217
|
+
/** Color data for colored modes (RGBA array) */
|
|
218
|
+
colorData?: Uint8ClampedArray;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Read a File as a data URL
|
|
223
|
+
*/
|
|
224
|
+
export declare const readFileAsDataURL: (file: File) => Promise<string>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Hook to get a typed ref for ASCIIR component
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* const asciiRef = useASCIIRender();
|
|
232
|
+
*
|
|
233
|
+
* return (
|
|
234
|
+
* <ASCIIR ref={asciiRef} src={imageSrc} />
|
|
235
|
+
* );
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export declare const useASCIIRender: () => RefObject<ASCIIRenderRef>;
|
|
239
|
+
|
|
240
|
+
export { }
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asciirender/asciir",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A powerful React component for converting images to beautiful ASCII art with customizable settings",
|
|
5
|
+
"author": "dvinay",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ACIIRenderer/ASCIIR.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ACIIRenderer/ASCIIR#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ACIIRenderer/ASCIIR/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ascii",
|
|
21
|
+
"ascii-art",
|
|
22
|
+
"image-processing",
|
|
23
|
+
"react",
|
|
24
|
+
"react-component",
|
|
25
|
+
"image-to-ascii",
|
|
26
|
+
"text-art",
|
|
27
|
+
"canvas",
|
|
28
|
+
"svg",
|
|
29
|
+
"typescript",
|
|
30
|
+
"image-converter"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "./dist/asciir.umd.cjs",
|
|
34
|
+
"module": "./dist/asciir.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/asciir.js",
|
|
40
|
+
"require": "./dist/asciir.umd.cjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "vite --config vite.demo.config.ts",
|
|
49
|
+
"build": "npm run build:lib",
|
|
50
|
+
"build:lib": "tsc -p tsconfig.lib.json && vite build --config vite.config.ts",
|
|
51
|
+
"build:demo": "vite build --config vite.demo.config.ts",
|
|
52
|
+
"preview": "vite preview --config vite.demo.config.ts",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"prepublishOnly": "npm run build:lib",
|
|
55
|
+
"size": "npm run build:lib && npx size-limit"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": ">=17.0.0",
|
|
59
|
+
"react-dom": ">=17.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^22.14.0",
|
|
63
|
+
"@types/react": "^18.2.0",
|
|
64
|
+
"@types/react-dom": "^18.2.0",
|
|
65
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
66
|
+
"lucide-react": "^0.562.0",
|
|
67
|
+
"react": "^19.2.3",
|
|
68
|
+
"react-dom": "^19.2.3",
|
|
69
|
+
"react-router-dom": "^7.12.0",
|
|
70
|
+
"typescript": "~5.8.2",
|
|
71
|
+
"vite": "^6.2.0",
|
|
72
|
+
"vite-plugin-dts": "^4.5.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|