summaryrefslogtreecommitdiff
path: root/src/web/app.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/web/app.mjs')
-rw-r--r--src/web/app.mjs74
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);