233 lines
9.5 KiB
Nim
233 lines
9.5 KiB
Nim
|
|
## termimg demo — full visual showcase of every rendering path.
|
||
|
|
##
|
||
|
|
## Usage:
|
||
|
|
## nim c -r --path:src examples/demo.nim
|
||
|
|
##
|
||
|
|
## Shows: scanner pattern, rainbow gradient, target circles,
|
||
|
|
## gallery layout (3 across), column layout (3 densities stacked),
|
||
|
|
## fit-mode comparison grid, and individual renders for each protocol.
|
||
|
|
|
||
|
|
import std/[strutils, math]
|
||
|
|
import termimg
|
||
|
|
|
||
|
|
# ── Pattern generators ──────────────────────────────────────────
|
||
|
|
proc checkerboard(w, h, csize: int): seq[uint8] =
|
||
|
|
result = newSeq[uint8](w * h * 4)
|
||
|
|
for y in 0 ..< h:
|
||
|
|
for x in 0 ..< w:
|
||
|
|
let off = (y * w + x) * 4
|
||
|
|
let bright = if ((x div csize) + (y div csize)) mod 2 == 0: 220 else: 40
|
||
|
|
result[off + 0] = uint8(bright)
|
||
|
|
result[off + 1] = uint8(bright)
|
||
|
|
result[off + 2] = uint8(bright)
|
||
|
|
result[off + 3] = 255
|
||
|
|
|
||
|
|
proc rainbowBars(w, h: int): seq[uint8] =
|
||
|
|
result = newSeq[uint8](w * h * 4)
|
||
|
|
for y in 0 ..< h:
|
||
|
|
for x in 0 ..< w:
|
||
|
|
let off = (y * w + x) * 4
|
||
|
|
let band = float(x) / float(w) * 6.0
|
||
|
|
let iband = int(band)
|
||
|
|
let frac = band - float(iband)
|
||
|
|
var r, g, b: float
|
||
|
|
case iband
|
||
|
|
of 0: (r, g, b) = (1.0, frac, 0.0)
|
||
|
|
of 1: (r, g, b) = (1.0 - frac, 1.0, 0.0)
|
||
|
|
of 2: (r, g, b) = (0.0, 1.0, frac)
|
||
|
|
of 3: (r, g, b) = (0.0, 1.0 - frac, 1.0)
|
||
|
|
of 4: (r, g, b) = (frac, 0.0, 1.0)
|
||
|
|
else: (r, g, b) = (1.0, 0.0, 1.0 - frac)
|
||
|
|
let ymul = 0.6 + 0.4 * float(y) / float(h)
|
||
|
|
result[off + 0] = uint8(clamp(int(r * 255.0 * ymul), 0, 255))
|
||
|
|
result[off + 1] = uint8(clamp(int(g * 255.0 * ymul), 0, 255))
|
||
|
|
result[off + 2] = uint8(clamp(int(b * 255.0 * ymul), 0, 255))
|
||
|
|
result[off + 3] = 255
|
||
|
|
|
||
|
|
proc targetCircle(w, h: int): seq[uint8] =
|
||
|
|
result = newSeq[uint8](w * h * 4)
|
||
|
|
let cx = float(w) / 2.0
|
||
|
|
let cy = float(h) / 2.0
|
||
|
|
let maxR = min(cx, cy)
|
||
|
|
for y in 0 ..< h:
|
||
|
|
for x in 0 ..< w:
|
||
|
|
let off = (y * w + x) * 4
|
||
|
|
let dx = float(x) - cx
|
||
|
|
let dy = float(y) - cy
|
||
|
|
let r = sqrt(dx * dx + dy * dy)
|
||
|
|
let ring = int(r / (maxR / 5.0)) mod 2
|
||
|
|
if ring == 0:
|
||
|
|
result[off + 0] = 200; result[off + 1] = 50; result[off + 2] = 50
|
||
|
|
else:
|
||
|
|
result[off + 0] = 255; result[off + 1] = 255; result[off + 2] = 255
|
||
|
|
result[off + 3] = 255
|
||
|
|
|
||
|
|
proc warmGlow(w, h: int): seq[uint8] =
|
||
|
|
result = newSeq[uint8](w * h * 4)
|
||
|
|
let cx = float(w) / 2.0
|
||
|
|
let cy = float(h) / 2.0
|
||
|
|
# maxR intentionally omitted: warmGlow normalizes by cx,cy directly
|
||
|
|
for y in 0 ..< h:
|
||
|
|
for x in 0 ..< w:
|
||
|
|
let off = (y * w + x) * 4
|
||
|
|
let dx = (float(x) - cx) / cx
|
||
|
|
let dy = (float(y) - cy) / cy
|
||
|
|
let dist = sqrt(dx * dx + dy * dy)
|
||
|
|
let glow = clamp(int(255.0 * exp(-dist * 2.5)), 0, 255)
|
||
|
|
result[off + 0] = uint8(glow)
|
||
|
|
result[off + 1] = uint8(glow * 3 div 5)
|
||
|
|
result[off + 2] = uint8(glow div 3)
|
||
|
|
result[off + 3] = 255
|
||
|
|
|
||
|
|
# ── Side-by-side render helper ─────────────────────────────────
|
||
|
|
proc renderSideBySide(images: openArray[seq[uint8]],
|
||
|
|
widths: openArray[int],
|
||
|
|
heights: openArray[int],
|
||
|
|
caps: TerminalCapabilities,
|
||
|
|
opts: RenderOptions,
|
||
|
|
gap: int = 2): string =
|
||
|
|
## Renders N images at the same geometry and stitches them row-by-row.
|
||
|
|
let geo = computeGeometry(widths[0], heights[0], caps, opts)
|
||
|
|
var parts = newSeq[(seq[string], int)]() # (lines, frames_consumed)
|
||
|
|
for i in 0 ..< images.len:
|
||
|
|
let outStr = renderHalfBlock(
|
||
|
|
ImageData(width: widths[i], height: heights[i], data: images[i]),
|
||
|
|
opts.backgroundRgb, geo.columns, geo.sampleHeight)
|
||
|
|
parts.add (outStr.split('\n'), 0)
|
||
|
|
let maxRows = min(min(parts[0][0].len, parts[1][0].len),
|
||
|
|
(if parts.len > 2: parts[2][0].len else: parts[1][0].len))
|
||
|
|
for row in 0 ..< maxRows:
|
||
|
|
for i in 0 ..< parts.len:
|
||
|
|
if row < parts[i][0].len:
|
||
|
|
result.add parts[i][0][row]
|
||
|
|
if i < parts.len - 1:
|
||
|
|
result.add spaces(gap)
|
||
|
|
result.add "\n"
|
||
|
|
|
||
|
|
when isMainModule:
|
||
|
|
echo ""
|
||
|
|
echo "╭──────────────────────────────────────────────╮"
|
||
|
|
echo "│ termimg — Full Demo │"
|
||
|
|
echo "╰──────────────────────────────────────────────╯"
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
let caps = detectCapabilities()
|
||
|
|
|
||
|
|
echo "Terminal: ", caps.columns, "x", caps.rows, " cells"
|
||
|
|
echo "Cell: ", caps.cell.width, "x", caps.cell.height, " px"
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
echo "Detected protocols:"
|
||
|
|
for proto in [ptKitty, ptIterm2, ptSixel, ptQuarterBlock, ptHalfBlock]:
|
||
|
|
let mark = if caps.supports(proto): " ✓" else: " ✗"
|
||
|
|
echo " ", ($proto).align(14), mark
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── Pattern generation ──────────────────────────────────────────
|
||
|
|
const patternW = 64
|
||
|
|
const patternH = 48
|
||
|
|
let chk = checkerboard(patternW, patternH, 8)
|
||
|
|
let rnb = rainbowBars(patternW, patternH)
|
||
|
|
let tgt = targetCircle(patternW, patternH)
|
||
|
|
let glow = warmGlow(patternW, patternH)
|
||
|
|
|
||
|
|
# ── 1. Gallery: 3 patterns side-by-side ─────────────────────────
|
||
|
|
echo "── Gallery: 3 patterns side-by-side ──"
|
||
|
|
var optsGal = defaultOptions()
|
||
|
|
optsGal.maxWidth = (caps.columns - 4) div 3
|
||
|
|
optsGal.fit = fmWidth
|
||
|
|
stdout.write(renderSideBySide([chk, rnb, tgt], [patternW, patternW, patternW],
|
||
|
|
[patternH, patternH, patternH], caps, optsGal))
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── 2. All four patterns in a column (thumbnail pipeline) ──────
|
||
|
|
echo "── All four patterns (thumbnail pipeline) ──"
|
||
|
|
let thumbOpts = thumbnailOptions()
|
||
|
|
for (name, pix) in [("Checkerboard", chk), ("Rainbow bars", rnb),
|
||
|
|
("Target circles", tgt), ("Warm glow", glow)]:
|
||
|
|
echo " ", name, ":"
|
||
|
|
let outStr = renderImageRgba(pix, patternW, patternH, caps, thumbOpts)
|
||
|
|
stdout.write(outStr)
|
||
|
|
stdout.write("\n")
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── 3. Column layout: rainbow at 3 densities for comparison ────
|
||
|
|
echo "── Column layout: rainbow at 3 block densities ──"
|
||
|
|
let colW = caps.columns - 2
|
||
|
|
for density in [bdFull, bdHalf, bdQuarter]:
|
||
|
|
var opts = defaultOptions()
|
||
|
|
opts.density = density
|
||
|
|
opts.maxWidth = colW
|
||
|
|
opts.fit = fmWidth
|
||
|
|
let geo = computeGeometry(patternW, patternH, caps, opts)
|
||
|
|
let outStr = renderHalfBlock(
|
||
|
|
ImageData(width: patternW, height: patternH, data: rnb),
|
||
|
|
opts.backgroundRgb, geo.columns, geo.sampleHeight)
|
||
|
|
stdout.write(outStr)
|
||
|
|
stdout.write("\n")
|
||
|
|
echo " ($1 — $2 cols x $3 samples)" % [$density, $geo.columns, $geo.sampleHeight]
|
||
|
|
echo ""
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── 4. Fit-mode comparison grid (quarter-block, dithered) ──────
|
||
|
|
echo "── Fit-mode grid: target circles at 4 fit modes ──"
|
||
|
|
let halfW = caps.columns div 2 - 1
|
||
|
|
for fit in [fmContain, fmWidth, fmOriginal, fmStretch]:
|
||
|
|
var opts = defaultOptions()
|
||
|
|
opts.fit = fit
|
||
|
|
opts.maxWidth = halfW
|
||
|
|
opts.density = bdQuarter
|
||
|
|
let geo = computeGeometry(patternW, patternH, caps, opts)
|
||
|
|
let outStr = renderQuarterBlock(
|
||
|
|
ImageData(width: patternW, height: patternH, data: tgt),
|
||
|
|
opts.backgroundRgb, geo.columns, geo.sampleHeight, dither = true)
|
||
|
|
stdout.write(outStr)
|
||
|
|
stdout.write("\n")
|
||
|
|
echo " ($1 — $2 px, $3 cols x $4 samples)" % [
|
||
|
|
$fit, $geo.pixelWidth & "x" & $geo.pixelHeight,
|
||
|
|
$geo.columns, $geo.sampleHeight]
|
||
|
|
echo ""
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── 5. Individual renders per protocol ──────────────────────────
|
||
|
|
echo "── Default render (half-block, contain fit) ──"
|
||
|
|
let out1 = renderImageRgba(chk, patternW, patternH, caps, defaultOptions())
|
||
|
|
stdout.write(out1)
|
||
|
|
stdout.write("\n")
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
echo "── Quarter-block thumbnail (40 cols, dithered) ──"
|
||
|
|
let out2 = renderImageRgba(rnb, patternW, patternH, caps, thumbnailOptions())
|
||
|
|
stdout.write(out2)
|
||
|
|
stdout.write("\n")
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ── 6. Extreme aspect-ratio demo ────────────────────────────────
|
||
|
|
echo "── Wide vs tall: checkerboard at extreme ratios ──"
|
||
|
|
var optsW = defaultOptions()
|
||
|
|
optsW.maxWidth = caps.columns - 2
|
||
|
|
optsW.fit = fmWidth
|
||
|
|
let geoW = computeGeometry(patternW, patternH, caps, optsW)
|
||
|
|
let outW = renderHalfBlock(
|
||
|
|
ImageData(width: patternW, height: patternH, data: chk),
|
||
|
|
optsW.backgroundRgb, geoW.columns, geoW.sampleHeight)
|
||
|
|
stdout.write(outW)
|
||
|
|
echo " (wide: $1 cols x $2 samples)" % [$geoW.columns, $geoW.sampleHeight]
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
let tallPixels = checkerboard(patternH, patternW, 6) # 48x64
|
||
|
|
var optsT = defaultOptions()
|
||
|
|
optsT.maxHeight = caps.rows - 4
|
||
|
|
optsT.fit = fmContain
|
||
|
|
let geoT = computeGeometry(patternH, patternW, caps, optsT)
|
||
|
|
let outT = renderHalfBlock(
|
||
|
|
ImageData(width: patternH, height: patternW, data: tallPixels),
|
||
|
|
optsT.backgroundRgb, geoT.columns, geoT.sampleHeight)
|
||
|
|
stdout.write(outT)
|
||
|
|
echo " (tall: $1 cols x $2 samples)" % [$geoT.columns, $geoT.sampleHeight]
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
echo "╭──────────────────────────────────────────────╮"
|
||
|
|
echo "│ Demo complete. All renders done. │"
|
||
|
|
echo "╰──────────────────────────────────────────────╯"
|