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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
|
// PETSCII image rendering — a fallback when the terminal has no kitty graphics,
// and a per-pane toggle. Ported in spirit from caioluders/petsciinator: split the
// image into character cells and match each cell to the Unicode block/sextant glyph
// + a C64 foreground/background color pair that best reproduces the cell's pixels
// (minimum per-pixel color error). Pure: only std + the raw RGBA bytes (no vaxis),
// so the matcher is unit-testable on its own.
const std = @import("std");
// The match palette is supplied by the caller (mode-dependent): the Commodore 64
// colors below by default, or the terminal's own ANSI palette. Cells store fg/bg as
// palette indices; image.draw paints them as C64 RGB or as indexed colors per mode.
// This is Pepto's canonical C64 palette.
pub const commodore = [16][3]u8{
.{ 0x00, 0x00, 0x00 }, // 0 black
.{ 0xff, 0xff, 0xff }, // 1 white
.{ 0x68, 0x37, 0x2b }, // 2 red
.{ 0x70, 0xa4, 0xb2 }, // 3 cyan
.{ 0x6f, 0x3d, 0x86 }, // 4 purple
.{ 0x58, 0x8d, 0x43 }, // 5 green
.{ 0x35, 0x28, 0x79 }, // 6 blue
.{ 0xb8, 0xc7, 0x6f }, // 7 yellow
.{ 0x6f, 0x4f, 0x25 }, // 8 orange
.{ 0x43, 0x39, 0x00 }, // 9 brown
.{ 0x9a, 0x67, 0x59 }, // 10 light red
.{ 0x44, 0x44, 0x44 }, // 11 dark grey
.{ 0x6c, 0x6c, 0x6c }, // 12 grey
.{ 0x9a, 0xd2, 0x84 }, // 13 light green
.{ 0x6c, 0x5e, 0xb5 }, // 14 light blue
.{ 0x95, 0x95, 0x95 }, // 15 light grey
};
const Palette = [16][3]u8;
// One rendered character cell: the UTF-8 bytes of the chosen glyph (stored inline
// so the slice handed to the retained vaxis screen outlives the frame) + palette
// fg/bg indices.
pub const Cell = struct {
glyph: [4]u8 = .{ ' ', 0, 0, 0 },
glen: u3 = 1,
fg: u4 = 1,
bg: u4 = 0,
};
pub const Grid = struct { cells: []Cell, gw: usize, gh: usize };
fn dist2(a: [3]u8, b: [3]u8) u32 {
const dr = @as(i32, a[0]) - b[0];
const dg = @as(i32, a[1]) - b[1];
const db = @as(i32, a[2]) - b[2];
return @intCast(dr * dr + dg * dg + db * db);
}
fn nearest(px: [3]u8, pal: Palette) u4 {
var best: u4 = 0;
var bestd: u32 = std.math.maxInt(u32);
for (pal, 0..) |c, i| {
const d = dist2(px, c);
if (d < bestd) {
bestd = d;
best = @intCast(i);
}
}
return best;
}
// ---- glyph set: each is a codepoint + an 8x8 ink bitmap (bit y*8+x set = fg) ----
const Glyph = struct { cp: u21, bits: u64 };
// The Unicode codepoint for a 2x3 sextant pattern. Bits: 1=upper-left, 2=upper-right,
// 4=mid-left, 8=mid-right, 16=lower-left, 32=lower-right. The four patterns that
// coincide with existing block characters are mapped to those instead.
fn sextantCp(p: u6) u21 {
return switch (p) {
0 => ' ',
21 => 0x258C, // left half ▌
42 => 0x2590, // right half ▐
63 => 0x2588, // full block █
else => blk: {
var off: u21 = @as(u21, p) - 1;
if (p > 21) off -= 1;
if (p > 42) off -= 1;
break :blk 0x1FB00 + off; // Symbols for Legacy Computing sextants
},
};
}
// the 8x8 ink bitmap for a sextant pattern (2 cols x 3 rows of subcells).
fn sextantBits(p: u6) u64 {
var bits: u64 = 0;
var y: usize = 0;
while (y < 8) : (y += 1) {
const band = (y * 3) / 8; // 0,0,0,1,1,1,2,2
var x: usize = 0;
while (x < 8) : (x += 1) {
const col: usize = if (x < 4) 0 else 1;
const sub: u6 = @intCast(band * 2 + col);
if ((p >> sub) & 1 != 0) bits |= @as(u64, 1) << @intCast(y * 8 + x);
}
}
return bits;
}
// pack 8 row-bytes (bit x set, x=0 leftmost) into the 8x8 bitmap.
fn rows(r: [8]u8) u64 {
var b: u64 = 0;
for (r, 0..) |row, y| b |= @as(u64, row) << @intCast(y * 8);
return b;
}
// the horizontal half blocks and the ten 2x2 quadrants — sextants are 2x3, so they
// can't express an exact 4-row half or a quarter; these fill that gap.
const block_glyphs = [_]Glyph{
.{ .cp = 0x2580, .bits = rows(.{ 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0 }) }, // ▀ top half
.{ .cp = 0x2584, .bits = rows(.{ 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff }) }, // ▄ bottom half
.{ .cp = 0x2598, .bits = rows(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0, 0, 0, 0 }) }, // ▘ TL
.{ .cp = 0x259d, .bits = rows(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0, 0, 0, 0 }) }, // ▝ TR
.{ .cp = 0x2596, .bits = rows(.{ 0, 0, 0, 0, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▖ BL
.{ .cp = 0x2597, .bits = rows(.{ 0, 0, 0, 0, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▗ BR
.{ .cp = 0x259a, .bits = rows(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▚ TL+BR
.{ .cp = 0x259e, .bits = rows(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▞ TR+BL
.{ .cp = 0x259b, .bits = rows(.{ 0xff, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▛ ¬BR
.{ .cp = 0x259c, .bits = rows(.{ 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▜ ¬BL
.{ .cp = 0x2599, .bits = rows(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0xff, 0xff, 0xff, 0xff }) }, // ▙ ¬TR
.{ .cp = 0x259f, .bits = rows(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xff, 0xff, 0xff }) }, // ▟ ¬TL
};
// line/diagonal glyphs add the characteristic PETSCII "drawn" look on edges.
const line_glyphs = [_]Glyph{
.{ .cp = 0x2500, .bits = rows(.{ 0, 0, 0, 0xff, 0xff, 0, 0, 0 }) }, // ─
.{ .cp = 0x2502, .bits = rows(.{ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 }) }, // │
.{ .cp = 0x253c, .bits = rows(.{ 0x18, 0x18, 0x18, 0xff, 0xff, 0x18, 0x18, 0x18 }) }, // ┼
.{ .cp = 0x2572, .bits = rows(.{ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }) }, // ╲
.{ .cp = 0x2571, .bits = rows(.{ 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }) }, // ╱
.{ .cp = 0x2573, .bits = rows(.{ 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81 }) }, // ╳
};
// Real 8x8 font bitmaps for the printable ASCII range (extracted from the cp850-8x8
// console font). This is how petsciinator's charset works: the FULL character set is
// matched, each glyph scored against the cell by its actual bitmap (ink = fg), then
// rendered by its codepoint — the terminal draws the letter in its own font. (bit
// y*8+x set = ink; x=0 leftmost. Generated, not hand-drawn.)
const ascii_glyphs = [_]Glyph{
.{ .cp = 0x21, .bits = 0x00180018183c3c18 }, // !
.{ .cp = 0x22, .bits = 0x0000000000246666 }, // "
.{ .cp = 0x23, .bits = 0x0036367f367f3636 }, // #
.{ .cp = 0x24, .bits = 0x00183e603c067c18 }, // $
.{ .cp = 0x25, .bits = 0x0063660c18336300 }, // %
.{ .cp = 0x26, .bits = 0x006e333b6e1c361c }, // &
.{ .cp = 0x27, .bits = 0x00000000000c1818 }, // '
.{ .cp = 0x28, .bits = 0x0030180c0c0c1830 }, // (
.{ .cp = 0x29, .bits = 0x000c18303030180c }, // )
.{ .cp = 0x2a, .bits = 0x0000663cff3c6600 }, // *
.{ .cp = 0x2b, .bits = 0x000018187e181800 }, // +
.{ .cp = 0x2c, .bits = 0x0c18180000000000 }, // ,
.{ .cp = 0x2d, .bits = 0x000000007e000000 }, // -
.{ .cp = 0x2e, .bits = 0x0018180000000000 }, // .
.{ .cp = 0x2f, .bits = 0x000103060c183060 }, // /
.{ .cp = 0x30, .bits = 0x001c36636b63361c }, // 0
.{ .cp = 0x31, .bits = 0x007e181818181c18 }, // 1
.{ .cp = 0x32, .bits = 0x007f660c3860633e }, // 2
.{ .cp = 0x33, .bits = 0x003e63603c60633e }, // 3
.{ .cp = 0x34, .bits = 0x0078307f33363c38 }, // 4
.{ .cp = 0x35, .bits = 0x003e63603f03037f }, // 5
.{ .cp = 0x36, .bits = 0x003e63633f03061c }, // 6
.{ .cp = 0x37, .bits = 0x000c0c0c1830637f }, // 7
.{ .cp = 0x38, .bits = 0x003e63633e63633e }, // 8
.{ .cp = 0x39, .bits = 0x001e30607e63633e }, // 9
.{ .cp = 0x3a, .bits = 0x0018180000181800 }, // :
.{ .cp = 0x3b, .bits = 0x0c18180000181800 }, // ;
.{ .cp = 0x3c, .bits = 0x006030180c183060 }, // <
.{ .cp = 0x3d, .bits = 0x00007e00007e0000 }, // =
.{ .cp = 0x3e, .bits = 0x00060c1830180c06 }, // >
.{ .cp = 0x3f, .bits = 0x001800181830633e }, // ?
.{ .cp = 0x40, .bits = 0x001e037b7b7b633e }, // @
.{ .cp = 0x41, .bits = 0x006363637f63361c }, // A
.{ .cp = 0x42, .bits = 0x003f66663e66663f }, // B
.{ .cp = 0x43, .bits = 0x003c66030303663c }, // C
.{ .cp = 0x44, .bits = 0x001f36666666361f }, // D
.{ .cp = 0x45, .bits = 0x007f46161e16467f }, // E
.{ .cp = 0x46, .bits = 0x000f06161e16467f }, // F
.{ .cp = 0x47, .bits = 0x005c66730303663c }, // G
.{ .cp = 0x48, .bits = 0x006363637f636363 }, // H
.{ .cp = 0x49, .bits = 0x003c18181818183c }, // I
.{ .cp = 0x4a, .bits = 0x001e333330303078 }, // J
.{ .cp = 0x4b, .bits = 0x006766361e366667 }, // K
.{ .cp = 0x4c, .bits = 0x007f66460606060f }, // L
.{ .cp = 0x4d, .bits = 0x0063636b7f7f7763 }, // M
.{ .cp = 0x4e, .bits = 0x006363737b6f6763 }, // N
.{ .cp = 0x4f, .bits = 0x003e63636363633e }, // O
.{ .cp = 0x50, .bits = 0x000f06063e66663f }, // P
.{ .cp = 0x51, .bits = 0x703e73636363633e }, // Q
.{ .cp = 0x52, .bits = 0x006766363e66663f }, // R
.{ .cp = 0x53, .bits = 0x003c6630180c663c }, // S
.{ .cp = 0x54, .bits = 0x003c1818185a7e7e }, // T
.{ .cp = 0x55, .bits = 0x003e636363636363 }, // U
.{ .cp = 0x56, .bits = 0x001c366363636363 }, // V
.{ .cp = 0x57, .bits = 0x00367f6b6b636363 }, // W
.{ .cp = 0x58, .bits = 0x006363361c366363 }, // X
.{ .cp = 0x59, .bits = 0x003c18183c666666 }, // Y
.{ .cp = 0x5a, .bits = 0x007f664c1831637f }, // Z
.{ .cp = 0x5b, .bits = 0x003c0c0c0c0c0c3c }, // [
.{ .cp = 0x5c, .bits = 0x00406030180c0603 }, // \\
.{ .cp = 0x5d, .bits = 0x003c30303030303c }, // ]
.{ .cp = 0x5e, .bits = 0x0000000063361c08 }, // ^
.{ .cp = 0x5f, .bits = 0xff00000000000000 }, // _
.{ .cp = 0x60, .bits = 0x000000000030180c }, // `
.{ .cp = 0x61, .bits = 0x006e333e301e0000 }, // a
.{ .cp = 0x62, .bits = 0x003b6666663e0607 }, // b
.{ .cp = 0x63, .bits = 0x003e6303633e0000 }, // c
.{ .cp = 0x64, .bits = 0x006e3333333e3038 }, // d
.{ .cp = 0x65, .bits = 0x003e037f633e0000 }, // e
.{ .cp = 0x66, .bits = 0x000f06061f06663c }, // f
.{ .cp = 0x67, .bits = 0x1f303e33336e0000 }, // g
.{ .cp = 0x68, .bits = 0x006766666e360607 }, // h
.{ .cp = 0x69, .bits = 0x003c1818181c0018 }, // i
.{ .cp = 0x6a, .bits = 0x3c66666060600060 }, // j
.{ .cp = 0x6b, .bits = 0x0067361e36660607 }, // k
.{ .cp = 0x6c, .bits = 0x003c18181818181c }, // l
.{ .cp = 0x6d, .bits = 0x006b6b6b7f370000 }, // m
.{ .cp = 0x6e, .bits = 0x00666666663b0000 }, // n
.{ .cp = 0x6f, .bits = 0x003e6363633e0000 }, // o
.{ .cp = 0x70, .bits = 0x0f063e66663b0000 }, // p
.{ .cp = 0x71, .bits = 0x78303e33336e0000 }, // q
.{ .cp = 0x72, .bits = 0x000f06066e3b0000 }, // r
.{ .cp = 0x73, .bits = 0x003f603e037e0000 }, // s
.{ .cp = 0x74, .bits = 0x00386c0c0c3f0c0c }, // t
.{ .cp = 0x75, .bits = 0x006e333333330000 }, // u
.{ .cp = 0x76, .bits = 0x001c366363630000 }, // v
.{ .cp = 0x77, .bits = 0x00367f6b6b630000 }, // w
.{ .cp = 0x78, .bits = 0x0063361c36630000 }, // x
.{ .cp = 0x79, .bits = 0x3f607e6363630000 }, // y
.{ .cp = 0x7a, .bits = 0x007e4c18327e0000 }, // z
.{ .cp = 0x7b, .bits = 0x007018180e181870 }, // {
.{ .cp = 0x7c, .bits = 0x0018181818181818 }, // |
.{ .cp = 0x7d, .bits = 0x000e18187018180e }, // }
.{ .cp = 0x7e, .bits = 0x0000000000003b6e }, // ~
};
// the block glyph table (sextants + half/quadrant blocks + line glyphs), built at
// comptime, and the same set extended with the ASCII glyphs. `render(ascii=…)` picks.
const glyphs = blk: {
@setEvalBranchQuota(100000);
var list: [64 + block_glyphs.len + line_glyphs.len]Glyph = undefined;
var p: usize = 0;
while (p < 64) : (p += 1) list[p] = .{ .cp = sextantCp(@intCast(p)), .bits = sextantBits(@intCast(p)) };
for (block_glyphs, 0..) |bg, i| list[64 + i] = bg;
for (line_glyphs, 0..) |lg, i| list[64 + block_glyphs.len + i] = lg;
break :blk list;
};
const glyphs_ascii = glyphs ++ ascii_glyphs;
// match one 8x8 RGB cell to the best (glyph, fg, bg). Candidate colors are the
// most-common palette colors among the 64 pixels; for each ordered pair the glyph
// cost is base(all-bg) + sum over the glyph's ink bits of (dist_fg - dist_bg).
fn matchCell(cell: *const [64][3]u8, pal: Palette, gset: []const Glyph) Cell {
var counts = [_]u16{0} ** 16;
for (cell) |px| counts[nearest(px, pal)] += 1;
var cand: [4]u4 = undefined;
var ncand: usize = 0;
var used = [_]bool{false} ** 16;
while (ncand < 4) : (ncand += 1) {
var best: ?usize = null;
for (counts, 0..) |c, i| {
if (used[i] or c == 0) continue;
if (best == null or c > counts[best.?]) best = i;
}
if (best) |bi| {
cand[ncand] = @intCast(bi);
used[bi] = true;
} else break;
}
if (ncand == 0) return .{}; // can't happen (64 pixels), but keep it total
if (ncand == 1) return encode(' ', cand[0], cand[0]); // solid color
var best_cost: i64 = std.math.maxInt(i64);
var best = encode(' ', cand[0], cand[0]);
var fi: usize = 0;
while (fi < ncand) : (fi += 1) {
var bi: usize = 0;
while (bi < ncand) : (bi += 1) {
if (fi == bi) continue;
const fg = cand[fi];
const bg = cand[bi];
var dfg: [64]u32 = undefined;
var dbg: [64]u32 = undefined;
var base: i64 = 0;
for (cell, 0..) |px, p| {
dfg[p] = dist2(px, pal[fg]);
dbg[p] = dist2(px, pal[bg]);
base += dbg[p];
}
for (gset) |g| {
var delta: i64 = 0;
var bits = g.bits;
while (bits != 0) : (bits &= bits - 1) {
const p: usize = @ctz(bits);
delta += @as(i64, dfg[p]) - @as(i64, dbg[p]);
}
const cost = base + delta;
if (cost < best_cost) {
best_cost = cost;
best = encode(g.cp, fg, bg);
}
}
}
}
return best;
}
fn encode(cp: u21, fg: u4, bg: u4) Cell {
var c = Cell{ .fg = fg, .bg = bg };
const n = std.unicode.utf8Encode(cp, &c.glyph) catch 1;
c.glen = @intCast(n);
return c;
}
// Render `rgba` (iw x ih, 4 bytes/px) into a grid of at most cols x rows cells,
// preserving the image aspect with the terminal cell aspect (~1:2) corrected.
// Caller owns Grid.cells (gpa).
pub fn render(gpa: std.mem.Allocator, rgba: []const u8, iw: usize, ih: usize, cols: usize, rows_: usize, pal: Palette, ascii: bool) !Grid {
if (iw == 0 or ih == 0 or cols == 0 or rows_ == 0) return .{ .cells = try gpa.alloc(Cell, 0), .gw = 0, .gh = 0 };
const gset: []const Glyph = if (ascii) &glyphs_ascii else &glyphs;
// contain-fit; cells are ~twice as tall as wide, so a row spans 2 width-units.
var gw = cols;
var gh = (cols * ih) / (2 * iw);
if (gh > rows_) {
gh = rows_;
gw = (rows_ * 2 * iw) / ih;
}
gw = std.math.clamp(gw, 1, cols);
gh = std.math.clamp(gh, 1, rows_);
const cells = try gpa.alloc(Cell, gw * gh);
var cy: usize = 0;
while (cy < gh) : (cy += 1) {
const ry0 = cy * ih / gh;
const ry1 = @max(ry0 + 1, (cy + 1) * ih / gh);
var cx: usize = 0;
while (cx < gw) : (cx += 1) {
const rx0 = cx * iw / gw;
const rx1 = @max(rx0 + 1, (cx + 1) * iw / gw);
var cell: [64][3]u8 = undefined;
var sy: usize = 0;
while (sy < 8) : (sy += 1) {
const py0 = ry0 + sy * (ry1 - ry0) / 8;
const py1 = @max(py0 + 1, ry0 + (sy + 1) * (ry1 - ry0) / 8);
var sx: usize = 0;
while (sx < 8) : (sx += 1) {
const px0 = rx0 + sx * (rx1 - rx0) / 8;
const px1 = @max(px0 + 1, rx0 + (sx + 1) * (rx1 - rx0) / 8);
var rs: usize = 0;
var gs: usize = 0;
var bs: usize = 0;
var n: usize = 0;
var yy = py0;
while (yy < py1 and yy < ih) : (yy += 1) {
var xx = px0;
while (xx < px1 and xx < iw) : (xx += 1) {
const i = (yy * iw + xx) * 4;
rs += rgba[i];
gs += rgba[i + 1];
bs += rgba[i + 2];
n += 1;
}
}
if (n == 0) n = 1;
cell[sy * 8 + sx] = .{ @intCast(rs / n), @intCast(gs / n), @intCast(bs / n) };
}
}
cells[cy * gw + cx] = matchCell(&cell, pal, gset);
}
}
return .{ .cells = cells, .gw = gw, .gh = gh };
}
test "sextant codepoints: blocks + endpoints" {
try std.testing.expectEqual(@as(u21, ' '), sextantCp(0));
try std.testing.expectEqual(@as(u21, 0x2588), sextantCp(63));
try std.testing.expectEqual(@as(u21, 0x258C), sextantCp(21));
try std.testing.expectEqual(@as(u21, 0x2590), sextantCp(42));
try std.testing.expectEqual(@as(u21, 0x1FB00), sextantCp(1)); // first sextant
try std.testing.expectEqual(@as(u21, 0x1FB3B), sextantCp(62)); // last sextant
}
test "matchCell: solid color -> space on that bg" {
var cell: [64][3]u8 = undefined;
for (&cell) |*p| p.* = commodore[5]; // all green
const m = matchCell(&cell, commodore, &glyphs);
try std.testing.expectEqual(@as(u4, 5), m.bg);
try std.testing.expectEqual(@as(u8, ' '), m.glyph[0]);
}
test "matchCell: clean top/bottom split picks the two colors" {
var cell: [64][3]u8 = undefined;
for (0..64) |p| cell[p] = if (p < 32) commodore[1] else commodore[6]; // white over blue
const m = matchCell(&cell, commodore, &glyphs);
// both palette colors must be chosen (in some fg/bg order)
const a = @as(u4, @min(m.fg, m.bg));
const b = @as(u4, @max(m.fg, m.bg));
try std.testing.expectEqual(@as(u4, 1), a);
try std.testing.expectEqual(@as(u4, 6), b);
// a clean top/bottom split must resolve to a real block glyph (the top-4-rows
// half block ▀), never a blank cell.
const cp = std.unicode.utf8Decode(m.glyph[0..m.glen]) catch 0;
try std.testing.expectEqual(@as(u21, 0x2580), cp);
}
test "ascii option only enables the ascii glyphs" {
// the ascii glyph codepoints must be reachable exactly when ascii is on.
var seen_block = false;
var seen_ascii = false;
for (glyphs) |g| if (g.cp == '#') {
seen_block = true;
};
for (glyphs_ascii) |g| if (g.cp == '#') {
seen_ascii = true;
};
try std.testing.expect(!seen_block); // '#' is an ascii-only glyph
try std.testing.expect(seen_ascii);
try std.testing.expectEqual(glyphs.len + ascii_glyphs.len, glyphs_ascii.len);
}
test "ascii glyph is chosen when a cell has its exact shape" {
// a cell shaped exactly like the font 'S' (ink=white on black) must match 'S'
// with ascii on (cost 0), and fall back to some block glyph with ascii off.
const s_bits: u64 = 0x003c6630180c663c;
var cell: [64][3]u8 = undefined;
for (0..64) |p| cell[p] = if ((s_bits >> @intCast(p)) & 1 != 0) commodore[1] else commodore[0];
const on = matchCell(&cell, commodore, &glyphs_ascii);
try std.testing.expectEqual(@as(u21, 'S'), std.unicode.utf8Decode(on.glyph[0..on.glen]) catch 0);
const off = matchCell(&cell, commodore, &glyphs);
try std.testing.expect((std.unicode.utf8Decode(off.glyph[0..off.glen]) catch 0) != 'S');
}
test "render: tiny image produces a grid within bounds" {
const a = std.testing.allocator;
// 2x2 checker, RGBA
var img = [_]u8{0} ** (2 * 2 * 4);
img[0] = 255; img[1] = 255; img[2] = 255; img[3] = 255; // (0,0) white
img[(3) * 4 + 0] = 255; img[(3) * 4 + 1] = 255; img[(3) * 4 + 2] = 255; img[(3) * 4 + 3] = 255; // (1,1) white
const g = try render(a, &img, 2, 2, 10, 10, commodore, true);
defer a.free(g.cells);
try std.testing.expect(g.gw >= 1 and g.gw <= 10);
try std.testing.expect(g.gh >= 1 and g.gh <= 10);
try std.testing.expectEqual(g.gw * g.gh, g.cells.len);
}
|