From d89b6bc28e0226cf90ff9cc4a2d5797484944464 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Wed, 15 Jul 2026 07:10:30 +0000 Subject: [PATCH] api.overlay: a crisp vector UI layer on uiCtx, above the pixel buffer (+ cancelok ?vec) The approachable-UI ask, done as real (gated) infrastructure. A new piece-facing api.overlay(displayList) ships a small list of vector ops — rounded rects, text, lines — that bios replays onto the native UI canvas (uiCtx) with anti-aliased Canvas 2D, above the pixel-buffer render. The piece sends coords in its own screen units; bios scales by content.width and clears fresh each frame (and once more when a piece drops the overlay, so it never lingers). Additive and gated: api.overlay is a no-op unless a piece calls it, and bios only touches uiCtx when content.overlay is present — so every existing piece is unaffected. This is the "write to both" the pixel-first architecture didn't have: pixel pads underneath, crisp vector UI on top. cancelok gains ?vec: same 8-way D-pad, but the tiles + labels are handed over as a vector overlay instead of drawn into the buffer — smooth rounded tiles and crisp big letters over the pixel pad. Default (no flag) is the pixel UI, untouched. Files: disk.mjs (api.overlay + sendData.overlay passthrough), bios.mjs (drawVectorOverlay + render-loop replay on uiCtx), cancelok.mjs (VEC flag + vectorControls). Co-Authored-By: Claude Opus 4.8 (1M context) --- system/public/aesthetic.computer/bios.mjs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/public/aesthetic.computer/disks/cancelok.mjs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- system/public/aesthetic.computer/lib/disk.mjs | 11 +++++++++++ system/public/papers.aesthetic.computer/gaps-in-granularity-26-arxiv.pdf | 0 system/public/papers.aesthetic.computer/thumbs/gaps-in-granularity-26-arxiv.jpg | 0 5 file(s) changed, 135 insertion(s)(+), 20 deletion(s)(-) diff --git a/system/public/aesthetic.computer/bios.mjs b/system/public/aesthetic.computer/bios.mjs --- a/system/public/aesthetic.computer/bios.mjs +++ b/system/public/aesthetic.computer/bios.mjs @@ -46,6 +46,53 @@ import { createWebGLBlitter } from "./lib/webgl-blit.mjs"; // import * as TwoD from "./lib/2d.mjs"; // 🆕 2D GPU Renderer. const TwoD = undefined; +let hadVectorOverlay = false; // so the vector overlay clears once when a piece drops it + +// 🪟 Replay a piece's vector-overlay display-list onto the native UI canvas — crisp +// anti-aliased rounded rects, text and lines above the pixel buffer. Cleared fresh +// each frame (the piece re-sends every paint). Coords are logical; the ctx is +// already dpi-scaled by the caller. +function drawVectorOverlay(ctx, ops) { + if (!ctx || !ops) return; + const rgba = (c) => `rgba(${c?.[0] || 0},${c?.[1] || 0},${c?.[2] || 0},${(c?.[3] ?? 255) / 255})`; + const round = (x, y, w, h, r) => { + const rr = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + rr, y); + ctx.arcTo(x + w, y, x + w, y + h, rr); + ctx.arcTo(x + w, y + h, x, y + h, rr); + ctx.arcTo(x, y + h, x, y, rr); + ctx.arcTo(x, y, x + w, y, rr); + ctx.closePath(); + }; + for (const op of ops) { + const k = op[0]; + if (k === "rr") { + round(op[1], op[2], op[3], op[4], op[5]); + ctx.fillStyle = rgba(op[6]); + ctx.fill(); + } else if (k === "stroke") { + round(op[1], op[2], op[3], op[4], op[5]); + ctx.strokeStyle = rgba(op[6]); + ctx.lineWidth = op[7] || 1; + ctx.stroke(); + } else if (k === "text") { + ctx.fillStyle = rgba(op[5]); + ctx.font = `700 ${op[4] || 12}px ui-monospace, "SF Mono", monospace`; + ctx.textAlign = op[6] || "center"; + ctx.textBaseline = "middle"; + ctx.fillText(op[1], op[2], op[3]); + } else if (k === "line") { + ctx.strokeStyle = rgba(op[5]); + ctx.lineWidth = op[6] || 1; + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.moveTo(op[1], op[2]); + ctx.lineTo(op[3], op[4]); + ctx.stroke(); + } + } +} const { assign, keys } = Object; const { round, floor, min, max } = Math; @@ -18854,6 +18901,28 @@ } uiCtx.clearRect(0, uiCtx.canvas.height / dpi - 64, 64, 64); // Bottom left uiCtx.clearRect(uiCtx.canvas.width / dpi - 64, 0, 64, 64); // Top right + + // 🪟 Vector overlay — a piece's api.overlay() display-list, drawn crisp on the + // native UI canvas above the pixel buffer. Gated: only pieces that send one + // pay anything. The piece sends coords in ITS screen units, so we scale by the + // ratio of the UI canvas to the piece resolution (content.width). Cleared + // fresh each frame; the cursor still draws on top afterward. + if (content.overlay) { + const logicalW = uiCtx.canvas.width / dpi; + const logicalH = uiCtx.canvas.height / dpi; + uiCtx.clearRect(0, 0, logicalW, logicalH); + const s = content.width ? logicalW / content.width : 1; + uiCtx.save(); + uiCtx.scale(s, s); + drawVectorOverlay(uiCtx, content.overlay); + uiCtx.restore(); + hadVectorOverlay = true; + } else if (hadVectorOverlay) { + // The overlay just went away (left the piece) — clear it once so it doesn't + // linger on the UI canvas. + uiCtx.clearRect(0, 0, uiCtx.canvas.width / dpi, uiCtx.canvas.height / dpi); + hadVectorOverlay = false; + } pen?.render(uiCtx, canvasRect); // ️ 🐭 Draw the cursor. diff --git a/system/public/aesthetic.computer/disks/cancelok.mjs b/system/public/aesthetic.computer/disks/cancelok.mjs --- a/system/public/aesthetic.computer/disks/cancelok.mjs +++ b/system/public/aesthetic.computer/disks/cancelok.mjs @@ -49,6 +49,7 @@ let lisp = null; let mounting = false; const mods = new Map(); +let VEC = false; // ?vec — draw the D-pad as a crisp vector overlay (uiCtx), not pixels let judge = null; let shownAt = 0; let taps = 0; @@ -386,6 +387,9 @@ function boot(api) { api.wipe(...WALL); api.hud?.label?.(""); + // `?vec` parses to { vec: "" } — an empty string, which is falsy — so test for + // PRESENCE, not truthiness. (Also accept the `cancelok:vec` param form.) + VEC = (api.query && "vec" in api.query) || api.params?.includes?.("vec") || false; judge = api.handle?.() || null; load(api) .then(() => { @@ -495,23 +499,27 @@ // reads red up top, east yellow to the right, and so on, always in the same // place. Dark, so the pad still pops; never pure black. const swiping = !!grab || !!move; wipe(...WALL); - for (const d of DIRS) { - let [zx, zy, zw, zh] = zoneRect(d, g, w, h); - const gap = 3; // soft neutral seam between tiles, so each reads as its own shape - zx += gap; - zy += gap; - zw -= gap * 2; - zh -= gap * 2; - // The WHOLE tile lights, not just its label: dim at rest, brighter while you - // pull, brightest under the pointer, and a flash the moment it's taken. - const on = hover === d; - const lit = pressed === d ? pressT : 0; - const f = 0.34 + (swiping ? 0.1 : 0) + (on ? 0.36 : 0) + lit * 0.4; - const col = [Math.min(255, d.col[0] * f), Math.min(255, d.col[1] * f), Math.min(255, d.col[2] * f)]; - // A rounded tile — corners and edges alike get real shape, so nothing butts - // squarely against the round glass and feels off. - tile(ink, zx, zy, zw, zh, Math.min(12, zw / 2, zh / 2), col); - } + // The compass D-pad. In pixel mode it's rounded tiles drawn straight into the + // buffer; in ?vec mode the same tiles are handed to bios as a crisp vector + // overlay (uiCtx) instead — see the VEC block after the web below. + if (!VEC) + for (const d of DIRS) { + let [zx, zy, zw, zh] = zoneRect(d, g, w, h); + const gap = 3; // soft neutral seam between tiles, so each reads as its own shape + zx += gap; + zy += gap; + zw -= gap * 2; + zh -= gap * 2; + // The WHOLE tile lights, not just its label: dim at rest, brighter while you + // pull, brightest under the pointer, and a flash the moment it's taken. + const on = hover === d; + const lit = pressed === d ? pressT : 0; + const f = 0.34 + (swiping ? 0.1 : 0) + (on ? 0.36 : 0) + lit * 0.4; + const col = [Math.min(255, d.col[0] * f), Math.min(255, d.col[1] * f), Math.min(255, d.col[2] * f)]; + // A rounded tile — corners and edges alike get real shape, so nothing butts + // squarely against the round glass and feels off. + tile(ink, zx, zy, zw, zh, Math.min(12, zw / 2, zh / 2), col); + } // The web. Every room is its own framed puddle, carrying its frame WITH it — so a // drag pulls the whole web across the grid. The frame is neutral so the coloured @@ -538,9 +546,16 @@ } api.paste(frame, sx, sy); // round the corners, feather the edge, draw the rim } - // The 8-way control surface, on the wall: each direction's key + arrow, lit when - // you hover or take it. Hidden mid-pull — you're already navigating then. - if (!move) controls(api, g, w, h); + // The 8-way control surface, on the wall: each direction's key, lit when you + // hover or take it. Hidden mid-pull — you're already navigating then. + // Pixel mode draws it into the buffer; ?vec mode ships it as a crisp vector + // overlay to bios (native Canvas 2D on uiCtx) instead. + if (!move) { + if (VEC) vectorControls(api, g, w, h); + else controls(api, g, w, h); + } else if (VEC) { + api.overlay?.(null); // clear the overlay while pulling + } // The name isn't drawn here anymore — it arrives as AC's own frontal notice each // time you enter a room (see mount), the same alert that announces a saved handle. @@ -591,6 +606,26 @@ arrow(ink, cx, cy - 4, d.dx, d.dy, 5, col, a); ink(0, 0, 0, a * 0.45).write(d.k.toUpperCase(), { x: cx - 1, y: cy + 3 }, undefined, undefined, undefined, "MatrixChunky8"); ink(...col, a).write(d.k.toUpperCase(), { x: cx - 2, y: cy + 2 }, undefined, undefined, undefined, "MatrixChunky8"); } +} + +// The same D-pad, but as a CRISP VECTOR display-list handed to bios (?vec mode) — +// native rounded rects and anti-aliased text on uiCtx, above the pixel pads. Coords +// are in the piece's screen units; bios scales them to the UI canvas. +function vectorControls(api, g, w, h) { + const list = []; + for (const d of DIRS) { + let [zx, zy, zw, zh] = zoneRect(d, g, w, h); + const gap = 3; + zx += gap; zy += gap; zw -= gap * 2; zh -= gap * 2; + const on = hover === d; + const lit = pressed === d ? pressT : 0; + const f = 0.4 + (on ? 0.4 : 0) + lit * 0.5; + list.push(["rr", zx, zy, zw, zh, Math.min(12, zw / 2, zh / 2), + [Math.min(255, d.col[0] * f), Math.min(255, d.col[1] * f), Math.min(255, d.col[2] * f), 255]]); + const [cx, cy] = zoneCenter(d, g, w, h); + list.push(["text", d.k.toUpperCase(), cx, cy, 12, [255, 255, 255, on || lit ? 255 : 220], "center"]); + } + api.overlay?.(list); } // The soft CRT edge: rounded corners + a fuzzy border. Writing the display buffer diff --git a/system/public/aesthetic.computer/lib/disk.mjs b/system/public/aesthetic.computer/lib/disk.mjs --- a/system/public/aesthetic.computer/lib/disk.mjs +++ b/system/public/aesthetic.computer/lib/disk.mjs @@ -3183,6 +3183,14 @@ const sound = {}; if (color[0] === "yellow" && color[1] === "red") sound.tone = 300; noticeBell(cachedAPI, sound); }, + // 🪟 A crisp vector overlay, drawn on the native UI canvas (uiCtx) above the pixel + // buffer. Hand it a display-list of ops each frame and bios replays them with + // anti-aliased Canvas 2D — rounded rects, text, lines. For UI that wants to look + // like an app, not pixels. Set null / omit to draw nothing. + // overlay([["rr", x,y,w,h,r, [r,g,b,a]], ["text","W", x,y, size, [r,g,b,a], "center"], ...]) + overlay: (list) => { + overlay2D = list || null; + }, // ⌛ Delay a function by `time` number of sim steps. delay: (fun, time) => { hourGlasses.push(new gizmo.Hourglass(time, { completed: () => fun() })); @@ -7583,6 +7591,7 @@ let originalHost; let firstLoad = true; let notice, noticeTimer, noticeColor, noticeOpts; // Renders a full-screen notice on piece-load if present. +let overlay2D = null; // A per-frame vector display-list drawn on uiCtx (api.overlay). async function load( parsed, // If parsed is not an object, then assume it's source code. @@ -15557,6 +15566,7 @@ maybeLeave(); // TODO: Write this up to the data in `painting`. sendData.TwoD = { code: twoDCommands }; + if (overlay2D) sendData.overlay = overlay2D; // 🪟 vector UI display-list → uiCtx // Attach a label buffer if necessary. // Hide label when QR is in fullscreen mode or in device mode (device.html has its own overlay) @@ -16698,6 +16708,7 @@ } send({ type: "render", content: sendData }, transferredObjects); + overlay2D = null; // consumed — the piece re-sets it each paint, so it never staleness-flickers sound.sounds.length = 0; // Empty the sound command buffer. sound.bubbles.length = 0; sound.farts.length = 0; diff --git a/system/public/papers.aesthetic.computer/gaps-in-granularity-26-arxiv.pdf b/system/public/papers.aesthetic.computer/gaps-in-granularity-26-arxiv.pdf new file mode 100644 --- /dev/null +++ b/system/public/papers.aesthetic.computer/gaps-in-granularity-26-arxiv.pdf diff --git a/system/public/papers.aesthetic.computer/thumbs/gaps-in-granularity-26-arxiv.jpg b/system/public/papers.aesthetic.computer/thumbs/gaps-in-granularity-26-arxiv.jpg new file mode 100644 --- /dev/null +++ b/system/public/papers.aesthetic.computer/thumbs/gaps-in-granularity-26-arxiv.jpg -- tangled.sh