summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rw-r--r--src/web/app.mjs39
-rw-r--r--src/web/pardes.css8
2 files changed, 43 insertions, 4 deletions
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) {
diff --git a/src/web/pardes.css b/src/web/pardes.css
index a959e6c3..d523ec5c 100644
--- a/src/web/pardes.css
+++ b/src/web/pardes.css
@@ -58,6 +58,14 @@ body {
white-space: pre;
}
+/* The cell stays body-sized; only the glyph changes. The inherited fixed
+ line-height centers the smaller face vertically, and one glyph remains
+ centered in its original column just like the native atlas backends. */
+.pardes-cell.is-tagline {
+ font-size: var(--pardes-tagline-font-size, 100%);
+ text-align: center;
+}
+
.pardes-cell.is-cursor {
color: var(--cell-bg) !important;
background: var(--cell-fg) !important;