Monorepo for Aesthetic.Computer aesthetic.computer

blank: turntable swivel + write3D API + light/dark theme + UI bottom fix

- Add write3D to paint API (disk.mjs) for projecting text glyphs onto 3D planes
- Fix TextButton center:"x" + bottom positioning (ui.mjs early return bug)
- Rework blank.mjs: turntable laptop animation, scrolling description text,
light/dark theme support via $.dark, remove RETRY button state
- ac-electron: minor updates

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

+304 -143
+19
ac-electron/main.js
··· 2506 2506 } 2507 2507 return; 2508 2508 } 2509 + // Handle 'local' / 'prod' commands - switch between dev and production servers 2510 + try { 2511 + const navUrl = new URL(url); 2512 + const pathname = navUrl.pathname.replace(/^\//, ''); 2513 + if (pathname === 'local' || pathname === 'prod') { 2514 + navEvent.preventDefault(); 2515 + const base = pathname === 'local' 2516 + ? 'http://localhost:8888' 2517 + : 'https://aesthetic.computer'; 2518 + const hostWin = BrowserWindow.getAllWindows().find(win => 2519 + !win.isDestroyed() && win.webContents.id === contents.hostWebContents?.id 2520 + ); 2521 + if (hostWin) { 2522 + console.log(`[main] Switching to ${pathname} server: ${base}`); 2523 + hostWin.webContents.send('navigate', `${base}/prompt?desktop`); 2524 + } 2525 + return; 2526 + } 2527 + } catch (e) {} 2509 2528 if (url.startsWith('ac://open')) { 2510 2529 navEvent.preventDefault(); 2511 2530 let targetUrl = '';
+9
ac-electron/renderer/flip-view.html
··· 662 662 if (e.isMainFrame) { 663 663 const piece = extractPieceName(e.url); 664 664 console.log('[flip] In-page navigation to piece:', piece); 665 + // Intercept 'local' and 'prod' to switch servers 666 + if (piece === 'local' || piece === 'prod') { 667 + const base = piece === 'local' 668 + ? 'http://localhost:8888' 669 + : 'https://aesthetic.computer'; 670 + console.log(`[flip] Switching to ${piece} server: ${base}`); 671 + webviewEl.src = `${base}/prompt?desktop`; 672 + return; 673 + } 665 674 ipcRenderer.invoke('set-current-piece', piece); 666 675 syncMasterVolume(); 667 676 }
+205 -122
system/public/aesthetic.computer/disks/blank.mjs
··· 1 1 // blank, 26.03.20 2 2 // AC Blank — AC Native Laptop product page & checkout 3 3 4 - const { floor, sin, cos, abs, min, max, PI } = Math; 4 + const { floor, sin, cos, abs, min, max, PI, sqrt } = Math; 5 5 6 6 // Pricing (cents) 7 7 const pricing = { ··· 31 31 32 32 // Animation 33 33 let frame = 0; 34 + let scrollY = 0; 34 35 35 36 const charH = 16; 36 37 38 + // Scrolling text lines 39 + const scrollLines = [ 40 + "AC Native Laptop", 41 + "", 42 + "A surplus laptop running AC Native OS.", 43 + "Stable commands. Nothing extra.", 44 + "Like a blank tape waiting to be filled.", 45 + "", 46 + "Lenovo ThinkPad Yoga 11e", 47 + "11.6\" touchscreen · flip design", 48 + "", 49 + ]; 50 + 37 51 function displayAmount(amt, cur) { 38 52 if (cur === "dkk") return `${(amt / 100).toFixed(0)} kr`; 39 53 return `$${(amt / 100).toFixed(0)}`; 40 54 } 41 55 42 - function tierLabel(amt) { 43 - if (amt >= 51200) return "tutorial"; 44 - if (amt > 9600) return "support"; 45 - return "blank"; 46 - } 47 - 48 56 function getBuyText() { 49 57 if (buyPending) return "CHECKING OUT..."; 50 - if (checkoutError) return "RETRY"; 51 58 return `BUY ${displayAmount(amount, currency)}`; 52 59 } 53 60 54 - async function boot({ params, ui, screen, cursor, hud, api }) { 61 + async function boot({ params, ui, screen, cursor, hud, api, dark }) { 55 62 cursor("native"); 56 63 hud.labelBack(); 57 64 58 - // Check for thanks state 59 65 if (params[0] === "thanks") { 60 66 thanks = true; 61 67 return; ··· 72 78 } 73 79 74 80 function setupButtons(ui, screen) { 75 - const h = screen.height; 76 - 77 - // Buy button (bottom center) 78 - buyBtn = new ui.TextButton(getBuyText(), { center: "x", bottom: 12, screen }); 81 + buyBtn = new ui.TextButton(getBuyText(), { center: "x", bottom: 20, screen }); 79 82 80 - // Tier buttons (stacked, above buy button) 81 - const tierStartY = buyBtn.btn.box.y - (buyBtn.height + 6) * 3 - 8; 83 + const gap = buyBtn.height + 6; 84 + const tierStartY = buyBtn.btn.box.y - gap * tiers.length - 8; 82 85 tierBtns = tiers.map((t, i) => { 83 86 return new ui.TextButton(tierText(i), { 84 87 center: "x", 85 - y: tierStartY + i * (buyBtn.height + 6), 88 + y: tierStartY + i * gap, 86 89 screen, 87 90 }); 88 91 }); 89 92 90 - // Currency toggle (top right) 91 93 currencyBtn = new ui.TextButton(currency.toUpperCase(), { 92 94 top: 8, 93 95 right: 8, ··· 128 130 } 129 131 } 130 132 131 - function paint({ wipe, ink, write, box, line, screen }) { 133 + function paint($) { 134 + const { wipe, ink, write, write3D, box, line, screen, dark: isDark } = $; 132 135 frame += 1; 133 136 const w = screen.width; 134 137 const h = screen.height; 135 138 136 - // Dark background 137 - wipe(12, 12, 14); 139 + // Theme colors 140 + const bg = isDark ? [12, 12, 14] : [245, 243, 240]; 141 + const fg = isDark ? 255 : 20; 142 + const fgDim = isDark ? 120 : 100; 143 + const fgFaint = isDark ? 60 : 160; 144 + const btnFill = isDark ? [22, 22, 25] : [230, 230, 232]; 145 + const btnOutline = isDark ? [60, 60, 65] : [180, 180, 185]; 146 + const btnText = isDark ? [140, 140, 145] : [60, 60, 65]; 147 + const selFill = isDark ? [40, 50, 40] : [220, 235, 220]; 148 + const selOutline = isDark ? [120, 200, 120] : [60, 150, 60]; 149 + const selText = isDark ? [220, 255, 220] : [30, 80, 30]; 150 + const hoverFill = isDark ? [35, 35, 40] : [240, 240, 245]; 151 + const hoverOutline = isDark ? [150, 150, 180] : [120, 120, 150]; 152 + const hoverText = isDark ? [200, 200, 220] : [40, 40, 60]; 153 + const wireAlpha = isDark ? 140 : 120; 154 + 155 + wipe(...bg); 156 + 157 + // Debug: test write3D with a flat plane (identity projection) 158 + ink(255, 0, 0).write3D("TEST", { 159 + origin: [20, 20, 0], 160 + right: [1, 0, 0], 161 + down: [0, 1, 0], 162 + project: ([x, y, z]) => [x, y], 163 + }); 138 164 139 165 // Thanks page 140 166 if (thanks) { 141 167 const cy = floor(h / 2); 142 - ink(255).write("your blank is coming.", { 143 - center: "x", 144 - y: cy - 30, 145 - screen, 146 - }); 147 - ink(140).write("we'll be in touch.", { center: "x", y: cy, screen }); 168 + ink(fg).write("your blank is coming.", { center: "x", y: cy - 30, screen }); 169 + ink(fgDim).write("we'll be in touch.", { center: "x", y: cy, screen }); 148 170 return; 149 171 } 150 172 151 - // Title 152 - const titleY = floor(h * 0.08); 153 - ink(255).write("AC Blank", { 154 - center: "x", 155 - y: titleY, 156 - screen, 157 - }); 173 + // Compute button zone height 174 + const btnH = buyBtn ? buyBtn.height + 6 : 26; 175 + const uiZoneH = btnH * (tiers.length + 1) + 20; 176 + const contentBottom = h - uiZoneH - 20; 158 177 159 - // Subtitle 160 - ink(120).write("AC Native Laptop", { 161 - center: "x", 162 - y: titleY + charH + 4, 163 - screen, 164 - }); 165 - 166 - // Description block 167 - const descY = titleY + charH * 2 + 16; 168 - const descLines = [ 169 - "A surplus laptop running AC Native OS.", 170 - "Stable commands. Nothing extra.", 171 - "Like a blank tape waiting to be filled.", 172 - ]; 173 - descLines.forEach((ln, i) => { 174 - ink(90).write(ln, { center: "x", y: descY + i * (charH + 2), screen }); 175 - }); 176 - 177 - // Specs 178 - const specY = descY + descLines.length * (charH + 2) + 12; 179 - ink(70).write("Lenovo ThinkPad Yoga 11e", { 180 - center: "x", 181 - y: specY, 182 - screen, 183 - }); 184 - ink(50).write("11.6\" touchscreen · flip design", { 185 - center: "x", 186 - y: specY + charH + 2, 187 - screen, 188 - }); 189 - 190 - // 💻 Wireframe laptop (two hinged halves) 178 + // 💻 Wireframe laptop (turntable swivel) 191 179 { 192 180 const cx = floor(w / 2); 193 - const laptopTop = specY + charH * 2 + 16; 194 - const laptopBottom = tierBtns.length > 0 ? tierBtns[0].btn.box.y - 12 : h * 0.55; 195 - const cy = floor((laptopTop + laptopBottom) / 2); 196 - const size = min(w, laptopBottom - laptopTop) * 0.28; 181 + const laptopTop = 20; 182 + const cy = floor((laptopTop + contentBottom) / 2); 183 + const size = min(w * 0.45, (contentBottom - laptopTop) * 0.35); 197 184 const fov = 260; 185 + 186 + // Turntable rotation (slow steady swivel) 198 187 const ay = frame * 0.008; 199 - const ax = frame * 0.005; 188 + const ax = 0.3; // fixed downward tilt 200 189 201 - // Hinge angle: smoothly open and close 202 - const hingeAngle = (sin(frame * 0.012) * 0.5 + 0.5) * PI * 0.85 + PI * 0.1; 190 + // Fixed open angle (~120 degrees) 191 + const hingeAngle = PI * 0.67; 203 192 204 - // Half-box dimensions: wide, thin, moderate depth 193 + // Half-box dimensions 205 194 const hw = 1.4, hh = 0.08, hd = 0.9; 206 195 207 - // Bottom half (base) 196 + // Base (keyboard half) 208 197 const base = [ 209 - [-hw, -hh, -hd], [ hw, -hh, -hd], [ hw, hh, -hd], [-hw, hh, -hd], 210 - [-hw, -hh, hd], [ hw, -hh, hd], [ hw, hh, hd], [-hw, hh, hd], 198 + [-hw, -hh, -hd], [hw, -hh, -hd], [hw, hh, -hd], [-hw, hh, -hd], 199 + [-hw, -hh, hd], [hw, -hh, hd], [hw, hh, hd], [-hw, hh, hd], 211 200 ]; 212 201 213 - // Top half (lid) — hinged at back edge 202 + // Lid — hinged at back edge 214 203 const lidLocal = [ 215 - [-hw, -hh, 0], [ hw, -hh, 0], [ hw, hh, 0], [-hw, hh, 0], 216 - [-hw, -hh, 2 * hd], [ hw, -hh, 2 * hd], [ hw, hh, 2 * hd], [-hw, hh, 2 * hd], 204 + [-hw, -hh, 0], [hw, -hh, 0], [hw, hh, 0], [-hw, hh, 0], 205 + [-hw, -hh, 2 * hd], [hw, -hh, 2 * hd], [hw, hh, 2 * hd], [-hw, hh, 2 * hd], 217 206 ]; 218 207 const cosH = cos(hingeAngle), sinH = sin(hingeAngle); 219 208 const lid = lidLocal.map(([lx, ly, lz]) => { ··· 223 212 }); 224 213 225 214 const halfEdges = [ 226 - [0,1],[1,2],[2,3],[3,0], 227 - [4,5],[5,6],[6,7],[7,4], 228 - [0,4],[1,5],[2,6],[3,7], 215 + [0, 1], [1, 2], [2, 3], [3, 0], 216 + [4, 5], [5, 6], [6, 7], [7, 4], 217 + [0, 4], [1, 5], [2, 6], [3, 7], 229 218 ]; 230 219 231 220 const project = ([x, y, z]) => { ··· 260 249 floor(r * 255 * brightness), 261 250 floor(g * 255 * brightness), 262 251 floor(bl * 255 * brightness), 263 - 140, 264 - ).line( 265 - proj[a][0], proj[a][1], 266 - proj[b][0], proj[b][1], 267 - ); 252 + wireAlpha, 253 + ).line(proj[a][0], proj[a][1], proj[b][0], proj[b][1]); 268 254 }); 269 255 }; 270 256 ··· 281 267 const flicker = 0.6 + sin(frame * 0.1 + i * 1.3) * 0.4; 282 268 ink(...pColor, floor(flicker * 150)).box(px - 1, py - 1, 2, 2); 283 269 }); 270 + 271 + // 🖥️ "AC Blank" projected in 3D on the lid screen 272 + // Screen face: inner face of lid (y=-hh before hinge) 273 + const inset = 0.15; 274 + const screenTL = [-hw + inset, -hh - 0.002, inset]; 275 + const screenTR = [hw - inset, -hh - 0.002, inset]; 276 + const screenBL = [-hw + inset, -hh - 0.002, 2 * hd - inset]; 277 + 278 + // Transform screen corners through hinge rotation 279 + const hingeXform = ([lx, ly, lz]) => { 280 + const ry = ly * cosH - lz * sinH; 281 + const rz = ly * sinH + lz * cosH; 282 + return [lx, ry + hh, rz - hd]; 283 + }; 284 + const sTL = hingeXform(screenTL); 285 + const sTR = hingeXform(screenTR); 286 + const sBL = hingeXform(screenBL); 287 + 288 + // Compute plane vectors (right = TL→TR, down = TL→BL) 289 + const planeRight = [sTR[0] - sTL[0], sTR[1] - sTL[1], sTR[2] - sTL[2]]; 290 + const planeDown = [sBL[0] - sTL[0], sBL[1] - sTL[1], sBL[2] - sTL[2]]; 291 + 292 + // Screen-space normal check (is it facing the camera?) 293 + const projTL = project(sTL); 294 + const projTR = project(sTR); 295 + const projBL = project(sBL); 296 + const ex1 = projTR[0] - projTL[0], ey1 = projTR[1] - projTL[1]; 297 + const ex2 = projBL[0] - projTL[0], ey2 = projBL[1] - projTL[1]; 298 + const cross = ex1 * ey2 - ey1 * ex2; 299 + 300 + if (cross > 0) { 301 + const maxCross = size * size * 0.5; 302 + const facing = min(1, abs(cross) / maxCross); 303 + const textAlpha = floor(facing * 255); 304 + 305 + // Plane width in world units 306 + const planeW = sqrt(planeRight[0] ** 2 + planeRight[1] ** 2 + planeRight[2] ** 2); 307 + const planeH = sqrt(planeDown[0] ** 2 + planeDown[1] ** 2 + planeDown[2] ** 2); 308 + 309 + // Normalize plane vectors per glyph-pixel unit 310 + // A glyph pixel at scale 1 = 1/planeW of the right vector (scaled to fit ~20 chars) 311 + const glyphScale = planeW / (6 * 14); // fit ~14 chars across screen width 312 + const rn = [planeRight[0] / planeW * glyphScale, 313 + planeRight[1] / planeW * glyphScale, 314 + planeRight[2] / planeW * glyphScale]; 315 + const dn = [planeDown[0] / planeH * glyphScale, 316 + planeDown[1] / planeH * glyphScale, 317 + planeDown[2] / planeH * glyphScale]; 318 + 319 + // Center "AC Blank" (8 chars × 6px = 48px wide, 10px tall) 320 + const textW = 8 * 6; // glyph pixels 321 + const textH = 10; 322 + const offsetR = (planeW / glyphScale - textW) / 2; 323 + const offsetD = (planeH / glyphScale - textH) / 2; 324 + 325 + // Origin = screen TL + centering offset 326 + const textOrigin = [ 327 + sTL[0] + offsetR * rn[0] + offsetD * dn[0], 328 + sTL[1] + offsetR * rn[1] + offsetD * dn[1], 329 + sTL[2] + offsetR * rn[2] + offsetD * dn[2], 330 + ]; 331 + 332 + const titleColor = isDark ? [255, 255, 255] : [20, 20, 20]; 333 + ink(titleColor[0], titleColor[1], titleColor[2], textAlpha) 334 + .write3D("AC Blank", { 335 + origin: textOrigin, 336 + right: rn, 337 + down: dn, 338 + project, 339 + }); 340 + } 341 + } 342 + 343 + // Scrolling text (description, between laptop and buttons) 344 + { 345 + scrollY += 0.3; 346 + const scrollAreaTop = contentBottom - 10; 347 + const lineH = charH + 4; 348 + const totalScrollH = scrollLines.length * lineH; 349 + const scrollOffset = scrollY % totalScrollH; 350 + 351 + for (let i = 0; i < scrollLines.length; i++) { 352 + const ln = scrollLines[i]; 353 + if (!ln) continue; 354 + const rawY = scrollAreaTop - scrollOffset + i * lineH; 355 + // Wrap around 356 + const y = rawY < scrollAreaTop - totalScrollH 357 + ? rawY + totalScrollH 358 + : rawY; 359 + if (y < scrollAreaTop - lineH || y > scrollAreaTop + lineH * 2) continue; 360 + // Fade out as it scrolls up 361 + const dist = scrollAreaTop - y; 362 + const alpha = max(0, min(255, 255 - floor(dist * 3))); 363 + const color = i === 0 ? fgDim : fgFaint; 364 + ink(color, color, color, alpha).write(ln, { center: "x", y: floor(y), screen }); 365 + } 284 366 } 285 367 286 368 // Tier buttons 287 - const $ = { ink }; 369 + const $btn = { ink }; 288 370 tierBtns.forEach((btn, i) => { 289 371 const t = tiers[i]; 290 372 const tierAmt = currency === "dkk" ? t.dkk : t.amount; 291 373 const isSelected = amount === tierAmt; 292 - 293 - const selectedScheme = [[40, 50, 40], [120, 200, 120], [220, 255, 220]]; 294 - const defaultScheme = [[22, 22, 25], [60, 60, 65], [140, 140, 145]]; 295 - const hoverScheme = [[35, 35, 40], [150, 150, 180], [200, 200, 220]]; 296 - 297 - btn.paint($, isSelected ? selectedScheme : defaultScheme, hoverScheme); 374 + btn.paint( 375 + $btn, 376 + isSelected ? [selFill, selOutline, selText] : [btnFill, btnOutline, btnText], 377 + [hoverFill, hoverOutline, hoverText], 378 + ); 298 379 }); 299 380 300 381 // Buy button 301 382 if (buyBtn) { 302 - buyBtn.reposition({ center: "x", bottom: 12, screen }, getBuyText()); 383 + buyBtn.reposition({ center: "x", bottom: 20, screen }, getBuyText()); 303 384 304 385 let scheme, hover; 305 386 if (buyPending) { 306 387 const pulse = sin(performance.now() / 150) * 0.5 + 0.5; 307 - scheme = [ 308 - [floor(30 + pulse * 30), floor(40 + pulse * 20), 30], 309 - [floor(150 + pulse * 105), floor(200 + pulse * 55), 100], 310 - [floor(200 + pulse * 55), floor(220 + pulse * 35), 180], 311 - ]; 312 - hover = scheme; 313 - } else if (checkoutError) { 314 - scheme = [[50, 25, 25], [200, 80, 80], [255, 120, 120]]; 388 + scheme = isDark 389 + ? [[floor(30 + pulse * 30), floor(40 + pulse * 20), 30], 390 + [floor(150 + pulse * 105), floor(200 + pulse * 55), 100], 391 + [floor(200 + pulse * 55), floor(220 + pulse * 35), 180]] 392 + : [[floor(200 + pulse * 30), floor(220 + pulse * 20), 200], 393 + [floor(60 + pulse * 40), floor(120 + pulse * 40), 60], 394 + [floor(30 + pulse * 20), floor(80 + pulse * 30), 30]]; 315 395 hover = scheme; 316 396 } else { 317 397 const blink = sin(performance.now() / 500) * 0.3 + 0.7; 318 - scheme = [ 319 - [25, 35, 25], 320 - [floor(80 + blink * 70), floor(160 + blink * 95), floor(80 + blink * 70)], 321 - [180, 230, 180], 322 - ]; 323 - hover = [[40, 55, 40], [150, 255, 150], [200, 255, 200]]; 398 + scheme = isDark 399 + ? [[25, 35, 25], 400 + [floor(80 + blink * 70), floor(160 + blink * 95), floor(80 + blink * 70)], 401 + [180, 230, 180]] 402 + : [[220, 235, 220], 403 + [floor(40 + blink * 40), floor(100 + blink * 55), floor(40 + blink * 40)], 404 + [30, 80, 30]]; 405 + hover = isDark 406 + ? [[40, 55, 40], [150, 255, 150], [200, 255, 200]] 407 + : [[210, 230, 210], [40, 180, 40], [20, 60, 20]]; 324 408 } 325 - buyBtn.paint($, scheme, hover); 409 + buyBtn.paint($btn, scheme, hover); 326 410 } 327 411 328 - // Currency toggle (top right) 412 + // Currency toggle 329 413 if (currencyBtn) { 330 - currencyBtn.paint($, [[12, 12, 14], [50, 50, 55], [80, 80, 85]], [[12, 12, 14], [120, 120, 130], [200, 200, 210]]); 414 + const curDefault = isDark 415 + ? [[12, 12, 14], [50, 50, 55], [80, 80, 85]] 416 + : [[245, 243, 240], [180, 180, 185], [100, 100, 105]]; 417 + const curHover = isDark 418 + ? [[12, 12, 14], [120, 120, 130], [200, 200, 210]] 419 + : [[245, 243, 240], [100, 100, 110], [40, 40, 50]]; 420 + currencyBtn.paint($btn, curDefault, curHover); 331 421 } 332 - 333 - // Subtle bottom line 334 - ink(30).line(0, h - 1, w, h - 1); 335 422 } 336 423 337 424 function act({ event: e, screen, jump, sound, ui, api }) { ··· 341 428 setupButtons(ui, screen); 342 429 } 343 430 344 - // Tier selection 345 431 tierBtns.forEach((btn, i) => { 346 432 btn.act(e, { 347 433 down: () => { ··· 353 439 if (newAmount !== amount) { 354 440 amount = newAmount; 355 441 sound?.synth({ type: "sine", tone: 600 + i * 150, duration: 0.06, volume: 0.3 }); 356 - // Update tier labels to reflect selection 357 442 tierBtns.forEach((tb, j) => tb.replaceLabel(tierText(j))); 358 443 fetchCheckout(api); 359 444 } ··· 361 446 }); 362 447 }); 363 448 364 - // Currency toggle 365 449 currencyBtn?.act(e, { 366 450 push: () => { 367 451 currency = currency === "usd" ? "dkk" : "usd"; ··· 369 453 (t) => (currency === "usd" ? t.dkk : t.amount) === amount, 370 454 ); 371 455 if (tierIdx >= 0) { 372 - amount = 373 - currency === "dkk" ? tiers[tierIdx].dkk : tiers[tierIdx].amount; 456 + amount = currency === "dkk" ? tiers[tierIdx].dkk : tiers[tierIdx].amount; 374 457 } else { 375 458 amount = pricing[currency].suggested; 376 459 } ··· 381 464 }, 382 465 }); 383 466 384 - // Buy button 385 467 buyBtn?.act(e, { 386 468 down: () => { 387 469 sound?.synth({ type: "sine", tone: 440, duration: 0.05, volume: 0.3 }); ··· 393 475 sound?.synth({ type: "sine", tone: 880, duration: 0.1, volume: 0.4 }); 394 476 jump(checkoutUrl); 395 477 } else if (checkoutError) { 478 + // Silently retry 396 479 checkoutError = null; 397 480 fetchCheckout(api); 398 481 sound?.synth({ type: "sine", tone: 550, duration: 0.06, volume: 0.3 });
+56
system/public/aesthetic.computer/lib/disk.mjs
··· 5544 5544 LINE, 5545 5545 CUBEL, 5546 5546 ORIGIN, 5547 + // Project text glyphs onto a 3D plane, drawing each character's line/point 5548 + // commands through a projection function. 5549 + // Usage: ink(r,g,b,a).write3D("text", { origin, right, down, project }, scale) 5550 + // origin: [x,y,z] — top-left of the first character in world space 5551 + // right: [x,y,z] — unit-ish direction for text flow (gets scaled by glyph size) 5552 + // down: [x,y,z] — unit-ish direction for descending lines 5553 + // project: ([x,y,z]) => [screenX, screenY] — world-to-screen projection 5554 + // scale: multiplier for glyph coordinate spacing (default 1) 5555 + write3D: function (text, plane, scale = 1) { 5556 + if (!text || !plane || !tf) return $activePaintApi; 5557 + const { origin, right, down, project } = plane; 5558 + if (!origin || !right || !down || !project) return $activePaintApi; 5559 + 5560 + const blockW = tf.blockWidth || 6; 5561 + const charSpacing = blockW * scale; 5562 + let cursorX = 0; // accumulated character offset along `right` 5563 + 5564 + for (let ci = 0; ci < text.length; ci++) { 5565 + const char = text[ci]; 5566 + const glyph = tf.glyphs?.[char]; 5567 + const advance = tf.getAdvance?.(char) ?? blockW; 5568 + 5569 + if (glyph?.commands) { 5570 + for (const { name, args } of glyph.commands) { 5571 + if (name === "point") { 5572 + const px = (cursorX + args[0] * scale); 5573 + const py = args[1] * scale; 5574 + const wx = origin[0] + px * right[0] + py * down[0]; 5575 + const wy = origin[1] + px * right[1] + py * down[1]; 5576 + const wz = origin[2] + px * right[2] + py * down[2]; 5577 + const [sx, sy] = project([wx, wy, wz]); 5578 + $activePaintApi.box(Math.floor(sx), Math.floor(sy), 1, 1); 5579 + } else if (name === "line") { 5580 + const x1 = cursorX + args[0] * scale; 5581 + const y1 = args[1] * scale; 5582 + const x2 = cursorX + args[2] * scale; 5583 + const y2 = args[3] * scale; 5584 + const w1x = origin[0] + x1 * right[0] + y1 * down[0]; 5585 + const w1y = origin[1] + x1 * right[1] + y1 * down[1]; 5586 + const w1z = origin[2] + x1 * right[2] + y1 * down[2]; 5587 + const w2x = origin[0] + x2 * right[0] + y2 * down[0]; 5588 + const w2y = origin[1] + x2 * right[1] + y2 * down[1]; 5589 + const w2z = origin[2] + x2 * right[2] + y2 * down[2]; 5590 + const [s1x, s1y] = project([w1x, w1y, w1z]); 5591 + const [s2x, s2y] = project([w2x, w2y, w2z]); 5592 + graph.line( 5593 + Math.floor(s1x), Math.floor(s1y), 5594 + Math.floor(s2x), Math.floor(s2y), 5595 + ); 5596 + } 5597 + } 5598 + } 5599 + cursorX += advance * scale; 5600 + } 5601 + return $activePaintApi; 5602 + }, 5547 5603 }; 5548 5604 5549 5605 // TODO: Eventually move this to `num`. 24.07.23.18.52
+15 -21
system/public/aesthetic.computer/lib/ui.mjs
··· 941 941 } 942 942 943 943 if (pos.center === "x") { 944 - return { 945 - x: (pos.screen?.x || 0) + (pos.screen?.width || 0) / 2 - w / 2, 946 - y: (pos.screen?.y || 0) + (pos.y || 0), 947 - w, 948 - h, 949 - }; 944 + x = (pos.screen?.x || 0) + (pos.screen?.width || 0) / 2 - w / 2; 945 + } else { 946 + x = (pos.screen?.x || 0) + (pos.x || 0); 947 + if (pos.right !== undefined) { 948 + x += pos.screen.width - pos.right - w; 949 + } else { 950 + x += pos.left || 0; 951 + } 950 952 } 951 953 952 - // Position from top left if x and y are set on pos 953 - x = (pos.screen?.x || 0) + (pos.x || 0); 954 954 y = (pos.screen?.y || 0) + (pos.y || 0); 955 - 956 - // Compute "bottom" and "right" properties if they exist. 957 955 if (pos.bottom !== undefined) { 958 956 y += pos.screen.height - pos.bottom - this.#h; 959 957 } else { 960 958 y += pos.top || 0; 961 - } 962 - 963 - if (pos.right !== undefined) { 964 - x += pos.screen.width - pos.right - w; 965 - } else { 966 - x += pos.left || 0; 967 959 } 968 960 969 961 return { x, y, w, h }; ··· 1097 1089 return { x: sx + sw / 2 - w / 2, y: sy + sh / 2 - h / 2, w, h }; 1098 1090 } 1099 1091 if (pos.center === "x") { 1100 - return { x: sx + sw / 2 - w / 2, y: sy + y, w, h }; 1092 + x = sx + sw / 2 - w / 2; 1101 1093 } 1102 - if (pos.right !== undefined) { 1103 - x = sx + sw - pos.right - w; 1104 - } else { 1105 - x = sx + (pos.left || pos.x || 0); 1094 + if (pos.center !== "x") { 1095 + if (pos.right !== undefined) { 1096 + x = sx + sw - pos.right - w; 1097 + } else { 1098 + x = sx + (pos.left || pos.x || 0); 1099 + } 1106 1100 } 1107 1101 if (pos.bottom !== undefined) { 1108 1102 y = sy + sh - pos.bottom - h;