From 1551e409c31992437cb2fa864f576d45c8433801 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 16 Aug 2026 15:49:12 -0300 Subject: big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh --- src/web/app.mjs | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) (limited to 'src/web/app.mjs') diff --git a/src/web/app.mjs b/src/web/app.mjs index d7a4b6f6..631c6067 100644 --- a/src/web/app.mjs +++ b/src/web/app.mjs @@ -1,4 +1,7 @@ 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(); @@ -31,6 +34,9 @@ export class DomRenderer { this.cols = 0; this.rowCount = 0; this.snapshot = null; + const taglinePercent = this.wasm.pardes_gui_tagline_font_percent(); + if (taglinePercent < 1 || taglinePercent > 100) throw new Error(`bad tagline font percentage ${taglinePercent}`); + this.root.style.setProperty("--pardes-tagline-font-size", `${taglinePercent}%`); } render() { @@ -117,6 +123,7 @@ export class PardesRuntime { this.running = false; this.pointer = null; this.frameHandle = 0; + this.animationClock = { last: null, lag: 0 }; this.bindEvents(); } @@ -130,7 +137,8 @@ export class PardesRuntime { this.renderer.render(); this.drainEffects(); this.running = true; - this.frameHandle = requestAnimationFrame(() => this.frame()); + this.animationClock = { last: null, lag: 0 }; + this.frameHandle = requestAnimationFrame((timestamp) => this.frame(timestamp)); this.root.focus({ preventScroll: true }); } @@ -140,12 +148,14 @@ export class PardesRuntime { this.wasm.pardes_deinit(); } - frame() { + frame(timestamp) { if (!this.running) return; - this.wasm.pardes_tick(); + for (let ticks = animationTicks(this.animationClock, timestamp); ticks > 0; ticks -= 1) { + this.wasm.pardes_tick(); + } this.renderer.render(); this.drainEffects(); - this.frameHandle = requestAnimationFrame(() => this.frame()); + this.frameHandle = requestAnimationFrame((nextTimestamp) => this.frame(nextTimestamp)); } measure() { @@ -299,6 +309,7 @@ export class PardesRuntime { this.root.addEventListener("pointermove", (event) => this.pointerMove(event)); this.root.addEventListener("pointerup", (event) => this.pointerUp(event)); this.root.addEventListener("pointercancel", (event) => this.pointerUp(event)); + this.root.addEventListener("pointerleave", () => this.wasm.pardes_pointer_leave()); this.root.addEventListener("wheel", (event) => this.wheel(event), { passive: false }); globalThis.addEventListener?.("resize", () => this.resize()); } @@ -342,6 +353,25 @@ 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; +} + export async function instantiate(url = "./pardes.wasm") { const response = await fetch(url); if (!response.ok) throw new Error(`fetch ${url}: HTTP ${response.status}`); @@ -386,6 +416,7 @@ function applyStyle(cell, fgCode, bgCode, attrs, cursor, cursorBar) { cell.classList.toggle("is-cursor", cursor); cell.classList.toggle("is-bar", cursor && cursorBar); cell.classList.toggle("is-blinking", Boolean(attrs & (1 << 3))); + cell.classList.toggle("is-tagline", Boolean(attrs & ATTR_TAGLINE)); } function color(encoded, fallback) { -- cgit v1.3