diff options
Diffstat (limited to 'src/web/app.mjs')
| -rw-r--r-- | src/web/app.mjs | 74 |
1 files changed, 28 insertions, 46 deletions
diff --git a/src/web/app.mjs b/src/web/app.mjs index 11d8f228..03829178 100644 --- a/src/web/app.mjs +++ b/src/web/app.mjs @@ -1,7 +1,5 @@ const CELL_SIZE = 20; const ATTR_TAGLINE = 1 << 7; -const ANIMATION_FRAME_MS = 1000 / 60; -const MAX_ANIMATION_TICKS = 4; const DEFAULT_FG = "#cccccc"; const DEFAULT_BG = "#121212"; const decoder = new TextDecoder(); @@ -66,13 +64,16 @@ export class DomRenderer { if (!val(0)) continue; const gripCols = this.wasm.pardes_grip_columns?.() ?? 2; const textInset = this.wasm.pardes_tag_text_inset?.() ?? gripCols; - if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) { - const offset = this.wasm.pardes_tagline_band_offset(val(5),rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256; - for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(val(5)*cols+x,offset); - if (val(1) === 2) paneIndicators.add(val(5)*cols+val(4)-textInset); - } - for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) { - if (val(5) < rows) tagCovered[val(5) * cols + x] = 1; + // A row to each line of a taller tag. + for (let y = val(5); y < val(5) + Math.max(1, val(11)); y++) { + if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) { + const offset = this.wasm.pardes_tagline_band_offset(y,rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256; + for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(y*cols+x,offset); + if (val(1) === 2) paneIndicators.add(y*cols+val(4)-textInset); + } + for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) { + if (y < rows) tagCovered[y * cols + x] = 1; + } } } } @@ -120,27 +121,35 @@ export class DomRenderer { const nodes = []; const bw = this.cellWidth, bh = this.cellHeight, tw = this.taglineWidth, th = this.taglineHeight; for (let i = 0; i < this.wasm.pardes_tag_layer_limit(); i++) { - const values = Array.from({length:11}, (_,field) => this.wasm.pardes_tag_layer_value(i,field)); - const cols = values[0]; + const layerValues = Array.from({length:13}, (_,field) => this.wasm.pardes_tag_layer_value(i,field)); + const cols = layerValues[0]; if (!cols) continue; const ptr = this.wasm.pardes_tag_layer_cells(i); if (!ptr) continue; - const bytes = new Uint8Array(this.wasm.memory.buffer,ptr,cols*CELL_SIZE); + // A row to each line of a taller tag, each drawn as a band of its own. + for (let line = 0; line < Math.max(1, layerValues[11]); line++) { + const key = i*64+line; + const values = layerValues.slice(0,11); + values[5] = layerValues[5] + line; + values[7] = 1; + values[8] = layerValues[12] === line ? layerValues[8] : 65535; + const bytes = new Uint8Array(this.wasm.memory.buffer,ptr+line*cols*CELL_SIZE,cols*CELL_SIZE); const offset = this.wasm.pardes_tagline_band_offset(values[5],this.rowCount*bh*256,Math.round(bh*256),Math.round(th*256))/256; const gripCols = this.wasm.pardes_grip_columns?.() ?? 2; const gripX = values[4] - (this.wasm.pardes_tag_text_inset?.() ?? gripCols); const gripCell = canonical[values[5]*this.cols+gripX]; const grip = values[1] === 2 && gripX >= 0 && gripCell && th >= 1 && tw >= 1; const geometry = [bw,bh,tw,th,offset,...values,grip,gripCell?.fg,gripCell?.bg,gripCell?.attrs,gripCell?.text,globalThis.devicePixelRatio].join(":"); - const cached = this.tagNodes.get(i); + const cached = this.tagNodes.get(key); if (cached && cached.geometry === geometry && cached.bytes.length === bytes.length && cached.bytes.every((byte,index)=>byte===bytes[index])) { nodes.push(cached.node); if (cached.grip) nodes.push(cached.grip); continue; } - const view = new DataView(this.wasm.memory.buffer,ptr,cols*CELL_SIZE); + const view = new DataView(this.wasm.memory.buffer,ptr+line*cols*CELL_SIZE,cols*CELL_SIZE); const layer = this.root.ownerDocument.createElement("div"); layer.dataset.tagLayer = String(i); + layer.dataset.tagLine = String(line); layer.style.cssText = `position:absolute;overflow:hidden;white-space:pre;left:${values[4]*bw}px;top:${values[5]*bh+offset}px;width:${values[6]*bw}px;height:${th}px;background:${color(values[10],DEFAULT_BG)}`; for (let col = 0; col < cols; col++) { const off = col*CELL_SIZE; @@ -173,9 +182,10 @@ export class DomRenderer { gripNode.append(mark); } } - this.tagNodes.set(i,{geometry,bytes:bytes.slice(),node:layer,grip:gripNode}); + this.tagNodes.set(key,{geometry,bytes:bytes.slice(),node:layer,grip:gripNode}); nodes.push(layer); if (gripNode) nodes.push(gripNode); + } } if (nodes.length !== this.tagRoot.childNodes.length || nodes.some((node,index)=>node!==this.tagRoot.childNodes[index])) this.tagRoot.replaceChildren(...nodes); } @@ -325,7 +335,6 @@ export class PardesRuntime { this.running = false; this.pointer = null; this.frameHandle = 0; - this.animationClock = { last: null, lag: 0 }; this.bindEvents(); } @@ -338,7 +347,6 @@ export class PardesRuntime { this.wasm.pardes_tick(); this.renderer.render(); this.running = true; - this.animationClock = { last: null, lag: 0 }; this.frameHandle = requestAnimationFrame((timestamp) => this.frame(timestamp)); this.root.focus({ preventScroll: true }); } @@ -355,13 +363,9 @@ export class PardesRuntime { this.resize(); this.root.style.setProperty("--pardes-tagline-font-size", `${this.measuredTaglinePercent}%`); } - // JS owns the animation clock; the pump that follows applies those ticks, - // runs the core's effects and presents one frame however fast the display - // is. Both halves are needed: an idle core still has input and effects. - for (let ticks = animationTicks(this.animationClock, timestamp); ticks > 0; ticks -= 1) { - this.wasm.pardes_animation_tick(); - } - this.wasm.pardes_tick(); + // The core's clock is this timestamp: it steps its own animations + // by it, however fast the display refreshes. + this.wasm.pardes_tick(timestamp); if (this.wasm.pardes_should_quit()) this.running = false; this.renderer.render(); this.frameHandle = requestAnimationFrame((nextTimestamp) => this.frame(nextTimestamp)); @@ -559,28 +563,6 @@ export class PardesRuntime { } } -/// Spend monotonic requestAnimationFrame time as fixed 60 Hz core ticks. A -/// high-refresh display therefore renders repeated states rather than making -/// frame-count transitions run twice as fast. Long suspension is capped so a -/// returning tab cannot execute an unbounded loop before drawing. -export function animationTicks(clock, timestamp) { - if (!Number.isFinite(timestamp)) return 0; - if (clock.last === null || timestamp < clock.last) { - clock.last = timestamp; - clock.lag = 0; - return 0; - } - const elapsed = Math.min(timestamp - clock.last, ANIMATION_FRAME_MS * MAX_ANIMATION_TICKS); - clock.last = timestamp; - clock.lag += elapsed; - const ticks = Math.min(MAX_ANIMATION_TICKS, Math.floor((clock.lag + 1e-7) / ANIMATION_FRAME_MS)); - clock.lag -= ticks * ANIMATION_FRAME_MS; - return ticks; -} - -/// The four things the core asks the page for, as the module's imports. The -/// holder exists because the import object has to be built before the instance -/// whose memory those imports read. export function browserHost() { const host = { wasm: null }; const view = (ptr, len) => new Uint8Array(host.wasm.memory.buffer, ptr, len); |
