mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-10 06:54:04 +09:00
playtaud: waterfall-of-text dynamic bg
This commit is contained in:
@@ -87,8 +87,8 @@ const COL_LABEL = 220 // amber panel label
|
|||||||
const COL_DIM = 235 // muted text
|
const COL_DIM = 235 // muted text
|
||||||
const COL_TITLE = 230 // bright white-yellow song title
|
const COL_TITLE = 230 // bright white-yellow song title
|
||||||
const COL_VALUE = 254 // bright white numeric values
|
const COL_VALUE = 254 // bright white numeric values
|
||||||
const COL_TICK_LIVE = 46 // green tick light
|
const COL_TICK_LIVE = 76 // green tick light
|
||||||
const COL_TICK_DEAD = 22 // dim green
|
const COL_TICK_DEAD = 20 // dim green
|
||||||
const COL_ORDER_PAST = 235
|
const COL_ORDER_PAST = 235
|
||||||
const COL_ORDER_CUR = 226 // bright yellow active cue
|
const COL_ORDER_CUR = 226 // bright yellow active cue
|
||||||
const COL_ORDER_FUT = 250
|
const COL_ORDER_FUT = 250
|
||||||
@@ -731,20 +731,166 @@ function spawnEventsForRow(cueIdx, rowIdx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-lane rendering ──────────────────────────────────────────────────────
|
// ── Dynamic matrix background ────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// The renderer is structured as: each frame, blank the visualiser rows of all
|
// Behind the event lanes runs a "terminal matrix" of the raw tracker data,
|
||||||
// five lanes, walk the active events, and draw each into its lane. Painting
|
// re-spelled as pseudo-opcodes and streamed one row's worth at a time in
|
||||||
// is reasonably cheap because (a) the bordered side columns stay untouched,
|
// lock-step with the playhead's row cadence. Each tracker cell on the current
|
||||||
// and (b) blanking inside is two cells per row × ~14 rows = 30 cells.
|
// row contributes up to four 7-char tokens (only for the sub-fields it carries):
|
||||||
|
//
|
||||||
|
// NT:nnnn note (4-hex noteVal)
|
||||||
|
// VO:i.jj volume column (i = selector 0..3, jj = 2-digit value 00..63)
|
||||||
|
// PN:k.ll pan column (k = selector 0..3, ll = 2-digit value 00..63)
|
||||||
|
// Fs:eeee effect (s = base-36 opcode symbol, eeee = 4-hex argument)
|
||||||
|
//
|
||||||
|
// Tokens flow left-to-right and wrap at the canvas edge; when the print head
|
||||||
|
// runs off the bottom it rolls back to the top, and a cue change resets it to
|
||||||
|
// the top too. Column wrapping only ever breaks between a token's three 2-char
|
||||||
|
// atoms AA / bb / cc — never mid-atom — and a colon that would land at a line
|
||||||
|
// edge is dropped, so a line never starts or ends with ':' (it may start with a
|
||||||
|
// single separator space). Each freshly printed cell is brightest and decays
|
||||||
|
// one palette step per row, trailing a comet tail behind the head.
|
||||||
|
const BG_TOP = ROW_TONAL_TOP // matrix shares the whole visuals canvas
|
||||||
|
const BG_BOT = ROW_DRUMS_BOT
|
||||||
|
const BG_ROWS = BG_BOT - BG_TOP + 1
|
||||||
|
const BG_L = COL_INSIDE_L
|
||||||
|
const BG_COLS = LANE_W
|
||||||
|
const BG_BLANK = ' '.repeat(BG_COLS)
|
||||||
|
|
||||||
function blankLanes() {
|
// Palette runs dim → bright per the spec; fresh text takes the bright end.
|
||||||
colour(COL_DIM, COL_BG)
|
const BG_PALETTE = [97,243,242,242,241,241,241] // index 0 = freshest .. last = oldest
|
||||||
const blank = ' '.repeat(LANE_W)
|
const BG_LIFE = 48 // rows a cell stays lit before going dark
|
||||||
for (let r = ROW_TONAL_TOP; r <= ROW_TONAL_BOT; r++)
|
|
||||||
mvtext(r, COL_INSIDE_L, blank)
|
const bgChar = new Uint8Array(BG_ROWS * BG_COLS)
|
||||||
for (let r = ROW_DRUMS_TOP; r <= ROW_DRUMS_BOT; r++)
|
const bgLvl = new Int8Array(BG_ROWS * BG_COLS) // 0 = dark, BG_LIFE = freshest
|
||||||
mvtext(r, COL_INSIDE_L, blank)
|
const bgDith = new Uint8Array(BG_ROWS * BG_COLS) // per-cell ordered-dither threshold 0..15
|
||||||
|
|
||||||
|
// Ordered colour dithering. Each opcode atom (the AA / bb / cc of an "AA:bbcc"
|
||||||
|
// token) is stamped with ONE 4×4 Bayer threshold taken from its start cell, so
|
||||||
|
// the atom dithers as a coherent unit while neighbouring atoms differ — this
|
||||||
|
// stipples the otherwise-flat palette bands of the ageing tail into a smooth
|
||||||
|
// gradient. The threshold biases the floor() that picks between the two palette
|
||||||
|
// entries bracketing a cell's fractional colour index.
|
||||||
|
const BG_BAYER = [
|
||||||
|
0, 8, 2, 10,
|
||||||
|
12, 4, 14, 6,
|
||||||
|
3, 11, 1, 9,
|
||||||
|
15, 7, 13, 5
|
||||||
|
]
|
||||||
|
const BG_DITHER_N = 16
|
||||||
|
function bgBayerAt(gr, gc) { return BG_BAYER[(gr & 3) * 4 + (gc & 3)] }
|
||||||
|
|
||||||
|
// BG_PALETTE[0] is reserved for the freshest row — the cells appended this very
|
||||||
|
// row (lvl == BG_LIFE) — no matter how large BG_LIFE is. Its continuous index
|
||||||
|
// is pinned to exactly 0, which no dither bias can lift, so it stays solid.
|
||||||
|
// Ageing levels carry a *fractional* palette index in [1, BG_LAST]; the dither
|
||||||
|
// resolves that fraction into a spatial mix of the two bracketing entries.
|
||||||
|
const BG_LAST = BG_PALETTE.length - 1
|
||||||
|
const bgContLut = new Float32Array(BG_LIFE + 1)
|
||||||
|
bgContLut[BG_LIFE] = 0
|
||||||
|
for (let lvl = 1; lvl < BG_LIFE; lvl++) {
|
||||||
|
const span = BG_LIFE - 2 // ageing steps between the endpoints
|
||||||
|
const age = (BG_LIFE - 1) - lvl // 0 = freshest aged .. span = oldest
|
||||||
|
const t = span > 0 ? age / span : 0
|
||||||
|
let f = 1 + t * (BG_LAST - 1) // continuous index in [1, BG_LAST]
|
||||||
|
if (f > BG_LAST) f = BG_LAST
|
||||||
|
if (f < 1) f = 1
|
||||||
|
bgContLut[lvl] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
let bgHeadR = 0, bgHeadC = 0
|
||||||
|
|
||||||
|
function bgNewline() { bgHeadR = (bgHeadR + 1) % BG_ROWS; bgHeadC = 0 }
|
||||||
|
|
||||||
|
function bgPut(code) { // single glue char; caller guarantees room
|
||||||
|
const idx = bgHeadR * BG_COLS + bgHeadC
|
||||||
|
bgChar[idx] = code; bgLvl[idx] = BG_LIFE; bgDith[idx] = bgBayerAt(bgHeadR, bgHeadC)
|
||||||
|
bgHeadC++
|
||||||
|
}
|
||||||
|
|
||||||
|
function bgPutAtom(c0, c1) { // 2-char atom; wraps as a unit, dithers as a unit
|
||||||
|
if (bgHeadC + 2 > BG_COLS) bgNewline()
|
||||||
|
const base = bgHeadR * BG_COLS
|
||||||
|
const d = bgBayerAt(bgHeadR, bgHeadC) // one threshold for the whole atom
|
||||||
|
bgChar[base + bgHeadC] = c0; bgLvl[base + bgHeadC] = BG_LIFE; bgDith[base + bgHeadC] = d; bgHeadC++
|
||||||
|
bgChar[base + bgHeadC] = c1; bgLvl[base + bgHeadC] = BG_LIFE; bgDith[base + bgHeadC] = d; bgHeadC++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay out one "AA:bbcc" token (prefix2 = 2 chars, val4 = 4 chars) with the
|
||||||
|
// break rules above.
|
||||||
|
function bgEmitToken(prefix2, val4) {
|
||||||
|
if (bgHeadC > 0) { // separator space between tokens
|
||||||
|
if (bgHeadC + 3 > BG_COLS) bgNewline() // ...carried to the next line if needed
|
||||||
|
bgPut(0x20)
|
||||||
|
}
|
||||||
|
bgPutAtom(prefix2.charCodeAt(0), prefix2.charCodeAt(1)) // AA
|
||||||
|
if (bgHeadC + 3 <= BG_COLS) { // colon + bb both fit on this line
|
||||||
|
bgPut(0x3A) // ':'
|
||||||
|
bgPutAtom(val4.charCodeAt(0), val4.charCodeAt(1)) // bb
|
||||||
|
} else { // drop the colon, bb opens the next line
|
||||||
|
bgNewline()
|
||||||
|
bgPutAtom(val4.charCodeAt(0), val4.charCodeAt(1)) // bb
|
||||||
|
}
|
||||||
|
bgPutAtom(val4.charCodeAt(2), val4.charCodeAt(3)) // cc (may wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the matrix by one tracker row: decay every lit cell one step, then
|
||||||
|
// stream the pseudo-opcodes for whatever the row's cells carry. A cue change
|
||||||
|
// rolls the print head back to the top first.
|
||||||
|
function bgAdvanceRow(cueIdx, rowIdx, cueChanged) {
|
||||||
|
for (let i = 0; i < bgLvl.length; i++) {
|
||||||
|
if (bgLvl[i] > 0) bgLvl[i]--
|
||||||
|
}
|
||||||
|
if (cueChanged) { bgHeadR = 0; bgHeadC = 0 }
|
||||||
|
const cue = song.cues[cueIdx]
|
||||||
|
if (!cue) return
|
||||||
|
const off = rowIdx * 8
|
||||||
|
for (let v = 0; v < song.numVoices; v++) {
|
||||||
|
const patIdx = cue.ptns[v]
|
||||||
|
if (patIdx === CUE_EMPTY || patIdx >= song.numPats) continue
|
||||||
|
const pat = song.patterns[patIdx]
|
||||||
|
if (!pat) continue
|
||||||
|
const note = pat[off] | (pat[off + 1] << 8)
|
||||||
|
const voleff = pat[off + 3]
|
||||||
|
const paneff = pat[off + 4]
|
||||||
|
const effop = pat[off + 5]
|
||||||
|
const effarg = pat[off + 6] | (pat[off + 7] << 8)
|
||||||
|
if (note !== 0)
|
||||||
|
bgEmitToken('NT', note.toString(16).toUpperCase().padStart(4, '0'))
|
||||||
|
if (voleff !== 0 && voleff !== 0xC0)
|
||||||
|
bgEmitToken('VO', (voleff >>> 6) + '.' + (voleff & 63).toString(10).padStart(2, '0'))
|
||||||
|
if (paneff !== 0 && paneff !== 0xC0)
|
||||||
|
bgEmitToken('PN', (paneff >>> 6) + '.' + (paneff & 63).toString(10).padStart(2, '0'))
|
||||||
|
if (effop !== 0)
|
||||||
|
bgEmitToken('F' + effop.toString(36).toUpperCase()[0],
|
||||||
|
effarg.toString(16).toUpperCase().padStart(4, '0'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint the matrix as the canvas backdrop; the event lanes draw over it. Each
|
||||||
|
// strip is blanked in one shot, then its lit cells are overlaid (spaces and dark
|
||||||
|
// cells skipped), batching colour switches so same-age runs share one call.
|
||||||
|
function drawBackground() {
|
||||||
|
let curFg = -1
|
||||||
|
for (let gr = 0; gr < BG_ROWS; gr++) {
|
||||||
|
const sr = BG_TOP + gr
|
||||||
|
colour(COL_DIM, COL_BG); curFg = COL_DIM
|
||||||
|
con.move(sr, BG_L)
|
||||||
|
print(BG_BLANK)
|
||||||
|
const base = gr * BG_COLS
|
||||||
|
for (let gc = 0; gc < BG_COLS; gc++) {
|
||||||
|
const lvl = bgLvl[base + gc]
|
||||||
|
if (lvl <= 0) continue
|
||||||
|
const ch = bgChar[base + gc]
|
||||||
|
if (ch === 0x20) continue
|
||||||
|
let idx = Math.floor(bgContLut[lvl] + (bgDith[base + gc] + 0.5) / BG_DITHER_N)
|
||||||
|
if (idx > BG_LAST) idx = BG_LAST
|
||||||
|
if (idx < 0) idx = 0
|
||||||
|
const fg = BG_PALETTE[idx]
|
||||||
|
if (fg !== curFg) { colour(fg, COL_BG); curFg = fg }
|
||||||
|
mvprn(sr, BG_L + gc, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function envColour(arch, volFrac) {
|
function envColour(arch, volFrac) {
|
||||||
@@ -883,7 +1029,7 @@ function drawEventMetal(ev, stage, volFrac, liveNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderEvents() {
|
function renderEvents() {
|
||||||
blankLanes()
|
drawBackground()
|
||||||
for (let v = 0; v < song.numVoices; v++) {
|
for (let v = 0; v < song.numVoices; v++) {
|
||||||
const ev = events[v]
|
const ev = events[v]
|
||||||
if (!ev) continue
|
if (!ev) continue
|
||||||
@@ -1014,8 +1160,10 @@ try {
|
|||||||
const curCue = audio.getCuePosition(0)
|
const curCue = audio.getCuePosition(0)
|
||||||
const curRow = audio.getTrackerRow(0)
|
const curRow = audio.getTrackerRow(0)
|
||||||
if (curCue !== lastSeenCue || curRow !== lastSeenRow) {
|
if (curCue !== lastSeenCue || curRow !== lastSeenRow) {
|
||||||
// Row boundary — spawn new events, reset synthetic tick counter.
|
// Row boundary — spawn new events, advance the matrix background
|
||||||
|
// (a cue change rolls its print head to the top), reset tick count.
|
||||||
spawnEventsForRow(curCue, curRow)
|
spawnEventsForRow(curCue, curRow)
|
||||||
|
bgAdvanceRow(curCue, curRow, curCue !== lastSeenCue)
|
||||||
lastSeenCue = curCue
|
lastSeenCue = curCue
|
||||||
lastSeenRow = curRow
|
lastSeenRow = curRow
|
||||||
synthTick = 0
|
synthTick = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user