Files
terminal-oscilloscope/src/osc/phosphor.nim
rolandnsharp 94d5b2cf63 Rename crt → osc throughout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:43:15 +10:00

101 lines
3.6 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Phosphor buffer with CRT physics: persistence decay, beam bloom,
## and half-block rendering with intensity-based shading.
import illwill, math
const
PhosphorDecay* = 0.60 # per-frame persistence (P31 green phosphor)
BeamIntensity* = 0.9 # brightness at beam impact
BloomInner* = 0.25 # glow spread to adjacent pixels
BloomOuter* = 0.08 # faint halo from electron scatter
MinBright* = 0.02 # below this, phosphor is considered off
type
Intensity* = enum
iHot ## beam core — white-hot
iBright ## fluoro green phosphor
iMedium ## green glow
iDim ## faint persistence trail
PhosphorBuffer* = object
w*, h*: int # terminal columns/rows
pixH*: int # pixel height (2× rows via half-blocks)
data*: seq[float] # brightness per pixel [w × pixH]
proc initPhosphor*(w, h: int): PhosphorBuffer =
let pixH = h * 2
PhosphorBuffer(w: w, h: h, pixH: pixH, data: newSeq[float](w * pixH))
proc idx(pb: PhosphorBuffer, x, y: int): int {.inline.} =
y * pb.w + x
proc add(pb: var PhosphorBuffer, x, y: int, intensity: float) {.inline.} =
if x >= 0 and x < pb.w and y >= 0 and y < pb.pixH:
pb.data[pb.idx(x, y)] = min(pb.data[pb.idx(x, y)] + intensity, 1.0)
proc decay*(pb: var PhosphorBuffer) =
for i in 0..<pb.data.len:
pb.data[i] *= PhosphorDecay
if pb.data[i] < MinBright:
pb.data[i] = 0.0
proc plotDot*(pb: var PhosphorBuffer, fx, fy: float) =
## Deposit a phosphor dot with physics-based bloom.
let x = int(fx)
let y = int(fy)
if x < 0 or x >= pb.w or y < 0 or y >= pb.pixH: return
# Beam impact
pb.add(x, y, BeamIntensity)
# Inner bloom — phosphor scatter
pb.add(x, y - 1, BloomInner)
pb.add(x, y + 1, BloomInner)
pb.add(x - 1, y, BloomInner * 0.5)
pb.add(x + 1, y, BloomInner * 0.5)
# Outer bloom — electron scatter
pb.add(x, y - 2, BloomOuter)
pb.add(x, y + 2, BloomOuter)
pb.add(x - 1, y - 1, BloomOuter)
pb.add(x + 1, y - 1, BloomOuter)
pb.add(x - 1, y + 1, BloomOuter)
pb.add(x + 1, y + 1, BloomOuter)
proc plotLine*(pb: var PhosphorBuffer, x0, y0, x1, y1: float) =
## Interpolated line of phosphor dots between two points.
let steps = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1)
for i in 0..steps:
let t = i.float / steps.float
pb.plotDot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t)
# ── Half-block rendering ────────────────────────────────────────────
proc toIntensity*(b: float): Intensity =
if b > 0.7: iHot elif b > 0.4: iBright elif b > 0.15: iMedium else: iDim
proc writePhosphor*(tb: var TerminalBuffer, x, y: int, ch: string,
intensity: Intensity) =
case intensity
of iHot: tb.write(x, y, fgWhite, styleBright, ch)
of iBright: tb.write(x, y, fgGreen, styleBright, ch)
of iMedium: tb.write(x, y, fgGreen, ch)
of iDim: tb.write(x, y, fgGreen, styleDim, ch)
proc render*(pb: PhosphorBuffer, tb: var TerminalBuffer) =
## Blit the phosphor buffer to the terminal using half-block characters.
for ty in 0..<pb.h:
let topRow = ty * 2
let botRow = ty * 2 + 1
for x in 0..<pb.w:
let topB = if topRow < pb.pixH: pb.data[pb.idx(x, topRow)] else: 0.0
let botB = if botRow < pb.pixH: pb.data[pb.idx(x, botRow)] else: 0.0
if topB > MinBright or botB > MinBright:
let tOn = topB > MinBright
let bOn = botB > MinBright
if tOn and bOn:
tb.writePhosphor(x, ty, "", toIntensity(max(topB, botB)))
elif tOn:
tb.writePhosphor(x, ty, "", toIntensity(topB))
else:
tb.writePhosphor(x, ty, "", toIntensity(botB))