Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265<!doctype html><html lang="en"><head><meta charset="utf-8"><title>oskiewar wall</title><!-- The host for wall.js. It owns everything the piece is not allowed to know about: a canvas, the drawing primitives, the stats poll, and the reel videos. The piece gets handed functions and draws.
wall-shell.html?pane=top&key=… DP-2, at eye level wall-shell.html?pane=bottom&key=… DP-1, below
This is a separate shell from mac-test.html on purpose. That file is hashed into the social-preview manifest, so editing it forces a preview re-burn or lith/deploy.fish rolls the deploy back — a bad trade for a dashboard that shares none of its gamepad, auth, or matchmaking machinery.--><style> @font-face { font-family: "Comic Relief"; src: url("./ComicRelief-Regular.ttf") format("truetype"), url("https://papers.aesthetic.computer/foundry/fonts/ComicRelief-Regular.ttf") format("truetype"); font-display: block; } * { margin: 0; padding: 0; } html, body { height: 100%; overflow: hidden; background: #0b0b0d; } canvas { display: block; width: 100vw; height: 100vh; } /* The reels are decoded by real video elements but never shown: the piece draws their frames onto the canvas, so the elements only need to be alive and playing, not visible. `display:none` would let a browser stop decoding, so they are merely moved out of sight. */ #reels { position: fixed; left: -20000px; top: 0; width: 1px; height: 1px; overflow: hidden; }</style></head><body><canvas id="screen" width="2560" height="1440" aria-label="oskiewar wall"></canvas><div id="reels" aria-hidden="true"></div>
<script type="module">import { blockGlyphs } from "./block-font.mjs";
const params = new URLSearchParams(location.search);const pane = params.get("pane") === "bottom" ? "bottom" : "top";const key = params.get("key") || "";const origin = params.get("api") || (location.protocol.startsWith("http") ? location.origin : "https://oskiewar.com");const REFRESH_MS = Number(params.get("every") || 60) * 1000;
const canvas = document.querySelector("#screen");const context = canvas.getContext("2d");
// The piece is written against a fixed 2560x1440 pane and the canvas is scaled// to whatever the monitor is, so nothing in the piece has to think about DPI.function resize() { const density = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); const width = Math.round(window.innerWidth * density); const height = Math.round(window.innerHeight * density); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; }}resize();window.addEventListener("resize", resize);
// --- the drawing surface handed to the piece ---------------------------------
const color = (ink) => { const [r = 0, g = 0, b = 0, a = 255] = ink.flat(); return `rgba(${r},${g},${b},${a / 255})`;};
function wipe(...ink) { context.setTransform(1, 0, 0, 1, 0, 0); context.fillStyle = color(ink); context.fillRect(0, 0, canvas.width, canvas.height);}
function box(x, y, width, height, ...ink) { context.fillStyle = color(ink); context.fillRect(x, y, width, height);}
function line(x1, y1, x2, y2, width, ...ink) { context.strokeStyle = color(ink); context.lineWidth = Math.max(1, width || 1); context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); context.stroke();}
// Heads are round, so the world needs one. oskiewar's own host has this too.function circle(x, y, radius, ...ink) { context.fillStyle = color(ink); context.beginPath(); context.arc(x, y, Math.max(0, radius), 0, Math.PI * 2); context.fill();}
// Seconds since the page opened. Injected rather than taken from Date inside// the piece so a native host can hand over its own monotonic clock later.const wallClock = () => performance.now() / 1000;
function triangle(x1, y1, x2, y2, x3, y3, ...ink) { context.fillStyle = color(ink); context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); context.lineTo(x3, y3); context.closePath(); context.fill();}
// The game's 5x7 HUD font, one filled rect per lit bit. Same advance as the// shell oskiewar itself runs under, so a string measured there measures here.function write(text, x, y, size, ...ink) { const cell = Math.max(2, Math.floor(size / 7)); const originX = x; let penX = x; let penY = y; context.fillStyle = color(ink); for (const character of String(text)) { if (character === "\n") { penX = originX; penY += cell * 9; continue; } const rows = blockGlyphs[character.toUpperCase()] || blockGlyphs["?"]; for (let row = 0; row < 7; row++) { for (let column = 0; column < 5; column++) { if (!(rows[row] & (1 << (4 - column)))) continue; context.fillRect(penX + column * cell, penY + row * cell, Math.max(1, cell - 1), Math.max(1, cell - 1)); } } penX += cell * 6; }}
function comicWrite(text, x, y, size, ...ink) { context.fillStyle = color(ink); context.font = `${size}px "Comic Relief", "Comic Sans MS", Arial, sans-serif`; context.textBaseline = "top"; const lines = String(text).split("\n"); for (let index = 0; index < lines.length; index++) context.fillText(lines[index], x, y + index * size * 1.15);}
// The pane the piece composes for, in its own coordinates. The canvas is scaled// to the real window in paint(), so the piece always draws 2560x1440.const PANE_WIDTH = 2560;const PANE_HEIGHT = 1440;const gameView = () => ({ width: PANE_WIDTH, height: PANE_HEIGHT });
// --- reels ------------------------------------------------------------------
// One <video> per reel, muted and looping so autoplay is allowed. crossOrigin is// deliberately NOT set: Spaces may not answer with CORS headers, and asking for// it would fail the load outright. Drawing an unclean video taints the canvas,// which costs nothing here because nothing reads pixels back.const reelHost = document.querySelector("#reels");const reels = [];const reelsById = new Map();
function syncReels(posts = []) { for (const post of posts) { if (!post.reel || reelsById.has(post.id)) continue; const video = document.createElement("video"); video.src = post.reel; video.muted = true; video.loop = true; video.autoplay = true; video.playsInline = true; video.preload = "auto"; // A wall reloads rarely and these are a few megabytes each, so a failed // load should retry rather than leave a permanent hole. video.addEventListener("error", () => { setTimeout(() => { video.load(); video.play().catch(() => {}); }, 30000); }); reelHost.appendChild(video); video.play().catch(() => {}); const entry = { id: post.id, video }; reelsById.set(post.id, entry); reels.push(entry); }}
// Draw one reel's current frame, cropped to fill the box rather than letterboxed// — a 9:16 reel in a 9:16 tile is exact, and anything else centres on the crop.function reelFrame(index, x, y, width, height) { const entry = reels[index]; const video = entry?.video; if (!video || video.readyState < 2 || !video.videoWidth) return false; const scale = Math.max(width / video.videoWidth, height / video.videoHeight); const drawWidth = video.videoWidth * scale; const drawHeight = video.videoHeight * scale; context.save(); context.beginPath(); context.rect(x, y, width, height); context.clip(); context.drawImage(video, x + (width - drawWidth) / 2, y + (height - drawHeight) / 2, drawWidth, drawHeight); context.restore(); return true;}
const reelCount = () => reels.length;
// --- stats ------------------------------------------------------------------
let stats = null;let lastAt = null;let lastError = "";
async function poll() { const url = new URL("/api/oskiewar-stats", origin); if (key) url.searchParams.set("key", key); try { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); stats = await response.json(); lastAt = Date.now(); lastError = ""; syncReels(stats.reels?.posts); } catch (error) { // Whatever was on screen stays there; the age readout is what tells the // truth about how old it is. lastError = String(error.message || error); }}
const wallStats = () => stats;const wallPane = () => pane;const wallError = () => lastError;const wallAge = () => (lastAt == null ? null : (Date.now() - lastAt) / 1000);
// --- the piece --------------------------------------------------------------
const HOST = { wipe, box, line, triangle, circle, write, comicWrite, gameView, wallClock, wallStats, wallPane, wallError, wallAge, reelFrame, reelCount,};const NAMES = Object.keys(HOST);
const source = await (await fetch("./wall.js", { cache: "no-store" })).text();const piece = new Function(...NAMES, `${source}\nreturn { boot, sim, paint };`)( ...NAMES.map((name) => HOST[name]));
await poll();piece.boot();setInterval(poll, REFRESH_MS);
function frame() { resize(); context.setTransform(1, 0, 0, 1, 0, 0); // Scale the piece's fixed pane onto the real canvas. context.scale(canvas.width / PANE_WIDTH, canvas.height / PANE_HEIGHT); piece.sim(); piece.paint(); requestAnimationFrame(frame);}requestAnimationFrame(frame);</script></body></html>