1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
## Sprite → truecolor ANSI for terminals (half-block packing).
## Each terminal cell covers 2 vertical pixels: bg = upper, fg = lower.
## Sequences are kept compact (single CSI, reset once per line) so tools like
## termshot/bunt and narrow PTYs do not choke mid-escape.
import std/[strutils, strformat]
import ./avatar
type Rgba* = object
width*, height*: int
data*: seq[int] ## w*h*4 bytes
func hexToRgb*(hex: string): (int, int, int) =
(parseHexInt(hex[1 .. 2]), parseHexInt(hex[3 .. 4]), parseHexInt(hex[5 .. 6]))
func spriteToRgba*(s: Sprite): Rgba =
## Expand a palette-indexed sprite to RGBA (alpha 0 for transparent).
result = Rgba(width: s.width, height: s.height,
data: newSeq[int](s.width * s.height * 4))
for i, idx in s.pixels:
let color = s.palette[idx]
if color != "transparent":
let (r, g, b) = hexToRgb(color)
let o = i * 4
result.data[o] = r
result.data[o + 1] = g
result.data[o + 2] = b
result.data[o + 3] = 255
func scaleRgba*(src: Rgba, scale: int): Rgba =
## Nearest-neighbor upscale.
if scale <= 1:
return src
let
w = src.width * scale
h = src.height * scale
result = Rgba(width: w, height: h, data: newSeq[int](w * h * 4))
for y in 0 ..< h:
let sy = y div scale
for x in 0 ..< w:
let
sx = x div scale
si = (sy * src.width + sx) * 4
di = (y * w + x) * 4
for k in 0 ..< 4:
result.data[di + k] = src.data[si + k]
const
ESC = "\x1b"
RESET = ESC & "[0m"
LOWER = "▄"
UPPER = "▀"
func fg(r, g, b: int): string = &"{ESC}[38;2;{r};{g};{b}m"
func bgFg(ur, ug, ub, lr, lg, lb: int): string =
&"{ESC}[48;2;{ur};{ug};{ub};38;2;{lr};{lg};{lb}m"
type Mode = enum mNone, mSpace, mLower, mUpper, mBoth
func rgbaToAnsi*(img: Rgba): string =
## Convert an RGBA raster to a multi-line truecolor ANSI string.
let
w = img.width
h = img.height
func px(y, x, k: int): int = img.data[(y * w + x) * 4 + k]
var lines: seq[string]
var y = 0
while y < h:
let y2 = y + 1
var
line = ""
mode = mNone
fr, fgc, fb, br, bg, bb = -1
for x in 0 ..< w:
let
ur = px(y, x, 0)
ug = px(y, x, 1)
ub = px(y, x, 2)
ua = px(y, x, 3)
lr = if y2 < h: px(y2, x, 0) else: 0
lg = if y2 < h: px(y2, x, 1) else: 0
lb = if y2 < h: px(y2, x, 2) else: 0
la = if y2 < h: px(y2, x, 3) else: 0
if ua == 0 and la == 0:
if mode notin {mNone, mSpace}: line.add RESET
line.add " "
mode = mSpace
fr = -1; fgc = -1; fb = -1; br = -1; bg = -1; bb = -1
elif ua == 0:
if mode != mLower or fr != lr or fgc != lg or fb != lb:
line.add fg(lr, lg, lb) & LOWER
mode = mLower
fr = lr; fgc = lg; fb = lb; br = -1; bg = -1; bb = -1
else:
line.add LOWER
elif la == 0:
if mode != mUpper or fr != ur or fgc != ug or fb != ub:
line.add fg(ur, ug, ub) & UPPER
mode = mUpper
fr = ur; fgc = ug; fb = ub; br = -1; bg = -1; bb = -1
else:
line.add UPPER
else:
# both: single CSI with bg = upper, fg = lower
if mode != mBoth or fr != lr or fgc != lg or fb != lb or
br != ur or bg != ug or bb != ub:
line.add bgFg(ur, ug, ub, lr, lg, lb) & LOWER
mode = mBoth
fr = lr; fgc = lg; fb = lb; br = ur; bg = ug; bb = ub
else:
line.add LOWER
if mode notin {mNone, mSpace}: line.add RESET
lines.add line
y += 2
lines.join("\n")
func avatarToAnsi*(av: Avatar, scale = 2, facing = fSouth, frame = 0): string =
rgbaToAnsi(scaleRgba(spriteToRgba(renderSpritePixels(av, facing, frame)), scale))
func didToAnsi*(did: string, scale = 2, facing = fSouth, frame = 0): string =
## DID → truecolor ANSI art (no network).
avatarToAnsi(deriveAvatar(did), scale, facing, frame)
|