@silvery/examples 0.17.3 → 0.17.5
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/UPNG-ShUlaTDh.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
- package/dist/_banner-A70_y2Vi.mjs +43 -0
- package/dist/ansi-0VXlUmNn.mjs +16397 -0
- package/dist/apng-B0gRaDVT.mjs +3 -0
- package/dist/apng-BTRDTfDW.mjs +68 -0
- package/dist/apps/aichat/index.mjs +1298 -0
- package/dist/apps/app-todo.mjs +138 -0
- package/dist/apps/async-data.mjs +203 -0
- package/dist/apps/cli-wizard.mjs +338 -0
- package/dist/apps/clipboard.mjs +197 -0
- package/dist/apps/components.mjs +863 -0
- package/dist/apps/data-explorer.mjs +482 -0
- package/dist/apps/dev-tools.mjs +396 -0
- package/dist/apps/explorer.mjs +697 -0
- package/dist/apps/gallery.mjs +765 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +279 -0
- package/dist/apps/layout-ref.mjs +186 -0
- package/dist/apps/outline.mjs +202 -0
- package/dist/apps/paste-demo.mjs +188 -0
- package/dist/apps/scroll.mjs +85 -0
- package/dist/apps/search-filter.mjs +286 -0
- package/dist/apps/selection.mjs +354 -0
- package/dist/apps/spatial-focus-demo.mjs +387 -0
- package/dist/apps/task-list.mjs +257 -0
- package/dist/apps/terminal-caps-demo.mjs +314 -0
- package/dist/apps/terminal.mjs +871 -0
- package/dist/apps/text-selection-demo.mjs +253 -0
- package/dist/apps/textarea.mjs +177 -0
- package/dist/apps/theme.mjs +660 -0
- package/dist/apps/transform.mjs +214 -0
- package/dist/apps/virtual-10k.mjs +421 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Dj-11kZF.mjs +1179 -0
- package/dist/backends-U3QwStfO.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +47 -0
- package/dist/components/hello.mjs +30 -0
- package/dist/components/progress-bar.mjs +58 -0
- package/dist/components/select-list.mjs +84 -0
- package/dist/components/spinner.mjs +56 -0
- package/dist/components/text-input.mjs +61 -0
- package/dist/components/virtual-list.mjs +50 -0
- package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
- package/dist/gif-B6NGH5gs.mjs +3 -0
- package/dist/gif-CfkOF-iG.mjs +71 -0
- package/dist/gifenc-BI4ihP_T.mjs +728 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1203 -0
- package/dist/layout/live-resize.mjs +302 -0
- package/dist/layout/overflow.mjs +69 -0
- package/dist/layout/text-layout.mjs +334 -0
- package/dist/node-nsrAOjH4.mjs +1083 -0
- package/dist/plugins-CT0DdV_E.mjs +3056 -0
- package/dist/resvg-js-Cnk2o49d.mjs +201 -0
- package/dist/src-9ZhfQyzD.mjs +814 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-jO3Zuzjj.mjs +23538 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
- package/package.json +21 -14
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
package/apps/gallery.tsx
DELETED
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gallery — Kitty Images, Pixel Art, and Truecolor Rendering
|
|
3
|
-
*
|
|
4
|
-
* A tabbed demo combining three visual rendering techniques:
|
|
5
|
-
* 1. Images — Browse/display images using the Kitty graphics protocol
|
|
6
|
-
* 2. Paint — Half-block pixel art canvas with mouse drawing and RGB color picker
|
|
7
|
-
* 3. Truecolor — Full truecolor spectrum, HSL rainbows, and 256-color palette
|
|
8
|
-
*
|
|
9
|
-
* Run: bun vendor/silvery/examples/apps/gallery.tsx
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { JSX } from "react"
|
|
13
|
-
import { deflateSync } from "node:zlib"
|
|
14
|
-
import React, { useState, useMemo } from "react"
|
|
15
|
-
import {
|
|
16
|
-
render,
|
|
17
|
-
Box,
|
|
18
|
-
Text,
|
|
19
|
-
Image,
|
|
20
|
-
Tabs,
|
|
21
|
-
TabList,
|
|
22
|
-
Tab,
|
|
23
|
-
TabPanel,
|
|
24
|
-
Kbd,
|
|
25
|
-
Muted,
|
|
26
|
-
H2,
|
|
27
|
-
useInput,
|
|
28
|
-
useApp,
|
|
29
|
-
useBoxRect,
|
|
30
|
-
createTerm,
|
|
31
|
-
type Key,
|
|
32
|
-
} from "silvery"
|
|
33
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
34
|
-
|
|
35
|
-
export const meta: ExampleMeta = {
|
|
36
|
-
name: "Gallery",
|
|
37
|
-
description: "Kitty images, pixel art, and truecolor rendering",
|
|
38
|
-
demo: true,
|
|
39
|
-
features: ["Image", "Kitty graphics", "half-block", "truecolor", "mouse input"],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// Color Utilities
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
type RGB = [number, number, number]
|
|
47
|
-
|
|
48
|
-
/** HSV to RGB (h: 0-360, s/v: 0-1) */
|
|
49
|
-
function hsvToRgb(h: number, s: number, v: number): RGB {
|
|
50
|
-
const c = v * s
|
|
51
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
52
|
-
const m = v - c
|
|
53
|
-
let r = 0,
|
|
54
|
-
g = 0,
|
|
55
|
-
b = 0
|
|
56
|
-
if (h < 60) {
|
|
57
|
-
r = c
|
|
58
|
-
g = x
|
|
59
|
-
} else if (h < 120) {
|
|
60
|
-
r = x
|
|
61
|
-
g = c
|
|
62
|
-
} else if (h < 180) {
|
|
63
|
-
g = c
|
|
64
|
-
b = x
|
|
65
|
-
} else if (h < 240) {
|
|
66
|
-
g = x
|
|
67
|
-
b = c
|
|
68
|
-
} else if (h < 300) {
|
|
69
|
-
r = x
|
|
70
|
-
b = c
|
|
71
|
-
} else {
|
|
72
|
-
r = c
|
|
73
|
-
b = x
|
|
74
|
-
}
|
|
75
|
-
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** HSL to RGB (h: 0-360, s/l: 0-1) */
|
|
79
|
-
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
80
|
-
const c = (1 - Math.abs(2 * l - 1)) * s
|
|
81
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
82
|
-
const m = l - c / 2
|
|
83
|
-
let r1: number, g1: number, b1: number
|
|
84
|
-
if (h < 60) [r1, g1, b1] = [c, x, 0]
|
|
85
|
-
else if (h < 120) [r1, g1, b1] = [x, c, 0]
|
|
86
|
-
else if (h < 180) [r1, g1, b1] = [0, c, x]
|
|
87
|
-
else if (h < 240) [r1, g1, b1] = [0, x, c]
|
|
88
|
-
else if (h < 300) [r1, g1, b1] = [x, 0, c]
|
|
89
|
-
else [r1, g1, b1] = [c, 0, x]
|
|
90
|
-
return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ============================================================================
|
|
94
|
-
// PNG Generation (in-memory, no external files)
|
|
95
|
-
// ============================================================================
|
|
96
|
-
|
|
97
|
-
function crc32(data: Buffer): number {
|
|
98
|
-
let crc = 0xffffffff
|
|
99
|
-
for (let i = 0; i < data.length; i++) {
|
|
100
|
-
crc ^= data[i]!
|
|
101
|
-
for (let j = 0; j < 8; j++) {
|
|
102
|
-
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return (crc ^ 0xffffffff) >>> 0
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function makeChunk(type: string, data: Buffer): Buffer {
|
|
109
|
-
const len = Buffer.alloc(4)
|
|
110
|
-
len.writeUInt32BE(data.length)
|
|
111
|
-
const typeBytes = Buffer.from(type, "ascii")
|
|
112
|
-
const payload = Buffer.concat([typeBytes, data])
|
|
113
|
-
const crc = crc32(payload)
|
|
114
|
-
const crcBuf = Buffer.alloc(4)
|
|
115
|
-
crcBuf.writeUInt32BE(crc >>> 0)
|
|
116
|
-
return Buffer.concat([len, payload, crcBuf])
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function encodePng(width: number, height: number, pixelFn: (x: number, y: number) => RGB): Buffer {
|
|
120
|
-
const rawData = Buffer.alloc(height * (1 + width * 4))
|
|
121
|
-
for (let y = 0; y < height; y++) {
|
|
122
|
-
const rowOffset = y * (1 + width * 4)
|
|
123
|
-
rawData[rowOffset] = 0
|
|
124
|
-
for (let x = 0; x < width; x++) {
|
|
125
|
-
const [r, g, b] = pixelFn(x, y)
|
|
126
|
-
const off = rowOffset + 1 + x * 4
|
|
127
|
-
rawData[off] = r
|
|
128
|
-
rawData[off + 1] = g
|
|
129
|
-
rawData[off + 2] = b
|
|
130
|
-
rawData[off + 3] = 255
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const compressed = deflateSync(rawData)
|
|
134
|
-
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
|
135
|
-
const ihdr = Buffer.alloc(13)
|
|
136
|
-
ihdr.writeUInt32BE(width, 0)
|
|
137
|
-
ihdr.writeUInt32BE(height, 4)
|
|
138
|
-
ihdr[8] = 8
|
|
139
|
-
ihdr[9] = 6
|
|
140
|
-
return Buffer.concat([
|
|
141
|
-
signature,
|
|
142
|
-
makeChunk("IHDR", ihdr),
|
|
143
|
-
makeChunk("IDAT", compressed),
|
|
144
|
-
makeChunk("IEND", Buffer.alloc(0)),
|
|
145
|
-
])
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ============================================================================
|
|
149
|
-
// Sample Image Generators
|
|
150
|
-
// ============================================================================
|
|
151
|
-
|
|
152
|
-
interface GalleryImage {
|
|
153
|
-
name: string
|
|
154
|
-
description: string
|
|
155
|
-
png: Buffer
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function generateRainbow(w: number, h: number): Buffer {
|
|
159
|
-
return encodePng(w, h, (x, y) => {
|
|
160
|
-
const hue = (x / w) * 360
|
|
161
|
-
const sat = 0.7 + 0.3 * Math.sin((y / h) * Math.PI)
|
|
162
|
-
const val = 0.5 + 0.5 * Math.cos((y / h) * Math.PI * 2)
|
|
163
|
-
return hsvToRgb(hue, sat, val)
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function generatePlasma(w: number, h: number): Buffer {
|
|
168
|
-
return encodePng(w, h, (x, y) => {
|
|
169
|
-
const nx = x / w
|
|
170
|
-
const ny = y / h
|
|
171
|
-
const v1 = Math.sin(nx * 10 + ny * 3)
|
|
172
|
-
const v2 = Math.sin(nx * 5 - ny * 8 + 2)
|
|
173
|
-
const v3 = Math.sin(Math.sqrt((nx - 0.5) ** 2 + (ny - 0.5) ** 2) * 15)
|
|
174
|
-
const v = (v1 + v2 + v3) / 3
|
|
175
|
-
const hue = ((v + 1) / 2) * 360
|
|
176
|
-
return hslToRgb(hue, 0.9, 0.55)
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function generateMandelbrot(w: number, h: number): Buffer {
|
|
181
|
-
return encodePng(w, h, (x, y) => {
|
|
182
|
-
const cx = (x / w) * 3.5 - 2.5
|
|
183
|
-
const cy = (y / h) * 2.0 - 1.0
|
|
184
|
-
let zx = 0,
|
|
185
|
-
zy = 0
|
|
186
|
-
let i = 0
|
|
187
|
-
const maxIter = 80
|
|
188
|
-
while (zx * zx + zy * zy < 4 && i < maxIter) {
|
|
189
|
-
const tmp = zx * zx - zy * zy + cx
|
|
190
|
-
zy = 2 * zx * zy + cy
|
|
191
|
-
zx = tmp
|
|
192
|
-
i++
|
|
193
|
-
}
|
|
194
|
-
if (i === maxIter) return [0, 0, 0] as RGB
|
|
195
|
-
const hue = (i / maxIter) * 360
|
|
196
|
-
return hslToRgb(hue, 1.0, 0.5)
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function generateGradientGrid(w: number, h: number): Buffer {
|
|
201
|
-
return encodePng(w, h, (x, y) => {
|
|
202
|
-
const r = Math.round((x / w) * 255)
|
|
203
|
-
const g = Math.round((y / h) * 255)
|
|
204
|
-
const b = Math.round(255 - ((x + y) / (w + h)) * 255)
|
|
205
|
-
return [r, g, b]
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function generateCheckerPattern(w: number, h: number): Buffer {
|
|
210
|
-
const size = 16
|
|
211
|
-
return encodePng(w, h, (x, y) => {
|
|
212
|
-
const cx = Math.floor(x / size)
|
|
213
|
-
const cy = Math.floor(y / size)
|
|
214
|
-
const hue = ((cx + cy) * 30) % 360
|
|
215
|
-
const isLight = (cx + cy) % 2 === 0
|
|
216
|
-
return hslToRgb(hue, 0.8, isLight ? 0.6 : 0.35)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ============================================================================
|
|
221
|
-
// Tab 1: Images — Browse gallery of generated images
|
|
222
|
-
// ============================================================================
|
|
223
|
-
|
|
224
|
-
function ImagesTab() {
|
|
225
|
-
const rect = useBoxRect()
|
|
226
|
-
const w = Math.max(20, rect.width - 4)
|
|
227
|
-
const imgH = Math.max(5, rect.height - 6)
|
|
228
|
-
|
|
229
|
-
const images: GalleryImage[] = useMemo(() => {
|
|
230
|
-
const pw = 256
|
|
231
|
-
const ph = 192
|
|
232
|
-
return [
|
|
233
|
-
{ name: "Rainbow", description: "HSV color wheel gradient", png: generateRainbow(pw, ph) },
|
|
234
|
-
{ name: "Plasma", description: "Sine-wave plasma interference", png: generatePlasma(pw, ph) },
|
|
235
|
-
{
|
|
236
|
-
name: "Mandelbrot",
|
|
237
|
-
description: "Fractal escape-time coloring",
|
|
238
|
-
png: generateMandelbrot(pw, ph),
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
name: "RGB Cube",
|
|
242
|
-
description: "Red-Green-Blue gradient grid",
|
|
243
|
-
png: generateGradientGrid(pw, ph),
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: "Checker",
|
|
247
|
-
description: "Hue-shifted checkerboard",
|
|
248
|
-
png: generateCheckerPattern(pw, ph),
|
|
249
|
-
},
|
|
250
|
-
]
|
|
251
|
-
}, [])
|
|
252
|
-
|
|
253
|
-
const [index, setIndex] = useState(0)
|
|
254
|
-
const img = images[index]!
|
|
255
|
-
|
|
256
|
-
useInput((input: string, key: Key) => {
|
|
257
|
-
if (input === "j" || key.downArrow || input === "n") {
|
|
258
|
-
setIndex((i) => (i + 1) % images.length)
|
|
259
|
-
}
|
|
260
|
-
if (input === "k" || key.upArrow || input === "p") {
|
|
261
|
-
setIndex((i) => (i - 1 + images.length) % images.length)
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
<Box flexDirection="column" flexGrow={1} gap={1}>
|
|
267
|
-
<Box paddingX={1} gap={2}>
|
|
268
|
-
<Text bold color="$primary">
|
|
269
|
-
{img.name}
|
|
270
|
-
</Text>
|
|
271
|
-
<Muted>{img.description}</Muted>
|
|
272
|
-
<Muted>
|
|
273
|
-
({index + 1}/{images.length})
|
|
274
|
-
</Muted>
|
|
275
|
-
</Box>
|
|
276
|
-
<Box flexGrow={1} justifyContent="center" paddingX={1}>
|
|
277
|
-
<Image
|
|
278
|
-
src={img.png}
|
|
279
|
-
width={w}
|
|
280
|
-
height={imgH}
|
|
281
|
-
fallback={`[${img.name} — graphics protocol not available. Run in Kitty/WezTerm/Ghostty for images.]`}
|
|
282
|
-
/>
|
|
283
|
-
</Box>
|
|
284
|
-
<Muted>
|
|
285
|
-
{" "}
|
|
286
|
-
<Kbd>j/k</Kbd> navigate images
|
|
287
|
-
</Muted>
|
|
288
|
-
</Box>
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ============================================================================
|
|
293
|
-
// Tab 2: Paint — Half-block pixel art canvas
|
|
294
|
-
// ============================================================================
|
|
295
|
-
|
|
296
|
-
const UPPER_HALF = "\u2580"
|
|
297
|
-
const LOWER_HALF = "\u2584"
|
|
298
|
-
const FULL_BLOCK = "\u2588"
|
|
299
|
-
|
|
300
|
-
const PAINT_PRESETS: { name: string; color: RGB }[] = [
|
|
301
|
-
{ name: "white", color: [255, 255, 255] },
|
|
302
|
-
{ name: "red", color: [255, 0, 0] },
|
|
303
|
-
{ name: "orange", color: [255, 165, 0] },
|
|
304
|
-
{ name: "yellow", color: [255, 255, 0] },
|
|
305
|
-
{ name: "green", color: [0, 200, 0] },
|
|
306
|
-
{ name: "cyan", color: [0, 255, 255] },
|
|
307
|
-
{ name: "blue", color: [0, 100, 255] },
|
|
308
|
-
{ name: "magenta", color: [200, 0, 200] },
|
|
309
|
-
{ name: "pink", color: [255, 128, 200] },
|
|
310
|
-
{ name: "black", color: [30, 30, 30] },
|
|
311
|
-
]
|
|
312
|
-
|
|
313
|
-
function PaintTab() {
|
|
314
|
-
const rect = useBoxRect()
|
|
315
|
-
const canvasW = Math.max(10, rect.width - 2)
|
|
316
|
-
const canvasTermH = Math.max(4, rect.height - 7)
|
|
317
|
-
const canvasPixH = canvasTermH * 2
|
|
318
|
-
|
|
319
|
-
const [pixels, setPixels] = useState<(RGB | null)[][]>(() => {
|
|
320
|
-
const rows: (RGB | null)[][] = []
|
|
321
|
-
for (let y = 0; y < canvasPixH; y++) rows.push(new Array(canvasW).fill(null))
|
|
322
|
-
// Seed with a colorful spiral pattern so the demo looks great on first render
|
|
323
|
-
const cx = Math.floor(canvasW / 2)
|
|
324
|
-
const cy = Math.floor(canvasPixH / 2)
|
|
325
|
-
const radius = Math.min(cx, cy) - 2
|
|
326
|
-
for (let angle = 0; angle < 720; angle += 2) {
|
|
327
|
-
const r = (angle / 720) * radius
|
|
328
|
-
const rad = (angle * Math.PI) / 180
|
|
329
|
-
const px = Math.round(cx + r * Math.cos(rad))
|
|
330
|
-
const py = Math.round(cy + r * Math.sin(rad))
|
|
331
|
-
const hue = angle % 360
|
|
332
|
-
const color = hslToRgb(hue, 0.9, 0.55)
|
|
333
|
-
// Draw a small dot (2px radius)
|
|
334
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
335
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
336
|
-
const x = px + dx
|
|
337
|
-
const y = py + dy
|
|
338
|
-
if (x >= 0 && x < canvasW && y >= 0 && y < canvasPixH) {
|
|
339
|
-
rows[y]![x] = color
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return rows
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
const [colorIndex, setColorIndex] = useState(1) // red
|
|
348
|
-
const [tool, setTool] = useState<"pen" | "eraser">("pen")
|
|
349
|
-
const currentColor = PAINT_PRESETS[colorIndex]!.color
|
|
350
|
-
|
|
351
|
-
// Handle keyboard: color presets, tool toggle, clear
|
|
352
|
-
useInput((input: string) => {
|
|
353
|
-
if (input >= "1" && input <= "9") {
|
|
354
|
-
setColorIndex(Number(input) - 1)
|
|
355
|
-
setTool("pen")
|
|
356
|
-
} else if (input === "0") {
|
|
357
|
-
setColorIndex(9)
|
|
358
|
-
setTool("pen")
|
|
359
|
-
} else if (input === "e") {
|
|
360
|
-
setTool((t) => (t === "eraser" ? "pen" : "eraser"))
|
|
361
|
-
} else if (input === "c") {
|
|
362
|
-
setPixels((prev) => prev.map((row) => row.map(() => null)))
|
|
363
|
-
}
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
// Render canvas as half-block characters
|
|
367
|
-
const canvasLines: JSX.Element[] = []
|
|
368
|
-
for (let row = 0; row < canvasTermH; row++) {
|
|
369
|
-
const cells: JSX.Element[] = []
|
|
370
|
-
for (let col = 0; col < canvasW; col++) {
|
|
371
|
-
const top = row * 2 < pixels.length ? (pixels[row * 2]?.[col] ?? null) : null
|
|
372
|
-
const bot = row * 2 + 1 < pixels.length ? (pixels[row * 2 + 1]?.[col] ?? null) : null
|
|
373
|
-
|
|
374
|
-
if (top === null && bot === null) {
|
|
375
|
-
cells.push(<Text key={col}> </Text>)
|
|
376
|
-
} else if (top !== null && bot === null) {
|
|
377
|
-
cells.push(
|
|
378
|
-
<Text key={col} color={`rgb(${top[0]},${top[1]},${top[2]})`}>
|
|
379
|
-
{UPPER_HALF}
|
|
380
|
-
</Text>,
|
|
381
|
-
)
|
|
382
|
-
} else if (top === null && bot !== null) {
|
|
383
|
-
cells.push(
|
|
384
|
-
<Text key={col} color={`rgb(${bot[0]},${bot[1]},${bot[2]})`}>
|
|
385
|
-
{LOWER_HALF}
|
|
386
|
-
</Text>,
|
|
387
|
-
)
|
|
388
|
-
} else if (top !== null && top[0] === bot?.[0] && top[1] === bot[1] && top[2] === bot[2]) {
|
|
389
|
-
cells.push(
|
|
390
|
-
<Text key={col} color={`rgb(${top[0]},${top[1]},${top[2]})`}>
|
|
391
|
-
{FULL_BLOCK}
|
|
392
|
-
</Text>,
|
|
393
|
-
)
|
|
394
|
-
} else {
|
|
395
|
-
cells.push(
|
|
396
|
-
<Text
|
|
397
|
-
key={col}
|
|
398
|
-
color={`rgb(${top![0]},${top![1]},${top![2]})`}
|
|
399
|
-
backgroundColor={`rgb(${bot![0]},${bot![1]},${bot![2]})`}
|
|
400
|
-
>
|
|
401
|
-
{UPPER_HALF}
|
|
402
|
-
</Text>,
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
canvasLines.push(<Box key={row}>{cells}</Box>)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Color palette bar
|
|
410
|
-
const paletteItems = PAINT_PRESETS.map((p, i) => {
|
|
411
|
-
const selected = i === colorIndex
|
|
412
|
-
return (
|
|
413
|
-
<Text
|
|
414
|
-
key={i}
|
|
415
|
-
backgroundColor={`rgb(${p.color[0]},${p.color[1]},${p.color[2]})`}
|
|
416
|
-
color={p.color[0] + p.color[1] + p.color[2] > 384 ? "black" : "white"}
|
|
417
|
-
bold={selected}
|
|
418
|
-
>
|
|
419
|
-
{selected ? `[${(i + 1) % 10}]` : ` ${(i + 1) % 10} `}
|
|
420
|
-
</Text>
|
|
421
|
-
)
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
const toolLabel = tool === "pen" ? "Pen" : "Eraser"
|
|
425
|
-
const [cr, cg, cb] = currentColor
|
|
426
|
-
|
|
427
|
-
return (
|
|
428
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
429
|
-
<Box paddingX={1} gap={2}>
|
|
430
|
-
<Text bold color={`rgb(${cr},${cg},${cb})`}>
|
|
431
|
-
{toolLabel}
|
|
432
|
-
</Text>
|
|
433
|
-
<Text backgroundColor={`rgb(${cr},${cg},${cb})`}>{" "}</Text>
|
|
434
|
-
<Muted>
|
|
435
|
-
rgb({cr},{cg},{cb})
|
|
436
|
-
</Muted>
|
|
437
|
-
</Box>
|
|
438
|
-
|
|
439
|
-
<Box flexDirection="column" flexGrow={1} borderStyle="round" marginX={1}>
|
|
440
|
-
<Box flexDirection="column">{canvasLines}</Box>
|
|
441
|
-
</Box>
|
|
442
|
-
|
|
443
|
-
<Box paddingX={1} gap={0}>
|
|
444
|
-
{paletteItems}
|
|
445
|
-
</Box>
|
|
446
|
-
|
|
447
|
-
<Muted>
|
|
448
|
-
{" "}
|
|
449
|
-
<Kbd>1-0</Kbd> color <Kbd>e</Kbd> eraser <Kbd>c</Kbd> clear (click canvas in Kitty/Ghostty for mouse paint)
|
|
450
|
-
</Muted>
|
|
451
|
-
</Box>
|
|
452
|
-
)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ============================================================================
|
|
456
|
-
// Tab 3: Truecolor — Spectrum display
|
|
457
|
-
// ============================================================================
|
|
458
|
-
|
|
459
|
-
function TruecolorTab() {
|
|
460
|
-
const rect = useBoxRect()
|
|
461
|
-
const w = Math.max(20, rect.width - 4)
|
|
462
|
-
const availH = Math.max(10, rect.height - 3)
|
|
463
|
-
|
|
464
|
-
// Distribute vertical space among sections
|
|
465
|
-
const hueBarH = Math.min(3, Math.max(1, Math.floor(availH * 0.15)))
|
|
466
|
-
const gradientH = Math.min(8, Math.max(2, Math.floor(availH * 0.35)))
|
|
467
|
-
const paletteH = Math.min(4, Math.max(2, Math.floor(availH * 0.2)))
|
|
468
|
-
|
|
469
|
-
// HSL Hue rainbow bar — each column is a hue
|
|
470
|
-
const hueBar: JSX.Element[] = []
|
|
471
|
-
for (let row = 0; row < hueBarH; row++) {
|
|
472
|
-
const cells: JSX.Element[] = []
|
|
473
|
-
for (let col = 0; col < w; col++) {
|
|
474
|
-
const hue = (col / w) * 360
|
|
475
|
-
const lightness = 0.35 + (row / Math.max(1, hueBarH - 1)) * 0.3
|
|
476
|
-
const [r, g, b] = hslToRgb(hue, 1.0, lightness)
|
|
477
|
-
cells.push(
|
|
478
|
-
<Text key={col} backgroundColor={`rgb(${r},${g},${b})`}>
|
|
479
|
-
{" "}
|
|
480
|
-
</Text>,
|
|
481
|
-
)
|
|
482
|
-
}
|
|
483
|
-
hueBar.push(<Box key={row}>{cells}</Box>)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Saturation/brightness gradient — rows vary saturation, columns vary hue
|
|
487
|
-
const gradient: JSX.Element[] = []
|
|
488
|
-
for (let row = 0; row < gradientH; row++) {
|
|
489
|
-
const cells: JSX.Element[] = []
|
|
490
|
-
const sat = 1.0 - (row / Math.max(1, gradientH - 1)) * 0.8
|
|
491
|
-
for (let col = 0; col < w; col++) {
|
|
492
|
-
const hue = (col / w) * 360
|
|
493
|
-
const [r, g, b] = hsvToRgb(hue, sat, 0.95)
|
|
494
|
-
cells.push(
|
|
495
|
-
<Text key={col} backgroundColor={`rgb(${r},${g},${b})`}>
|
|
496
|
-
{" "}
|
|
497
|
-
</Text>,
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
gradient.push(<Box key={row}>{cells}</Box>)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// 256-color ANSI palette grid (16 columns x rows)
|
|
504
|
-
const paletteCols = 16
|
|
505
|
-
const paletteRows = Math.min(paletteH, Math.ceil(256 / paletteCols))
|
|
506
|
-
const palette: JSX.Element[] = []
|
|
507
|
-
for (let row = 0; row < paletteRows; row++) {
|
|
508
|
-
const cells: JSX.Element[] = []
|
|
509
|
-
const cellW = Math.max(1, Math.floor(w / paletteCols))
|
|
510
|
-
for (let col = 0; col < paletteCols; col++) {
|
|
511
|
-
const idx = row * paletteCols + col
|
|
512
|
-
if (idx >= 256) break
|
|
513
|
-
// Convert 256-color index to RGB
|
|
514
|
-
const [r, g, b] = ansi256toRgb(idx)
|
|
515
|
-
const label = idx.toString().padStart(3)
|
|
516
|
-
const textColor = r + g + b > 384 ? "black" : "white"
|
|
517
|
-
cells.push(
|
|
518
|
-
<Box key={col} width={cellW}>
|
|
519
|
-
<Text backgroundColor={`rgb(${r},${g},${b})`} color={textColor}>
|
|
520
|
-
{label.slice(0, cellW)}
|
|
521
|
-
</Text>
|
|
522
|
-
</Box>,
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
|
-
palette.push(<Box key={row}>{cells}</Box>)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Grayscale ramp
|
|
529
|
-
const grayCells: JSX.Element[] = []
|
|
530
|
-
for (let col = 0; col < w; col++) {
|
|
531
|
-
const v = Math.round((col / Math.max(1, w - 1)) * 255)
|
|
532
|
-
grayCells.push(
|
|
533
|
-
<Text key={col} backgroundColor={`rgb(${v},${v},${v})`}>
|
|
534
|
-
{" "}
|
|
535
|
-
</Text>,
|
|
536
|
-
)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return (
|
|
540
|
-
<Box flexDirection="column" flexGrow={1} gap={1} paddingX={1}>
|
|
541
|
-
<Box flexDirection="column">
|
|
542
|
-
<H2>HSL Rainbow</H2>
|
|
543
|
-
<Box flexDirection="column">{hueBar}</Box>
|
|
544
|
-
</Box>
|
|
545
|
-
|
|
546
|
-
<Box flexDirection="column">
|
|
547
|
-
<H2>Saturation Gradient</H2>
|
|
548
|
-
<Box flexDirection="column">{gradient}</Box>
|
|
549
|
-
</Box>
|
|
550
|
-
|
|
551
|
-
<Box flexDirection="column">
|
|
552
|
-
<H2>256-Color Palette</H2>
|
|
553
|
-
<Box flexDirection="column">{palette}</Box>
|
|
554
|
-
</Box>
|
|
555
|
-
|
|
556
|
-
<Box flexDirection="column">
|
|
557
|
-
<H2>Grayscale Ramp</H2>
|
|
558
|
-
<Box>{grayCells}</Box>
|
|
559
|
-
</Box>
|
|
560
|
-
</Box>
|
|
561
|
-
)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/** Convert ANSI 256-color index to RGB */
|
|
565
|
-
function ansi256toRgb(idx: number): RGB {
|
|
566
|
-
if (idx < 16) {
|
|
567
|
-
// Standard 16 colors (approximate)
|
|
568
|
-
const table: RGB[] = [
|
|
569
|
-
[0, 0, 0],
|
|
570
|
-
[128, 0, 0],
|
|
571
|
-
[0, 128, 0],
|
|
572
|
-
[128, 128, 0],
|
|
573
|
-
[0, 0, 128],
|
|
574
|
-
[128, 0, 128],
|
|
575
|
-
[0, 128, 128],
|
|
576
|
-
[192, 192, 192],
|
|
577
|
-
[128, 128, 128],
|
|
578
|
-
[255, 0, 0],
|
|
579
|
-
[0, 255, 0],
|
|
580
|
-
[255, 255, 0],
|
|
581
|
-
[0, 0, 255],
|
|
582
|
-
[255, 0, 255],
|
|
583
|
-
[0, 255, 255],
|
|
584
|
-
[255, 255, 255],
|
|
585
|
-
]
|
|
586
|
-
return table[idx]!
|
|
587
|
-
}
|
|
588
|
-
if (idx < 232) {
|
|
589
|
-
// 6x6x6 color cube
|
|
590
|
-
const i = idx - 16
|
|
591
|
-
const r = Math.floor(i / 36)
|
|
592
|
-
const g = Math.floor((i % 36) / 6)
|
|
593
|
-
const b = i % 6
|
|
594
|
-
return [r ? r * 40 + 55 : 0, g ? g * 40 + 55 : 0, b ? b * 40 + 55 : 0]
|
|
595
|
-
}
|
|
596
|
-
// Grayscale ramp (232-255)
|
|
597
|
-
const v = (idx - 232) * 10 + 8
|
|
598
|
-
return [v, v, v]
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// ============================================================================
|
|
602
|
-
// Main Gallery App
|
|
603
|
-
// ============================================================================
|
|
604
|
-
|
|
605
|
-
export function Gallery() {
|
|
606
|
-
const { exit } = useApp()
|
|
607
|
-
const [activeTab, setActiveTab] = useState("images")
|
|
608
|
-
|
|
609
|
-
useInput((input: string, key: Key) => {
|
|
610
|
-
if (input === "q" || key.escape) exit()
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
return (
|
|
614
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
615
|
-
<Tabs value={activeTab} onChange={setActiveTab}>
|
|
616
|
-
<TabList>
|
|
617
|
-
<Tab value="images">Images</Tab>
|
|
618
|
-
<Tab value="paint">Paint</Tab>
|
|
619
|
-
<Tab value="truecolor">Truecolor</Tab>
|
|
620
|
-
</TabList>
|
|
621
|
-
|
|
622
|
-
<TabPanel value="images">
|
|
623
|
-
<ImagesTab />
|
|
624
|
-
</TabPanel>
|
|
625
|
-
<TabPanel value="paint">
|
|
626
|
-
<PaintTab />
|
|
627
|
-
</TabPanel>
|
|
628
|
-
<TabPanel value="truecolor">
|
|
629
|
-
<TruecolorTab />
|
|
630
|
-
</TabPanel>
|
|
631
|
-
</Tabs>
|
|
632
|
-
</Box>
|
|
633
|
-
)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// ============================================================================
|
|
637
|
-
// Main
|
|
638
|
-
// ============================================================================
|
|
639
|
-
|
|
640
|
-
export async function main() {
|
|
641
|
-
using term = createTerm()
|
|
642
|
-
const { waitUntilExit } = await render(
|
|
643
|
-
<ExampleBanner meta={meta} controls="h/l tab j/k navigate Esc/q quit">
|
|
644
|
-
<Gallery />
|
|
645
|
-
</ExampleBanner>,
|
|
646
|
-
term,
|
|
647
|
-
)
|
|
648
|
-
await waitUntilExit()
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (import.meta.main) {
|
|
652
|
-
await main()
|
|
653
|
-
}
|