Monorepo for Aesthetic.Computer aesthetic.computer

Ship current workspace changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+734 -102
+424
kidlisp-wasm/compiler.mjs
··· 1 + // KidLisp → WASM Compiler 2 + // Compiles KidLisp source directly to WebAssembly binary. 3 + 4 + // ─── WASM Binary Encoding ─────────────────────────────────────────── 5 + 6 + function uleb128(value) { 7 + const bytes = []; 8 + do { 9 + let byte = value & 0x7f; 10 + value >>>= 7; 11 + if (value !== 0) byte |= 0x80; 12 + bytes.push(byte); 13 + } while (value !== 0); 14 + return bytes; 15 + } 16 + 17 + function sleb128(value) { 18 + const bytes = []; 19 + let more = true; 20 + while (more) { 21 + let byte = value & 0x7f; 22 + value >>= 7; 23 + if ( 24 + (value === 0 && (byte & 0x40) === 0) || 25 + (value === -1 && (byte & 0x40) !== 0) 26 + ) { 27 + more = false; 28 + } else { 29 + byte |= 0x80; 30 + } 31 + bytes.push(byte); 32 + } 33 + return bytes; 34 + } 35 + 36 + function f32Bytes(value) { 37 + const buf = new ArrayBuffer(4); 38 + new Float32Array(buf)[0] = value; 39 + return [...new Uint8Array(buf)]; 40 + } 41 + 42 + function encodeString(str) { 43 + const bytes = new TextEncoder().encode(str); 44 + return [...uleb128(bytes.length), ...bytes]; 45 + } 46 + 47 + function section(id, contents) { 48 + return [id, ...uleb128(contents.length), ...contents]; 49 + } 50 + 51 + function vec(items) { 52 + return [...uleb128(items.length), ...items.flat()]; 53 + } 54 + 55 + // ─── Color Map ────────────────────────────────────────────────────── 56 + 57 + const COLORS = { 58 + red: [255, 0, 0], 59 + green: [0, 128, 0], 60 + blue: [0, 0, 255], 61 + white: [255, 255, 255], 62 + black: [0, 0, 0], 63 + yellow: [255, 255, 0], 64 + cyan: [0, 255, 255], 65 + magenta: [255, 0, 255], 66 + orange: [255, 165, 0], 67 + purple: [128, 0, 128], 68 + pink: [255, 192, 203], 69 + gray: [128, 128, 128], 70 + grey: [128, 128, 128], 71 + lime: [0, 255, 0], 72 + }; 73 + 74 + // ─── Parser ───────────────────────────────────────────────────────── 75 + 76 + function tokenize(source) { 77 + const tokens = []; 78 + let i = 0; 79 + while (i < source.length) { 80 + const ch = source[i]; 81 + if (ch === "(") { 82 + tokens.push({ type: "lparen" }); 83 + i++; 84 + } else if (ch === ")") { 85 + tokens.push({ type: "rparen" }); 86 + i++; 87 + } else if (ch === "\n") { 88 + tokens.push({ type: "newline" }); 89 + i++; 90 + } else if (/\s/.test(ch)) { 91 + i++; 92 + } else if (ch === ";") { 93 + while (i < source.length && source[i] !== "\n") i++; 94 + } else { 95 + let start = i; 96 + while (i < source.length && !/[\s()]/.test(source[i])) i++; 97 + const atom = source.slice(start, i); 98 + const num = Number(atom); 99 + if (!isNaN(num) && atom !== "") { 100 + tokens.push({ type: "number", value: num }); 101 + } else { 102 + tokens.push({ type: "symbol", value: atom }); 103 + } 104 + } 105 + } 106 + return tokens; 107 + } 108 + 109 + function parse(tokens) { 110 + const lines = []; 111 + let currentLine = []; 112 + let pos = 0; 113 + 114 + function parseExpr() { 115 + if (pos >= tokens.length) return null; 116 + const tok = tokens[pos]; 117 + if (tok.type === "lparen") { 118 + pos++; 119 + const items = []; 120 + while (pos < tokens.length && tokens[pos].type !== "rparen") { 121 + if (tokens[pos].type === "newline") { 122 + pos++; 123 + continue; 124 + } 125 + const expr = parseExpr(); 126 + if (expr) items.push(expr); 127 + } 128 + if (pos < tokens.length) pos++; // skip ) 129 + return { type: "list", items }; 130 + } else if (tok.type === "number") { 131 + pos++; 132 + return { type: "number", value: tok.value }; 133 + } else if (tok.type === "symbol") { 134 + pos++; 135 + return { type: "symbol", value: tok.value }; 136 + } 137 + return null; 138 + } 139 + 140 + while (pos < tokens.length) { 141 + if (tokens[pos].type === "newline") { 142 + if (currentLine.length > 0) { 143 + lines.push(currentLine); 144 + currentLine = []; 145 + } 146 + pos++; 147 + continue; 148 + } 149 + const expr = parseExpr(); 150 + if (expr) currentLine.push(expr); 151 + } 152 + if (currentLine.length > 0) lines.push(currentLine); 153 + 154 + // Wrap bare lines as function calls: 155 + // `ink 255 0 0` → `(ink 255 0 0)` 156 + const result = []; 157 + for (const line of lines) { 158 + if (line.length === 1) { 159 + result.push(line[0]); 160 + } else if (line.length > 1 && line[0].type === "symbol") { 161 + result.push({ type: "list", items: line }); 162 + } else { 163 + for (const expr of line) result.push(expr); 164 + } 165 + } 166 + return result; 167 + } 168 + 169 + // ─── WASM Opcodes ─────────────────────────────────────────────────── 170 + 171 + const OP = { 172 + LOCAL_GET: 0x20, 173 + LOCAL_SET: 0x21, 174 + GLOBAL_GET: 0x23, 175 + GLOBAL_SET: 0x24, 176 + CALL: 0x10, 177 + F32_CONST: 0x43, 178 + F32_ADD: 0x92, 179 + F32_SUB: 0x93, 180 + F32_MUL: 0x94, 181 + F32_DIV: 0x95, 182 + F32_SQRT: 0x91, 183 + F32_ABS: 0x8b, 184 + F32_NEG: 0x8c, 185 + F32_FLOOR: 0x8e, 186 + F32_CEIL: 0x8d, 187 + I32_CONST: 0x41, 188 + I32_ADD: 0x6a, 189 + DROP: 0x1a, 190 + END: 0x0b, 191 + }; 192 + 193 + const F32 = 0x7d; 194 + 195 + // ─── Compiler ─────────────────────────────────────────────────────── 196 + 197 + export class Compiler { 198 + constructor() { 199 + this.types = []; 200 + this.typeMap = new Map(); 201 + this.imports = []; 202 + this.importCount = 0; 203 + this.code = []; 204 + this.setupImports(); 205 + } 206 + 207 + addType(params, results) { 208 + const key = `${params.join(",")}->${results.join(",")}`; 209 + if (this.typeMap.has(key)) return this.typeMap.get(key); 210 + const idx = this.types.length; 211 + this.types.push({ params, results }); 212 + this.typeMap.set(key, idx); 213 + return idx; 214 + } 215 + 216 + addImport(module, name, paramCount, hasReturn = false) { 217 + const params = Array(paramCount).fill(F32); 218 + const results = hasReturn ? [F32] : []; 219 + const typeIdx = this.addType(params, results); 220 + const funcIdx = this.importCount++; 221 + this.imports.push({ module, name, typeIdx }); 222 + return funcIdx; 223 + } 224 + 225 + setupImports() { 226 + this.funcs = {}; 227 + // Drawing primitives — all f32 params 228 + this.funcs.wipe = this.addImport("env", "wipe", 3); 229 + this.funcs.ink = this.addImport("env", "ink", 3); 230 + this.funcs.line = this.addImport("env", "line", 4); 231 + this.funcs.box = this.addImport("env", "box", 4); 232 + this.funcs.circle = this.addImport("env", "circle", 3); 233 + this.funcs.plot = this.addImport("env", "plot", 2); 234 + this.funcs.tri = this.addImport("env", "tri", 6); 235 + 236 + // paint(w, h, frame) → () 237 + this.paintTypeIdx = this.addType([F32, F32, F32], []); 238 + } 239 + 240 + // Emit bytecode that pushes a value onto the WASM stack. 241 + compileExpr(expr) { 242 + if (expr.type === "number") { 243 + this.code.push(OP.F32_CONST, ...f32Bytes(expr.value)); 244 + } else if (expr.type === "symbol") { 245 + this.compileSymbol(expr.value); 246 + } else if (expr.type === "list") { 247 + this.compileCall(expr); 248 + } 249 + } 250 + 251 + compileSymbol(name) { 252 + // paint params: 0=w, 1=h, 2=frame 253 + if (name === "w") { 254 + this.code.push(OP.LOCAL_GET, ...uleb128(0)); 255 + return; 256 + } 257 + if (name === "h") { 258 + this.code.push(OP.LOCAL_GET, ...uleb128(1)); 259 + return; 260 + } 261 + if (name === "frame" || name === "f") { 262 + this.code.push(OP.LOCAL_GET, ...uleb128(2)); 263 + return; 264 + } 265 + 266 + // Division shorthand: w/2, h/3, etc. 267 + const divMatch = name.match(/^(\w+)\/(\d+(?:\.\d+)?)$/); 268 + if (divMatch) { 269 + this.compileSymbol(divMatch[1]); 270 + this.code.push(OP.F32_CONST, ...f32Bytes(parseFloat(divMatch[2]))); 271 + this.code.push(OP.F32_DIV); 272 + return; 273 + } 274 + 275 + // Color names → push 3 f32 values (r, g, b) 276 + if (COLORS[name]) { 277 + const [r, g, b] = COLORS[name]; 278 + this.code.push(OP.F32_CONST, ...f32Bytes(r)); 279 + this.code.push(OP.F32_CONST, ...f32Bytes(g)); 280 + this.code.push(OP.F32_CONST, ...f32Bytes(b)); 281 + return; 282 + } 283 + 284 + throw new Error(`Unknown symbol: ${name}`); 285 + } 286 + 287 + compileCall(expr) { 288 + if (expr.items.length === 0) return; 289 + const head = expr.items[0]; 290 + if (head.type !== "symbol") { 291 + throw new Error(`Expected function name, got ${JSON.stringify(head)}`); 292 + } 293 + 294 + const name = head.value; 295 + const args = expr.items.slice(1); 296 + 297 + // Arithmetic 298 + const arithOp = { "+": OP.F32_ADD, "-": OP.F32_SUB, "*": OP.F32_MUL, "/": OP.F32_DIV }; 299 + if (arithOp[name]) { 300 + this.compileExpr(args[0]); 301 + this.compileExpr(args[1]); 302 + this.code.push(arithOp[name]); 303 + return; 304 + } 305 + 306 + // Math builtins 307 + if (name === "sqrt") { 308 + this.compileExpr(args[0]); 309 + this.code.push(OP.F32_SQRT); 310 + return; 311 + } 312 + if (name === "abs") { 313 + this.compileExpr(args[0]); 314 + this.code.push(OP.F32_ABS); 315 + return; 316 + } 317 + if (name === "neg") { 318 + this.compileExpr(args[0]); 319 + this.code.push(OP.F32_NEG); 320 + return; 321 + } 322 + if (name === "floor") { 323 + this.compileExpr(args[0]); 324 + this.code.push(OP.F32_FLOOR); 325 + return; 326 + } 327 + 328 + // Drawing functions 329 + if (this.funcs[name] !== undefined) { 330 + for (const arg of args) this.compileExpr(arg); 331 + this.code.push(OP.CALL, ...uleb128(this.funcs[name])); 332 + return; 333 + } 334 + 335 + throw new Error(`Unknown function: ${name}`); 336 + } 337 + 338 + compile(source) { 339 + const tokens = tokenize(source); 340 + const ast = parse(tokens); 341 + 342 + for (const expr of ast) { 343 + if (expr.type === "list") { 344 + this.compileCall(expr); 345 + } else if (expr.type === "symbol" && COLORS[expr.value]) { 346 + // Bare color name on a line → wipe with that color 347 + const [r, g, b] = COLORS[expr.value]; 348 + this.code.push(OP.F32_CONST, ...f32Bytes(r)); 349 + this.code.push(OP.F32_CONST, ...f32Bytes(g)); 350 + this.code.push(OP.F32_CONST, ...f32Bytes(b)); 351 + this.code.push(OP.CALL, ...uleb128(this.funcs.wipe)); 352 + } 353 + } 354 + 355 + return this.emit(); 356 + } 357 + 358 + emit() { 359 + const bytes = []; 360 + 361 + // Magic + version 362 + bytes.push(0x00, 0x61, 0x73, 0x6d); // \0asm 363 + bytes.push(0x01, 0x00, 0x00, 0x00); // version 1 364 + 365 + // ── Type section (1) ── 366 + const typeEntries = this.types.map((t) => [ 367 + 0x60, 368 + ...uleb128(t.params.length), 369 + ...t.params, 370 + ...uleb128(t.results.length), 371 + ...t.results, 372 + ]); 373 + bytes.push(...section(1, vec(typeEntries))); 374 + 375 + // ── Import section (2) ── 376 + const importEntries = this.imports.map((imp) => [ 377 + ...encodeString(imp.module), 378 + ...encodeString(imp.name), 379 + 0x00, // func 380 + ...uleb128(imp.typeIdx), 381 + ]); 382 + bytes.push(...section(2, vec(importEntries))); 383 + 384 + // ── Function section (3) — declare paint ── 385 + bytes.push(...section(3, vec([[...uleb128(this.paintTypeIdx)]]))); 386 + 387 + // ── Export section (7) ── 388 + const paintIdx = this.importCount; // first non-import func 389 + const exportEntries = [ 390 + [...encodeString("paint"), 0x00, ...uleb128(paintIdx)], 391 + ]; 392 + bytes.push(...section(7, vec(exportEntries))); 393 + 394 + // ── Code section (10) ── 395 + const body = [ 396 + 0x00, // 0 local declarations 397 + ...this.code, 398 + OP.END, 399 + ]; 400 + const codeEntry = [...uleb128(body.length), ...body]; 401 + bytes.push(...section(10, vec([codeEntry]))); 402 + 403 + return new Uint8Array(bytes); 404 + } 405 + } 406 + 407 + // ─── CLI ──────────────────────────────────────────────────────────── 408 + 409 + import { readFileSync, writeFileSync } from "fs"; 410 + import { fileURLToPath } from "url"; 411 + 412 + if (process.argv[1] === fileURLToPath(import.meta.url)) { 413 + const input = process.argv[2]; 414 + if (!input) { 415 + console.error("Usage: node compiler.mjs <input.lisp> [output.wasm]"); 416 + process.exit(1); 417 + } 418 + const output = process.argv[3] || input.replace(/\.lisp$/, ".wasm"); 419 + const source = readFileSync(input, "utf-8"); 420 + const compiler = new Compiler(); 421 + const wasm = compiler.compile(source); 422 + writeFileSync(output, wasm); 423 + console.log(`Compiled ${input} → ${output} (${wasm.length} bytes)`); 424 + }
+12
kidlisp-wasm/hello.lisp
··· 1 + wipe 20 20 40 2 + ink 255 100 50 3 + circle w/2 h/2 40 4 + ink 50 200 100 5 + line 0 0 w h 6 + line w 0 0 h 7 + ink 255 255 255 8 + box 10 10 30 30 9 + ink red 10 + circle 100 30 15 11 + ink cyan 12 + box w/2 h/2 20 20
kidlisp-wasm/hello.ppm

This is a binary file and will not be displayed.

+185
kidlisp-wasm/run.mjs
··· 1 + #!/usr/bin/env node 2 + // KidLisp WASM Runner 3 + // Compiles a .lisp file, runs the WASM, outputs a PPM image. 4 + 5 + import { readFileSync, writeFileSync } from "fs"; 6 + import { Compiler } from "./compiler.mjs"; 7 + 8 + const WIDTH = 128; 9 + const HEIGHT = 128; 10 + 11 + // ─── Pixel Buffer ─────────────────────────────────────────────────── 12 + 13 + const fb = new Uint8Array(WIDTH * HEIGHT * 4); // RGBA 14 + 15 + function setPixel(x, y, r, g, b) { 16 + x = Math.round(x); 17 + y = Math.round(y); 18 + if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return; 19 + const i = (y * WIDTH + x) * 4; 20 + fb[i] = r; 21 + fb[i + 1] = g; 22 + fb[i + 2] = b; 23 + fb[i + 3] = 255; 24 + } 25 + 26 + // ─── Drawing State ────────────────────────────────────────────────── 27 + 28 + let inkR = 255, 29 + inkG = 255, 30 + inkB = 255; 31 + 32 + // ─── Host Functions ───────────────────────────────────────────────── 33 + 34 + function wipe(r, g, b) { 35 + r = Math.round(r); 36 + g = Math.round(g); 37 + b = Math.round(b); 38 + for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { 39 + fb[i] = r; 40 + fb[i + 1] = g; 41 + fb[i + 2] = b; 42 + fb[i + 3] = 255; 43 + } 44 + } 45 + 46 + function ink(r, g, b) { 47 + inkR = Math.round(r); 48 + inkG = Math.round(g); 49 + inkB = Math.round(b); 50 + } 51 + 52 + function plot(x, y) { 53 + setPixel(x, y, inkR, inkG, inkB); 54 + } 55 + 56 + function line(x0, y0, x1, y1) { 57 + x0 = Math.round(x0); 58 + y0 = Math.round(y0); 59 + x1 = Math.round(x1); 60 + y1 = Math.round(y1); 61 + const dx = Math.abs(x1 - x0); 62 + const dy = Math.abs(y1 - y0); 63 + const sx = x0 < x1 ? 1 : -1; 64 + const sy = y0 < y1 ? 1 : -1; 65 + let err = dx - dy; 66 + while (true) { 67 + setPixel(x0, y0, inkR, inkG, inkB); 68 + if (x0 === x1 && y0 === y1) break; 69 + const e2 = 2 * err; 70 + if (e2 > -dy) { 71 + err -= dy; 72 + x0 += sx; 73 + } 74 + if (e2 < dx) { 75 + err += dx; 76 + y0 += sy; 77 + } 78 + } 79 + } 80 + 81 + function box(x, y, w, h) { 82 + x = Math.round(x); 83 + y = Math.round(y); 84 + w = Math.round(w); 85 + h = Math.round(h); 86 + for (let py = y; py < y + h; py++) { 87 + for (let px = x; px < x + w; px++) { 88 + setPixel(px, py, inkR, inkG, inkB); 89 + } 90 + } 91 + } 92 + 93 + function circle(cx, cy, r) { 94 + cx = Math.round(cx); 95 + cy = Math.round(cy); 96 + r = Math.round(r); 97 + for (let y = -r; y <= r; y++) { 98 + for (let x = -r; x <= r; x++) { 99 + if (x * x + y * y <= r * r) { 100 + setPixel(cx + x, cy + y, inkR, inkG, inkB); 101 + } 102 + } 103 + } 104 + } 105 + 106 + function tri(x0, y0, x1, y1, x2, y2) { 107 + // Scanline triangle fill 108 + x0 = Math.round(x0); y0 = Math.round(y0); 109 + x1 = Math.round(x1); y1 = Math.round(y1); 110 + x2 = Math.round(x2); y2 = Math.round(y2); 111 + const minY = Math.max(0, Math.min(y0, y1, y2)); 112 + const maxY = Math.min(HEIGHT - 1, Math.max(y0, y1, y2)); 113 + for (let y = minY; y <= maxY; y++) { 114 + let minX = WIDTH, maxX = 0; 115 + const edges = [[x0,y0,x1,y1],[x1,y1,x2,y2],[x2,y2,x0,y0]]; 116 + for (const [ax,ay,bx,by] of edges) { 117 + if ((ay <= y && by > y) || (by <= y && ay > y)) { 118 + const t = (y - ay) / (by - ay); 119 + const x = Math.round(ax + t * (bx - ax)); 120 + if (x < minX) minX = x; 121 + if (x > maxX) maxX = x; 122 + } 123 + } 124 + for (let x = Math.max(0, minX); x <= Math.min(WIDTH - 1, maxX); x++) { 125 + setPixel(x, y, inkR, inkG, inkB); 126 + } 127 + } 128 + } 129 + 130 + // ─── Compile & Run ────────────────────────────────────────────────── 131 + 132 + const input = process.argv[2] || "hello.lisp"; 133 + const source = readFileSync( 134 + new URL(input, import.meta.url).pathname, 135 + "utf-8", 136 + ); 137 + 138 + console.log(`Compiling ${input}...`); 139 + const compiler = new Compiler(); 140 + const wasmBytes = compiler.compile(source); 141 + console.log(`WASM binary: ${wasmBytes.length} bytes`); 142 + 143 + const { instance } = await WebAssembly.instantiate(wasmBytes, { 144 + env: { wipe, ink, line, box, circle, plot, tri }, 145 + }); 146 + 147 + console.log("Running paint..."); 148 + instance.exports.paint(WIDTH, HEIGHT, 0); 149 + 150 + // ─── Output PPM ───────────────────────────────────────────────────── 151 + 152 + const ppm = Buffer.alloc(15 + WIDTH * HEIGHT * 3); // header + pixels 153 + const header = `P6\n${WIDTH} ${HEIGHT}\n255\n`; 154 + ppm.write(header); 155 + let offset = header.length; 156 + for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { 157 + ppm[offset++] = fb[i]; 158 + ppm[offset++] = fb[i + 1]; 159 + ppm[offset++] = fb[i + 2]; 160 + } 161 + 162 + const outFile = input.replace(/\.lisp$/, ".ppm"); 163 + writeFileSync(new URL(outFile, import.meta.url).pathname, ppm.slice(0, offset)); 164 + console.log(`Wrote ${outFile} (${WIDTH}x${HEIGHT})`); 165 + 166 + // ─── Terminal Preview (ANSI) ──────────────────────────────────────── 167 + 168 + const PREVIEW_W = Math.min(WIDTH, 64); 169 + const scaleX = WIDTH / PREVIEW_W; 170 + const scaleY = (HEIGHT / PREVIEW_W) * 2; // 2 rows per char with ▀ 171 + 172 + console.log(`\nPreview (${PREVIEW_W} cols):`); 173 + for (let row = 0; row < PREVIEW_W; row++) { 174 + let line = ""; 175 + for (let col = 0; col < PREVIEW_W; col++) { 176 + const tx = Math.floor(col * scaleX); 177 + const ty = Math.floor(row * scaleY); 178 + const by = Math.floor(row * scaleY + scaleY / 2); 179 + const ti = (ty * WIDTH + tx) * 4; 180 + const bi = (Math.min(by, HEIGHT - 1) * WIDTH + tx) * 4; 181 + line += `\x1b[38;2;${fb[ti]};${fb[ti + 1]};${fb[ti + 2]};48;2;${fb[bi]};${fb[bi + 1]};${fb[bi + 2]}m\u2580`; 182 + } 183 + line += "\x1b[0m"; 184 + process.stdout.write(line + "\n"); 185 + }
+113 -102
system/public/aesthetic.computer/disks/notepat.mjs
··· 520 520 const SECONDARY_BAR_TOP = TOP_BAR_BOTTOM; 521 521 const SECONDARY_BAR_HEIGHT = 12; 522 522 const SECONDARY_BAR_BOTTOM = SECONDARY_BAR_TOP + SECONDARY_BAR_HEIGHT; 523 + 524 + // OS bar — thin strip below the secondary bar for the "x86 os" button 525 + const OS_BAR_TOP = SECONDARY_BAR_BOTTOM; 526 + const OS_BAR_HEIGHT = 12; 527 + const OS_BAR_BOTTOM = OS_BAR_TOP + OS_BAR_HEIGHT; 528 + 523 529 const TOGGLE_BTN_PADDING_X = 2; 524 530 const TOGGLE_BTN_PADDING_Y = 2; 525 531 const TOGGLE_BTN_GAP = 3; // At least 1px visible gap between buttons ··· 1082 1088 1083 1089 // let qrcells; 1084 1090 1085 - let waveBtn, octBtn, osBtn; 1091 + let waveBtn, octBtn, osBtn; 1086 1092 let slideBtn, roomBtn, glitchBtn, quickBtn; // Toggle buttons for slide/room/glitch/quick modes 1087 1093 let metroBtn, bpmMinusBtn, bpmPlusBtn; // Metronome controls 1088 1094 let melodyAliasBtn; ··· 1503 1509 } 1504 1510 } 1505 1511 1506 - buildWaveButton(api); 1507 - buildOctButton(api); 1508 - buildOsButton(api); 1509 - buildToggleButtons(api); 1510 - buildMetronomeButtons(api); 1512 + buildWaveButton(api); 1513 + buildOctButton(api); 1514 + buildOsButton(api); 1515 + buildToggleButtons(api); 1516 + buildMetronomeButtons(api); 1511 1517 1512 1518 const newOctave = 1513 1519 parseInt(colon[0]) || parseInt(colon[1]) || parseInt(colon[2]); ··· 1933 1939 (qKeyWidth + qKeySpacing); 1934 1940 const qwertyHeight = QWERTY_MINIMAP_HEIGHT; 1935 1941 1936 - let pianoY = SECONDARY_BAR_BOTTOM; 1942 + let pianoY = OS_BAR_BOTTOM; 1937 1943 let pianoStartX = 58; 1938 1944 const centerX = layout?.centerX ?? (effectiveWidth - pianoWidth) / 2; 1939 1945 const centerWidth = layout?.centerAreaWidth ?? pianoWidth; ··· 1950 1956 const rotatedPianoWidth = whiteKeyHeight + 2; // Keys drawn horizontally but stacked vertically 1951 1957 const rotatedPianoHeight = MINI_PIANO_WHITE_KEYS.length * whiteKeyWidth; // Full piano height when rotated 1952 1958 pianoStartX = gridLeft + gridWidth + 4; // Gap from grid 1953 - pianoY = layout?.topButtonY || SECONDARY_BAR_BOTTOM; 1959 + pianoY = layout?.topButtonY || OS_BAR_BOTTOM; 1954 1960 1955 1961 // Check if rotated piano fits horizontally (x + width within screen) 1956 1962 const pianoRight = pianoStartX + rotatedPianoWidth; ··· 1994 2000 const gridWidth = (layout?.buttonsPerRow || 4) * (layout?.buttonWidth || 20) + (layout?.margin || 2) * 2; 1995 2001 const gridLeft = layout?.margin || 2; 1996 2002 pianoStartX = gridLeft + gridWidth + 8; // 8px gap from grid 1997 - pianoY = layout?.topButtonY || SECONDARY_BAR_BOTTOM; 2003 + pianoY = layout?.topButtonY || OS_BAR_BOTTOM; 1998 2004 1999 2005 // Check if piano fits horizontally 2000 2006 const pianoRight = pianoStartX + pianoWidth; ··· 2032 2038 } 2033 2039 2034 2040 if (isCompact) { 2035 - pianoY = SECONDARY_BAR_BOTTOM + 2; 2041 + pianoY = OS_BAR_BOTTOM + 2; 2036 2042 // Check if piano fits in center area - if not, skip it (return hidden flag) 2037 2043 if (centerWidth < pianoWidth + 4) { 2038 2044 // Piano doesn't fit - hide it but still compute QWERTY position for center ··· 2059 2065 const maxX = Math.max(minX, centerRight - pianoWidth); 2060 2066 pianoStartX = clamp(idealX, minX, maxX); 2061 2067 } else if (song) { 2062 - const effectiveTrackY = trackY ?? SECONDARY_BAR_BOTTOM; 2068 + const effectiveTrackY = trackY ?? OS_BAR_BOTTOM; 2063 2069 pianoY = effectiveTrackY + (trackHeight || 0) + 2; 2064 2070 pianoStartX = effectiveWidth - pianoWidth - 2; 2065 2071 } else { ··· 2313 2319 const notesPerSide = 12; 2314 2320 const buttonsPerRow = 4; // 4 notes per row on each side 2315 2321 const totalRows = Math.ceil(notesPerSide / buttonsPerRow); // 3 rows 2316 - const hudReserved = SECONDARY_BAR_BOTTOM; 2322 + const hudReserved = OS_BAR_BOTTOM; 2317 2323 2318 2324 // Piano dimensions (extended mini layout) 2319 2325 const whiteKeyWidth = getMiniPianoWhiteKeyWidth(true); ··· 2453 2459 }; 2454 2460 } 2455 2461 2456 - const hudReserved = SECONDARY_BAR_BOTTOM; 2462 + const hudReserved = OS_BAR_BOTTOM; 2457 2463 const trackHeight = songMode ? TRACK_HEIGHT : 0; 2458 2464 const trackSpacing = songMode ? TRACK_GAP : 0; 2459 2465 const baseReservedTop = hudReserved + trackHeight + trackSpacing; ··· 2474 2480 // Rotated piano: width = MINI_KEYBOARD_HEIGHT, height = pianoWidth (all keys stacked) 2475 2481 const rotatedPianoWidth = MINI_KEYBOARD_HEIGHT + 4; // Piano keys become vertical 2476 2482 const rotatedPianoHeight = pianoWidth; // Height needed to fit ALL white keys 2477 - const availableHeightForRotated = screen.height - SECONDARY_BAR_BOTTOM - 4; // Leave some margin 2483 + const availableHeightForRotated = screen.height - OS_BAR_BOTTOM - 4; // Leave some margin 2478 2484 // Only show rotated piano if ALL keys fit vertically 2479 2485 const narrowVerticalSpace = !horizontalSpaceForMini && 2480 2486 usableWidth - gridWidthEstimate - rotatedPianoWidth > 4 && ··· 2911 2917 // 🎨 KidLisp visualization — bounded to available space above/between buttons 2912 2918 if (kidlispBgEnabled && kidlispBackground && !paintPictureOverlay) { 2913 2919 wipe(bg); // Base background first 2914 - const klY = SECONDARY_BAR_BOTTOM; 2920 + const klY = OS_BAR_BOTTOM; 2915 2921 const klBottom = earlyLayout.topButtonY; 2916 2922 const klH = klBottom - klY; 2917 2923 if (klH > 10) { ··· 3189 3195 // 🎚️ Room parameter bar - appears when room mode is enabled 3190 3196 if (roomMode && roomBtn?.box) { 3191 3197 const barHeight = 6; 3192 - const barY = SECONDARY_BAR_BOTTOM + 2; 3198 + const barY = OS_BAR_BOTTOM + 2; 3193 3199 const barX = roomBtn.box.x; 3194 3200 const barWidth = Math.max(40, roomBtn.box.w * 2); 3195 3201 const fillWidth = Math.floor(barWidth * roomAmount); ··· 3496 3502 // In single-column mode (non-split), hide horizontal track entirely - only show vertical track if available 3497 3503 const showHorizontalTrack = showTrack && !useVerticalTrack && layout.splitLayout; 3498 3504 const trackHeight = showHorizontalTrack ? TRACK_HEIGHT : 0; 3499 - const trackY = showHorizontalTrack ? SECONDARY_BAR_BOTTOM : null; 3505 + const trackY = showHorizontalTrack ? OS_BAR_BOTTOM : null; 3500 3506 3501 3507 3502 3508 if (showHorizontalTrack) { ··· 4417 4423 ink("yellow"); 4418 4424 write("tap", { right: 6, top: 6 }); 4419 4425 } else if (!paintPictureOverlay) { 4420 - osBtn?.paint((btn) => { 4421 - ink(btn.down ? [20, 70, 70] : [10, 45, 45]).box( 4422 - btn.box.x, 4423 - btn.box.y + 3, 4424 - btn.box.w, 4425 - btn.box.h - 3, 4426 - ); 4427 - if (btn.over && !btn.down) { 4428 - ink(255, 255, 255, 24).box( 4429 - btn.box.x, 4430 - btn.box.y + 3, 4431 - btn.box.w, 4432 - btn.box.h - 3, 4433 - ); 4434 - ink(100, 255, 255, 140).box( 4435 - btn.box.x, 4436 - btn.box.y + 3, 4437 - btn.box.w, 4438 - btn.box.h - 3, 4439 - "outline", 4440 - ); 4441 - } 4442 - ink(70, 160, 160).line( 4443 - btn.box.x + btn.box.w, 4444 - btn.box.y + 3, 4445 - btn.box.x + btn.box.w, 4446 - btn.box.y + btn.box.h - 1, 4447 - ); 4448 - ink(btn.down ? [220, 255, 255] : [120, 255, 255]).write( 4449 - "os", 4450 - { x: btn.box.x + 3, y: btn.box.y + 5 }, 4451 - undefined, undefined, false, "MatrixChunky8" 4452 - ); 4453 - }); 4454 - 4455 - waveBtn?.paint((btn) => { 4426 + // OS bar background 4427 + ink(8, 30, 30).box(0, OS_BAR_TOP, screen.width, OS_BAR_HEIGHT); 4428 + ink(5, 20, 20).line(0, OS_BAR_TOP, screen.width, OS_BAR_TOP); 4429 + 4430 + osBtn?.paint((btn) => { 4431 + ink(btn.down ? [20, 70, 70] : [12, 40, 40]).box( 4432 + btn.box.x, 4433 + btn.box.y, 4434 + btn.box.w, 4435 + btn.box.h, 4436 + ); 4437 + if (btn.over && !btn.down) { 4438 + ink(255, 255, 255, 24).box( 4439 + btn.box.x, 4440 + btn.box.y, 4441 + btn.box.w, 4442 + btn.box.h, 4443 + ); 4444 + ink(100, 255, 255, 140).box( 4445 + btn.box.x, 4446 + btn.box.y, 4447 + btn.box.w, 4448 + btn.box.h, 4449 + "outline", 4450 + ); 4451 + } 4452 + ink(70, 160, 160).line( 4453 + btn.box.x + btn.box.w, 4454 + btn.box.y + 1, 4455 + btn.box.x + btn.box.w, 4456 + btn.box.y + btn.box.h - 1, 4457 + ); 4458 + ink(btn.down ? [220, 255, 255] : [120, 255, 255]).write( 4459 + btn.label || "x86 os", 4460 + { x: btn.box.x + 3, y: btn.box.y + 3 }, 4461 + undefined, undefined, false, "MatrixChunky8" 4462 + ); 4463 + }); 4464 + 4465 + waveBtn?.paint((btn) => { 4456 4466 ink(btn.down ? [40, 40, 100] : "darkblue").box( 4457 4467 btn.box.x, 4458 4468 btn.box.y + 3, ··· 4963 4973 // Use layout metrics to find a safe spot 4964 4974 const padTop = layout?.topButtonY || (screen.height - 120); 4965 4975 const osdX = screen.width - osdWidth - 4; 4966 - const osdY = Math.max(SECONDARY_BAR_BOTTOM + 2, padTop - osdHeight - 4); 4976 + const osdY = Math.max(OS_BAR_BOTTOM + 2, padTop - osdHeight - 4); 4967 4977 4968 4978 // Semi-transparent background 4969 4979 ink(0, 0, 0, 210).box(osdX - 2, osdY - 2, osdWidth, osdHeight); ··· 5643 5653 } 5644 5654 } 5645 5655 if (e.is("reframed")) { 5646 - setupButtons(api); 5647 - buildWaveButton(api); 5648 - buildOctButton(api); 5649 - buildOsButton(api); 5650 - buildToggleButtons(api); 5651 - buildMetronomeButtons(api); 5656 + setupButtons(api); 5657 + buildWaveButton(api); 5658 + buildOctButton(api); 5659 + buildOsButton(api); 5660 + buildToggleButtons(api); 5661 + buildMetronomeButtons(api); 5652 5662 // Resize picture to quarter resolution (half width, half height) 5653 5663 const resizedPictureWidth = Math.max(1, Math.floor(screen.width / 2)); 5654 5664 const resizedPictureHeight = Math.max(1, Math.floor(screen.height / 2)); ··· 5747 5757 const topPianoWidth = Math.min(140, Math.floor((screen.width - topBarBase) * 0.5)); 5748 5758 const topPianoEndX = topBarBase + topPianoWidth; 5749 5759 const vizLeft = topPianoEndX; // Start after piano 5750 - const vizRight = (osBtn?.box?.x ?? waveBtn?.box?.x ?? screen.width) - 1; 5751 - if (e.x >= vizLeft && e.x <= vizRight) { 5752 - recitalMode = true; 5753 - recitalBlinkPhase = 0; 5760 + const vizRight = (waveBtn?.box?.x ?? screen.width) - 1; 5761 + if (e.x >= vizLeft && e.x <= vizRight) { 5762 + recitalMode = true; 5763 + recitalBlinkPhase = 0; 5754 5764 } 5755 5765 } 5756 5766 ··· 5771 5781 if (layout.miniInputsEnabled && !recitalMode) { 5772 5782 5773 5783 const trackHeight = showTrack ? TRACK_HEIGHT : 0; 5774 - const trackY = showTrack ? SECONDARY_BAR_BOTTOM : null; 5784 + const trackY = showTrack ? OS_BAR_BOTTOM : null; 5775 5785 const pianoGeometry = getMiniPianoGeometry({ 5776 5786 screen, 5777 5787 layout, ··· 6653 6663 }, 6654 6664 }); 6655 6665 6656 - waveBtn?.act(e, { 6657 - down: () => api.beep(400), 6658 - push: (btn) => { 6659 - api.beep(); 6660 - waveIndex = (waveIndex + 1) % wavetypes.length; 6661 - wave = wavetypes[waveIndex]; 6662 - buildWaveButton(api); 6663 - }, 6664 - }); 6665 - 6666 - osBtn?.act(e, { 6667 - down: () => api.beep(400), 6668 - push: () => { 6669 - api.beep(); 6670 - jump("os"); 6671 - }, 6672 - }); 6666 + waveBtn?.act(e, { 6667 + down: () => api.beep(400), 6668 + push: (btn) => { 6669 + api.beep(); 6670 + waveIndex = (waveIndex + 1) % wavetypes.length; 6671 + wave = wavetypes[waveIndex]; 6672 + buildWaveButton(api); 6673 + }, 6674 + }); 6675 + 6676 + osBtn?.act(e, { 6677 + down: () => api.beep(400), 6678 + push: () => { 6679 + api.beep(); 6680 + jump("os"); 6681 + }, 6682 + }); 6673 6683 6674 6684 // 🎛️ Toggle button interactions 6675 6685 slideBtn?.act(e, { ··· 6704 6714 6705 6715 // 🎚️ Room parameter bar interaction (drag to adjust room amount) 6706 6716 if (roomMode && roomBtn?.box && (e.is("touch") || e.is("draw"))) { 6707 - const barY = SECONDARY_BAR_BOTTOM + 2; 6717 + const barY = OS_BAR_BOTTOM + 2; 6708 6718 const barHeight = 6; 6709 6719 const barX = roomBtn.box.x; 6710 6720 const barWidth = Math.max(40, roomBtn.box.w * 2); ··· 7787 7797 waveBtn.displayWave = displayWave; 7788 7798 } 7789 7799 7790 - function buildOctButton({ screen, ui, typeface }) { 7800 + function buildOctButton({ screen, ui, typeface }) { 7791 7801 const isNarrow = screen.width < 200; 7792 7802 const glyphWidth = typeface?.glyphs?.["0"]?.resolution?.[0] ?? matrixFont?.glyphs?.["0"]?.resolution?.[0] ?? 6; 7793 7803 const octWidth = octave.length * glyphWidth; ··· 7798 7808 octWidth + margin * 2 + 7, 7799 7809 10 + margin * 2 - 1 + 2, 7800 7810 ); 7801 - octBtn.id = "oct-button"; 7802 - octBtn.isNarrow = isNarrow; 7803 - } 7804 - 7805 - function buildOsButton({ ui }) { 7806 - const margin = 4; 7807 - const labelWidth = 2 * 6; 7808 - const buttonWidth = labelWidth + margin * 2; 7809 - const buttonHeight = 10 + margin * 2 - 1 + 2; 7810 - const waveX = waveBtn?.box?.x ?? 9999; 7811 - osBtn = new ui.Button( 7812 - waveX - buttonWidth - 3, 7813 - 0, 7814 - buttonWidth, 7815 - buttonHeight, 7816 - ); 7817 - osBtn.id = "os-button"; 7818 - } 7811 + octBtn.id = "oct-button"; 7812 + octBtn.isNarrow = isNarrow; 7813 + } 7814 + 7815 + function buildOsButton({ ui, screen }) { 7816 + const label = "x86 os"; 7817 + const margin = 3; 7818 + const labelWidth = label.length * 6; 7819 + const buttonWidth = labelWidth + margin * 2; 7820 + const buttonHeight = OS_BAR_HEIGHT; 7821 + osBtn = new ui.Button( 7822 + 0, 7823 + OS_BAR_TOP, 7824 + buttonWidth, 7825 + buttonHeight, 7826 + ); 7827 + osBtn.id = "os-button"; 7828 + osBtn.label = label; 7829 + } 7819 7830 7820 7831 // Build metronome controls and toggle buttons with responsive layout 7821 7832 // Calculates available space and shortens labels as needed to prevent overlap