Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: inline images in news posts + oven screenshot endpoint

- Add ![alt](url) markdown image support to news.aesthetic.computer renderer
- Add GET /news-screenshot/:piece.png to oven (1200×675, CDN-cached)
- Add `ac-news screenshot <piece>` CLI command
- Style inline images in news post bodies

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

+232 -76
+45
at/news-cli.mjs
··· 433 433 } 434 434 435 435 // --------------------------------------------------------------------------- 436 + // Screenshot (via oven) 437 + // --------------------------------------------------------------------------- 438 + 439 + const OVEN_URL = process.env.OVEN_URL || "https://oven.aesthetic.computer"; 440 + 441 + async function commandScreenshot(args) { 442 + const piece = args._[1]; 443 + if (!piece) { 444 + console.error( 445 + "Usage: ac-news screenshot <piece>\n" + 446 + " ac-news screenshot notepat\n" + 447 + " ac-news screenshot notepat --force\n" + 448 + " ac-news screenshot @jeffrey/my-piece", 449 + ); 450 + process.exit(1); 451 + } 452 + 453 + const force = !!args.force; 454 + const url = `${OVEN_URL}/news-screenshot/${encodeURIComponent(piece)}.png?json=true${force ? "&force=true" : ""}`; 455 + 456 + console.log(`\n Capturing ${piece}...`); 457 + 458 + const res = await fetch(url); 459 + if (!res.ok) { 460 + const body = await res.json().catch(() => ({})); 461 + console.error(` Oven error (${res.status}): ${body.error || res.statusText}`); 462 + process.exit(1); 463 + } 464 + 465 + const data = await res.json(); 466 + const mdImage = `![${piece}](${data.url})`; 467 + 468 + console.log(` ${data.cached ? "Cached" : "Captured"}: ${data.width}×${data.height}`); 469 + console.log(` URL: ${data.url}`); 470 + console.log(`\n Markdown (paste into post body):\n`); 471 + console.log(` ${mdImage}\n`); 472 + } 473 + 474 + // --------------------------------------------------------------------------- 436 475 // Help 437 476 // --------------------------------------------------------------------------- 438 477 ··· 449 488 post "Title" --stdin Read body from stdin 450 489 post ... --dry-run Preview without posting 451 490 491 + Media: 492 + screenshot <piece> Capture a piece via oven (1200×675 PNG) 493 + screenshot <piece> --force Force-regenerate (skip cache) 494 + 452 495 Manage: 453 496 list [--limit N] List recent posts 454 497 edit <code> --title "New Title" Edit post title ··· 465 508 ac-news post "Weekly Update" --file updates/2026-03-24.md 466 509 ac-news post "What's New" --editor 467 510 ac-news edit ncd2 --replace "https://aesthetic.computer)" --with "https://aesthetic.computer/chat)" 511 + ac-news screenshot notepat 468 512 ac-news list 469 513 `); 470 514 } ··· 479 523 list: commandList, 480 524 edit: commandEdit, 481 525 delete: commandDelete, 526 + screenshot: commandScreenshot, 482 527 }; 483 528 484 529 async function main() {
+73
oven/server.mjs
··· 2655 2655 } 2656 2656 }); 2657 2657 2658 + // ─── News screenshot endpoint ────────────────────────────────────────────── 2659 + // Captures a piece at 16:9 (1200×675) for embedding in news.aesthetic.computer 2660 + // posts. Returns JSON with the CDN URL so the CLI can emit markdown. 2661 + // Usage: GET /news-screenshot/notepat.png 2662 + // GET /news-screenshot/notepat.png?force=true 2663 + app.get('/news-screenshot/:piece.png', async (req, res) => { 2664 + const { piece } = req.params; 2665 + const force = req.query.force === 'true'; 2666 + const width = 1200, height = 675; // 16:9 2667 + 2668 + try { 2669 + addServerLog('capture', '📰', `News screenshot: ${piece} (${width}×${height}${force ? ' FORCE' : ''})`); 2670 + 2671 + const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate( 2672 + 'news-screenshots', 2673 + piece, 2674 + width, 2675 + height, 2676 + async () => { 2677 + const result = await grabPiece(piece, { 2678 + format: 'png', 2679 + width, 2680 + height, 2681 + density: 2, 2682 + viewportScale: 1, 2683 + skipCache: force, 2684 + source: 'news', 2685 + }); 2686 + 2687 + if (!result.success) throw new Error(result.error); 2688 + 2689 + if (result.cached && result.cdnUrl && !result.buffer) { 2690 + const response = await fetch(result.cdnUrl); 2691 + if (!response.ok) throw new Error(`Failed to fetch cached screenshot: ${response.status}`); 2692 + return Buffer.from(await response.arrayBuffer()); 2693 + } 2694 + 2695 + return result.buffer; 2696 + }, 2697 + 'png', 2698 + force, 2699 + ); 2700 + 2701 + // Return JSON only when explicitly requested; default to serving the image. 2702 + if (req.query.json === 'true') { 2703 + return res.json({ 2704 + piece, 2705 + url: cdnUrl || `https://oven.aesthetic.computer/news-screenshot/${piece}.png`, 2706 + cached: fromCache, 2707 + width, 2708 + height, 2709 + }); 2710 + } 2711 + 2712 + if (fromCache && cdnUrl && !force) { 2713 + res.setHeader('X-Cache', 'HIT'); 2714 + res.setHeader('Cache-Control', 'public, max-age=604800'); 2715 + return res.redirect(302, cdnUrl); 2716 + } 2717 + 2718 + res.setHeader('Content-Type', 'image/png'); 2719 + res.setHeader('Content-Length', buffer.length); 2720 + res.setHeader('Cache-Control', force ? 'no-store' : 'public, max-age=86400'); 2721 + res.setHeader('X-Cache', force ? 'REGENERATED' : 'MISS'); 2722 + res.send(buffer); 2723 + 2724 + } catch (error) { 2725 + console.error('News screenshot error:', error); 2726 + addServerLog('error', '❌', `News screenshot failed: ${piece} - ${error.message}`); 2727 + res.status(500).json({ error: error.message }); 2728 + } 2729 + }); 2730 + 2658 2731 // Bulk ZIP download endpoint 2659 2732 app.get('/app-screenshots/download/:piece', async (req, res) => { 2660 2733 const { piece } = req.params;
+101 -76
system/netlify/functions/index.mjs
··· 7 7 import he from "he"; 8 8 const { encode } = he; 9 9 import * as num from "../../public/aesthetic.computer/lib/num.mjs"; 10 - import { 11 - parse, 12 - metadata, 13 - inferTitleDesc, 14 - updateCode, 15 - } from "../../public/aesthetic.computer/lib/parse.mjs"; 16 - import { respond } from "../../backend/http.mjs"; 17 - import { handleFromPermahandle } from "../../backend/authorization.mjs"; 18 - import { connect } from "../../backend/database.mjs"; 19 - import { defaultTemplateStringProcessor as html } from "../../public/aesthetic.computer/lib/helpers.mjs"; 20 - import { networkInterfaces } from "os"; 21 - const dev = process.env.CONTEXT === "dev" || process.env.NETLIFY_DEV === "true"; 22 - const BARE_KIDLISP_CODE = /^[0-9A-Za-z]{3,64}$/; 10 + import { 11 + parse, 12 + metadata, 13 + inferTitleDesc, 14 + updateCode, 15 + } from "../../public/aesthetic.computer/lib/parse.mjs"; 16 + import { respond } from "../../backend/http.mjs"; 17 + import { handleFromPermahandle } from "../../backend/authorization.mjs"; 18 + import { connect } from "../../backend/database.mjs"; 19 + import { defaultTemplateStringProcessor as html } from "../../public/aesthetic.computer/lib/helpers.mjs"; 20 + import { networkInterfaces } from "os"; 21 + const dev = process.env.CONTEXT === "dev" || process.env.NETLIFY_DEV === "true"; 22 + const BARE_KIDLISP_CODE = /^[0-9A-Za-z]{3,64}$/; 23 23 24 24 // Fire-and-forget piece hit tracking (don't await, don't block page load) 25 - async function trackPieceHit(piece, type) { 25 + async function trackPieceHit(piece, type) { 26 26 try { 27 27 const baseUrl = dev ? "https://localhost:8888" : "https://aesthetic.computer"; 28 28 const { got } = await import("got"); ··· 37 37 // Silent fail - don't let tracking break page loads 38 38 if (dev) console.log("📊 Hit tracking failed:", e.message); 39 39 } 40 - } 41 - 42 - async function findBareKidlispCode(slug) { 43 - if (!BARE_KIDLISP_CODE.test(slug)) return false; 44 - 45 - try { 46 - const database = await connect(); 47 - const record = await database.db.collection("kidlisp").findOne( 48 - { code: slug }, 49 - { projection: { _id: 1 } }, 50 - ); 51 - await database.disconnect(); 52 - return !!record; 53 - } catch (err) { 54 - console.log(`[kidlisp] Bare /${slug} lookup failed:`, err?.message || err); 55 - return false; 56 - } 57 - } 58 - 59 - function redirectToKidlispCode(event, code) { 60 - const query = new URLSearchParams(event.queryStringParameters || {}); 61 - const suffix = query.toString() ? `?${query.toString()}` : ""; 62 - const location = `/$${code}${suffix}`; 63 - 64 - return respond( 65 - 302, 66 - `<a href="${location}">Redirecting to ${location}</a>`, 67 - { 68 - "Content-Type": "text/html", 69 - Location: location, 70 - }, 71 - ); 72 - } 40 + } 41 + 42 + async function findBareKidlispCode(slug) { 43 + if (!BARE_KIDLISP_CODE.test(slug)) return false; 44 + 45 + try { 46 + const database = await connect(); 47 + const record = await database.db.collection("kidlisp").findOne( 48 + { code: slug }, 49 + { projection: { _id: 1 } }, 50 + ); 51 + await database.disconnect(); 52 + return !!record; 53 + } catch (err) { 54 + console.log(`[kidlisp] Bare /${slug} lookup failed:`, err?.message || err); 55 + return false; 56 + } 57 + } 58 + 59 + function redirectToKidlispCode(event, code) { 60 + const query = new URLSearchParams(event.queryStringParameters || {}); 61 + const suffix = query.toString() ? `?${query.toString()}` : ""; 62 + const location = `/$${code}${suffix}`; 63 + 64 + return respond( 65 + 302, 66 + `<a href="${location}">Redirecting to ${location}</a>`, 67 + { 68 + "Content-Type": "text/html", 69 + Location: location, 70 + }, 71 + ); 72 + } 73 73 74 74 async function fun(event, context) { 75 75 const _startTime = Date.now(); ··· 762 762 } 763 763 } 764 764 } 765 - } else if (parsed.source) { 765 + } else if (parsed.source) { 766 766 // Handle inline kidlisp code that doesn't need file loading 767 767 console.log("[kidlisp] Using inline kidlisp source for metadata"); 768 768 ··· 779 779 if (title) { 780 780 meta = { ...meta, title, standaloneTitle: true }; 781 781 } 782 - } 783 - } 784 - 785 - if (statusCode === 404 && !sourceCode && !fromHandle && await findBareKidlispCode(slug)) { 786 - console.log(`[kidlisp] Converting bare /${slug} to /$${slug} and redirecting`); 787 - return redirectToKidlispCode(event, slug); 788 - } 789 - } catch (err) { 782 + } 783 + } 784 + 785 + if (statusCode === 404 && !sourceCode && !fromHandle && await findBareKidlispCode(slug)) { 786 + console.log(`[kidlisp] Converting bare /${slug} to /$${slug} and redirecting`); 787 + return redirectToKidlispCode(event, slug); 788 + } 789 + } catch (err) { 790 790 // If either module doesn't load, then we can fallback to the main route. 791 791 console.log("🔴 Error loading module:", err, sourceCode); 792 792 return redirect; ··· 978 978 // 🎛️ Spreadnob bridge — called directly by M4L script commands 979 979 window.acSnNote = function(n) { send({ type: "spreadnob:note", content: { note: n } }); }; 980 980 window.acSnTarget = function(name) { send({ type: "spreadnob:target", content: { name: name } }); }; 981 - window.acSnValue = function(v) { send({ type: "spreadnob:value", content: { value: v } }); }; 982 - window.acSnActive = function(v) { send({ type: "spreadnob:active", content: { active: v } }); }; 983 - window.acSnMin = function(v) { send({ type: "spreadnob:min", content: { min: v } }); }; 984 - window.acSnMax = function(v) { send({ type: "spreadnob:max", content: { max: v } }); }; 985 - window.acSnState = function(raw, note, shift, locked, ambiguous) { 986 - send({ 987 - type: "spreadnob:state", 988 - content: { raw: raw, note: note, shift: shift, locked: locked, ambiguous: ambiguous } 989 - }); 990 - }; 991 - window.acSnQwertyRange = function(low, high) { 992 - send({ type: "spreadnob:qwerty-range", content: { low: low, high: high } }); 993 - }; 994 - window.acSnReady = function() { send({ type: "spreadnob:ready", content: {} }); }; 981 + window.acSnValue = function(v) { send({ type: "spreadnob:value", content: { value: v } }); }; 982 + window.acSnActive = function(v) { send({ type: "spreadnob:active", content: { active: v } }); }; 983 + window.acSnMin = function(v) { send({ type: "spreadnob:min", content: { min: v } }); }; 984 + window.acSnMax = function(v) { send({ type: "spreadnob:max", content: { max: v } }); }; 985 + window.acSnState = function(raw, note, shift, locked, ambiguous) { 986 + send({ 987 + type: "spreadnob:state", 988 + content: { raw: raw, note: note, shift: shift, locked: locked, ambiguous: ambiguous } 989 + }); 990 + }; 991 + window.acSnQwertyRange = function(low, high) { 992 + send({ type: "spreadnob:qwerty-range", content: { low: low, high: high } }); 993 + }; 994 + window.acSnReady = function() { send({ type: "spreadnob:ready", content: {} }); }; 995 995 996 996 // Signal to M4L that we're ready 997 997 if (window.max) { ··· 1266 1266 "url": "https://aesthetic.computer/" + slug, 1267 1267 "applicationCategory": "Creative Coding", 1268 1268 "operatingSystem": "Web Browser", 1269 - "codeRepository": "https://tangled.org/aesthetic.computer/core", 1270 - "sourceOrganization": { "@type": "Organization", "name": "Aesthetic Computer", "url": "https://tangled.org/aesthetic.computer/core" } 1269 + "codeRepository": "https://tangled.org/aesthetic.computer/core", 1270 + "sourceOrganization": { "@type": "Organization", "name": "Aesthetic Computer", "url": "https://tangled.org/aesthetic.computer/core" } 1271 1271 })} 1272 1272 </script> 1273 1273 </head> ··· 1276 1276 <h1>${encode(title)}</h1> 1277 1277 <p>${encode(desc)}</p> 1278 1278 <p>Aesthetic Computer is an open creative computing platform for making art, games, and tools in the browser using JavaScript and KidLisp. Navigate by typing a piece name (e.g. &quot;painting&quot;, &quot;line&quot;, &quot;wand&quot;, &quot;prompt&quot;) into the command prompt and pressing Enter. See llms.txt for full documentation: https://aesthetic.computer/llms.txt</p> 1279 - <p>Source code: https://tangled.org/aesthetic.computer/core | Pieces (programs): https://tangled.org/aesthetic.computer/core/tree/main/system/public/aesthetic.computer/disks | Runtime: https://tangled.org/aesthetic.computer/core/tree/main/system/public/aesthetic.computer/lib</p> 1279 + <p>Source code: https://tangled.org/aesthetic.computer/core | Pieces (programs): https://tangled.org/aesthetic.computer/core/tree/main/system/public/aesthetic.computer/disks | Runtime: https://tangled.org/aesthetic.computer/core/tree/main/system/public/aesthetic.computer/lib</p> 1280 1280 </article> 1281 1281 <!-- Boot Canvas - VHS style with floating code pages --> 1282 1282 <canvas id="boot-canvas" style="position:fixed;top:0;left:0;width:100vw;height:100vh;height:100dvh;z-index:99999;pointer-events:none;margin:0;padding:0;image-rendering:pixelated;image-rendering:crisp-edges;"></canvas> ··· 1302 1302 var isNotepat=location.hostname==='notepat.com'||location.hostname==='www.notepat.com'||location.pathname==='/notepat'||location.pathname.startsWith('/notepat?')||location.pathname.startsWith('/notepat/'); 1303 1303 // Notebook: Python/Jupyter notebook with scientific aesthetic 1304 1304 var isNotebook=qs.indexOf('notebook=true')>=0; 1305 + // Boot animation mode: 'serious' (clean/refined, default) or 'aesthetic' (VHS/glitch) 1306 + var bootTheme=params.get('boot')||'serious';var isSerious=bootTheme==='serious'; 1305 1307 // Density param for scaling (default 1, FF1 uses 8 for 4K) 1306 1308 var densityMatch=qs.match(/density=(\d+)/);var densityParam=densityMatch?parseInt(densityMatch[1]):1; 1307 1309 var isLightMode=window.matchMedia&&window.matchMedia('(prefers-color-scheme:light)').matches; ··· 1520 1522 var logFS=densityParam===1&&isDeviceMode?Math.max(14,Math.floor(H/60)):4*S*dS; 1521 1523 x.font=logFS+'px monospace';var logY=(densityParam===1&&isDeviceMode?Math.floor(H/20):16*S*dS)+embedPad;var logSpacing=densityParam===1&&isDeviceMode?Math.floor(logFS*1.5):7*S*dS;for(var li=0;li<lines.length&&li<10;li++){var ln=lines[li],ly=logY+li*logSpacing,la=Math.max(0.3,1-li*0.08),lc=klCols[li%klCols.length];var tw=x.measureText(ln.text).width;var logX=densityParam===1&&isDeviceMode?20:10*S*dS;var textX=densityParam===1&&isDeviceMode?30:(logX+3*S*dS);var pillH=densityParam===1&&isDeviceMode?Math.floor(logFS*1.2):6*S*dS;var pillR=densityParam===1&&isDeviceMode?6:3*S*dS;var pillW=tw+(textX-logX)*2;x.globalAlpha=la*0.15;x.fillStyle='rgb('+lc[0]+','+lc[1]+','+lc[2]+')';x.beginPath();x.roundRect(logX,ly-pillH*0.65,pillW,pillH,pillR);x.fill();x.globalAlpha=la;x.fillStyle='rgb('+lc[0]+','+lc[1]+','+lc[2]+')';x.fillText(ln.text,textX,ly);} 1522 1524 x.globalAlpha=1;requestAnimationFrame(anim);return;} 1523 - // GIVE variant: HIGH ALERT SIREN MODE - keep logo/logs, add alarm effects 1524 - if(giveVariant){ 1525 + // Serious mode — clean, refined boot animation (default) 1526 + if(isSerious){var sbg=isLightMode?'#ffffff':'#000000';x.fillStyle=sbg;x.fillRect(0,0,W,H); 1527 + // Logo with pink bg + chromatic aberration 1528 + var lS=21*S,lX=5*S,lY=5*S;var logoImg=imgFullLoaded?imgFull:img; 1529 + var pnkR=isLightMode?(180+Math.sin(t*1.5)*15|0):(80+Math.sin(t*1.5)*20|0),pnkG=isLightMode?(100+Math.sin(t*2.1)*10|0):(30+Math.sin(t*2.1)*10|0),pnkB=isLightMode?(160+Math.sin(t*1.8)*20|0):(100+Math.sin(t*1.8)*25|0); 1530 + x.globalAlpha=0.6;x.fillStyle='rgb('+pnkR+','+pnkG+','+pnkB+')';x.fillRect(lX-2*S,lY-2*S,lS+4*S,lS+4*S);x.globalAlpha=1; 1531 + x.imageSmoothingEnabled=imgFullLoaded; 1532 + x.globalAlpha=0.2;x.filter='hue-rotate(-40deg) saturate(1.5)';x.drawImage(logoImg,lX-2*S,lY-1*S,lS,lS);x.filter='hue-rotate(40deg) saturate(1.5)';x.drawImage(logoImg,lX+2*S,lY+1*S,lS,lS); 1533 + x.filter='none';x.globalAlpha=1;x.drawImage(logoImg,lX,lY,lS,lS); 1534 + // Title + status — consistent 6*S line spacing 1535 + var tX=lX+lS+4*S,tYbase=lY+2*S,rowH=6*S,row=0; 1536 + x.font='bold '+(4*S)+'px monospace';x.fillStyle=isLightMode?'#000':'#fff';x.fillText('Aesthetic',tX,tYbase);var dotX=tX+x.measureText('Aesthetic').width+1*S;var dotPulse=0.5+Math.sin(t*2)*0.5;x.globalAlpha=dotPulse;x.fillStyle=isLightMode?'#444':'#ccc';x.fillText('.',dotX,tYbase);x.globalAlpha=1;var compX=dotX+x.measureText('.').width+1*S;x.fillStyle=isLightMode?'#000':'#fff';x.fillText('Computer',compX,tYbase); 1537 + var wsX=compX+x.measureText('Computer').width+4*S;var wsDotR=1.5*S;x.beginPath();x.arc(wsX+wsDotR,tYbase-2*S,wsDotR,0,Math.PI*2);if(sessionConnected){x.fillStyle=isLightMode?'#006400':'#80ffa0';}else{x.globalAlpha=0.4+Math.sin(t*4)*0.3;x.fillStyle=isLightMode?'#c8960a':'#ffb050';}x.fill();x.globalAlpha=1; 1538 + row++;var sec=(performance.now()-bootStart)/1000;var secT=sec.toFixed(2)+'s';x.font='bold '+(4*S)+'px monospace';x.globalAlpha=0.75;x.fillStyle=isLightMode?'#333':'#ddd';x.fillText(secT,tX,tYbase+row*rowH); 1539 + row++;var d=new Date(),utcT=d.getUTCHours().toString().padStart(2,'0')+':'+d.getUTCMinutes().toString().padStart(2,'0')+':'+d.getUTCSeconds().toString().padStart(2,'0')+' UTC';x.font=(3*S)+'px monospace';x.globalAlpha=0.6;x.fillStyle=isLightMode?'#555':'#bbb';x.fillText(utcT,tX,tYbase+row*rowH);x.globalAlpha=1; 1540 + if(uH){row++;var hAge=(performance.now()-hST)/1000,hFade=Math.min(1,hAge*2);x.font='bold '+(5*S)+'px monospace';x.globalAlpha=hFade*0.9;x.fillStyle=isLightMode?'#222':'#eee';x.fillText(uH,tX,tYbase+row*rowH);x.globalAlpha=1;} 1541 + // Boot log messages 1542 + row++;var tSY=tYbase+row*rowH;x.font=(4*S)+'px monospace';for(var i=0;i<lines.length;i++){var y=tSY+i*5*S;if(y>H-3*S)break;var al=Math.max(0.35,1-i*0.08);var dt=lines[i].text;var isActive=i===0&&dt.indexOf('_')>-1;if(isActive){var blink=Math.sin(t*6)>0;dt=blink?dt:dt.replace(/_$/,' ');}else if(i===0&&f%30<15)dt=dt.replace(/_$/,' ');x.globalAlpha=al;var gv=isLightMode?(30+i*8):(245-i*8);x.fillStyle='rgb('+gv+','+gv+','+gv+')';x.fillText(dt,tX,y);}x.globalAlpha=1; 1543 + // MOTD — inline after boot logs 1544 + if(motd){var mAge=(performance.now()-motdStart)/1000;var mFade=Math.min(1,mAge*0.8);var motdY=tSY+Math.min(lines.length,8)*5*S+4*S;x.font='bold '+(4*S)+'px YWFTProcessing-Bold, monospace';x.globalAlpha=mFade*0.85;x.fillStyle=isLightMode?'#222':'#eee';var maxMotdW=W-tX-8*S;var roughChars=Math.max(6,Math.floor(maxMotdW/(2.5*S)));var linesMotd=wrapMotdText(motd,roughChars);for(var li=0;li<linesMotd.length;li++){var my=motdY+li*5*S;if(my>H-8*S)break;x.fillText(linesMotd[li],tX,my);}if(motdHandle){var lastMotdY=motdY+Math.min(linesMotd.length,6)*5*S;x.font=(3*S)+'px monospace';x.globalAlpha=mFade*0.5;x.fillStyle=isLightMode?'#555':'#bbb';x.fillText('— '+motdHandle,tX,lastMotdY);}x.globalAlpha=1;} 1545 + if(connFlash>0.01){x.globalAlpha=connFlash*0.2;x.fillStyle=isLightMode?'#006400':'#80ffa0';x.fillRect(0,0,W,H);x.globalAlpha=1;} 1546 + if(errorMode||errorFlash>0.01){var errElapsed=errorStartTime?(performance.now()-errorStartTime)/1000:0;if(errorMode&&errElapsed>1.0){x.globalCompositeOperation='source-over';x.globalAlpha=1;x.fillStyle='rgb(0,0,0)';x.fillRect(0,0,W,H);}else if(errorMode&&errElapsed>0.5){var blackFade=(errElapsed-0.5)/0.5;x.globalCompositeOperation='source-over';x.globalAlpha=blackFade*0.9;x.fillStyle='rgb(0,0,0)';x.fillRect(0,0,W,H);x.globalAlpha=1-blackFade*0.7;var xSize=Math.min(W,H)*0.7,xThick=Math.max(8*S,xSize*0.12),cx=W/2,cy=H/2;x.strokeStyle='rgb(255,0,0)';x.lineWidth=xThick;x.lineCap='round';x.beginPath();x.moveTo(cx-xSize/2,cy-xSize/2);x.lineTo(cx+xSize/2,cy+xSize/2);x.stroke();x.beginPath();x.moveTo(cx+xSize/2,cy-xSize/2);x.lineTo(cx-xSize/2,cy+xSize/2);x.stroke();}else{x.globalCompositeOperation='screen';var errA=errorMode?0.4+Math.sin(t*8)*0.2:errorFlash*0.5;x.globalAlpha=errA;x.fillStyle='rgb(255,40,60)';x.fillRect(0,0,W,H);x.globalCompositeOperation='source-over';if(errorMode){var xGrow=Math.min(1,errElapsed/0.3);var xSize=Math.min(W,H)*0.7*xGrow,xThick=Math.max(8*S,xSize*0.12),cx=W/2,cy=H/2;x.globalAlpha=0.9;x.strokeStyle='rgb(255,20,0)';x.lineWidth=xThick;x.lineCap='round';x.beginPath();x.moveTo(cx-xSize/2,cy-xSize/2);x.lineTo(cx+xSize/2,cy+xSize/2);x.stroke();x.beginPath();x.moveTo(cx+xSize/2,cy-xSize/2);x.lineTo(cx-xSize/2,cy+xSize/2);x.stroke();}}x.globalCompositeOperation='source-over';x.globalAlpha=1;} 1547 + x.globalAlpha=1;requestAnimationFrame(anim);return;} 1548 + // (GIVE variant removed — promo ended) 1549 + if(false){ 1525 1550 var now=performance.now(); 1526 1551 // CRISP NEAREST-NEIGHBOR MODE 1527 1552 x.imageSmoothingEnabled=false; ··· 1970 1995 }catch(e){} 1971 1996 x.globalAlpha=1;requestAnimationFrame(anim);return; 1972 1997 } 1973 - // Normal boot rendering (non-GIVE mode) 1998 + // Aesthetic mode — VHS-style boot animation with scrolling source code (?boot=aesthetic) 1974 1999 // Light mode: warm sandy/tan/cream tones (matching kidlisp.com & AC light theme); Dark mode: deep moody colors 1975 2000 var BGCOLS=isLightMode?['#fcf7c5','#f5f0c0','#fffacd','#f5ebe0','#e8e3b0','#f0ebd0','#fcf5c8','#f5ecd0']:['#2d2020','#202d24','#20202d','#2d2820','#28202d','#202d2d','#2d2028','#242d20']; 1976 2001 var sHH=HH*S;
+5
system/netlify/functions/news.mjs
··· 351 351 s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>"); 352 352 // Italic (single *). 353 353 s = s.replace(/\*(.+?)\*/g, "<em>$1</em>"); 354 + // Markdown images ![alt](url) — must come before links. 355 + s = s.replace( 356 + /!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g, 357 + '<img src="$2" alt="$1" loading="lazy" />', 358 + ); 354 359 // Markdown links [text](url). 355 360 s = s.replace( 356 361 /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
+8
system/public/news.aesthetic.computer/main.css
··· 624 624 padding: 0; 625 625 } 626 626 627 + .news-op-body img { 628 + max-width: 100%; 629 + height: auto; 630 + border-radius: 4px; 631 + margin: 0.6em 0; 632 + display: block; 633 + } 634 + 627 635 .news-op-body a { 628 636 color: var(--ac-pink, #cd5c9b); 629 637 }