diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7299816..8e9bf59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -289,6 +289,9 @@ a lot with machine load, so compare against an existing wallpaper measured back - Bound per-pixel loops to the area you're drawing, not the whole screen. The `Canvas` helpers already do. - Precompute noise and textures into typed arrays in the constructor or `layout()` instead of calling `fbm` per pixel per frame. +- Every cell that changes is sent to the terminal. The engine holds back a cell's color change until it reaches 3 + levels of 255 in some channel (a new glyph always sends), so slow light such as an aurora or twinkling stars costs + little output; flickering glyphs still cost a lot. - `shape()` costs about the area of the shape's bounding box times its parts. Draw a sprawling thing (a tree, a palm's fronds) as several smaller calls, and skip things that are off screen. diff --git a/src/engine.tsx b/src/engine.tsx index ca9fbfc..61f9ed1 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -345,6 +345,33 @@ function evalQuad(q: Float32Array, mask: number) { split.err = err } +// The terminal is sent every cell whose character or colors differ from the last frame, and slow light (aurora, twinkle, +// bloom around moving things) nudges hundreds of cells by a level or two of 255 each frame. A color the engine drew +// that is within QUIET levels in every channel of what the cell showed last frame keeps last frame's value, so drift +// builds up unseen and is sent once it adds up; a new glyph always sends its true colors. +const QUIET = 3 + +function quieten(char: Uint32Array, fg: Uint16Array, bg: Uint16Array, inputFg: Uint16Array, inputBg: Uint16Array, shownChar: Uint32Array, shownFg: Uint16Array, shownBg: Uint16Array, cells: number) { + for (let i = 0; i < cells; i++) { + if (char[i] !== shownChar[i]) { + shownChar[i] = char[i] + continue + } + const o = i * 4 + settle(fg, inputFg, shownFg, o) + settle(bg, inputBg, shownBg, o) + } + shownFg.set(fg) + shownBg.set(bg) +} + +// Colors OpenCode drew (unchanged from its input) are left exactly as they are. +function settle(colors: Uint16Array, input: Uint16Array, shown: Uint16Array, o: number) { + if (colors[o] === input[o] && colors[o + 1] === input[o + 1] && colors[o + 2] === input[o + 2] && colors[o + 3] === input[o + 3]) return + for (let c = o; c < o + 4; c++) if (Math.abs(colors[c] - shown[c]) >= QUIET) return + for (let c = o; c < o + 4; c++) colors[c] = shown[c] +} + function isBlock(char: number) { return char >= BLOCKS_START && char <= BLOCKS_END } @@ -470,6 +497,12 @@ export function createEngine( let cellTint = new Int32Array(0) let previousMask = new Int16Array(0) let history = new Float32Array(0) + // What each cell showed last frame, and the colors OpenCode drew this frame before the scene went in, for quieten. + let shownChar = new Uint32Array(0) + let shownFg = new Uint16Array(0) + let shownBg = new Uint16Array(0) + let inputFg = new Uint16Array(0) + let inputBg = new Uint16Array(0) let lastStep = 0 let lastTarget = "" let imageCell = new Uint8Array(0) @@ -870,6 +903,15 @@ export function createEngine( } layerPlaced = false + if (shownChar.length !== cells) { + shownChar = new Uint32Array(cells) + shownFg = new Uint16Array(cells * 4) + shownBg = new Uint16Array(cells * 4) + inputFg = new Uint16Array(cells * 4) + inputBg = new Uint16Array(cells * 4) + } + inputFg.set(fg) + inputBg.set(bg) const q = quad const cover = look.scrim const reach = look.reach @@ -926,6 +968,7 @@ export function createEngine( applyTint(bg, o, cellTint[i]) } } + quieten(char, fg, bg, inputFg, inputBg, shownChar, shownFg, shownBg, cells) // Build next frame's image now; the back layer places it before OpenCode draws, so text lands on top. if (pixels && (advanced || !nextImage)) { @@ -1005,6 +1048,7 @@ export function createEngine( lastTarget = "" history = new Float32Array(0) previousMask = new Int16Array(0) + shownChar = new Uint32Array(0) lastActivity = Date.now() focused = true schedule()