diff --git a/system/backend/mime-media.mjs b/system/backend/mime-media.mjs --- a/system/backend/mime-media.mjs +++ b/system/backend/mime-media.mjs @@ -11,6 +11,7 @@ }; export const MEDIA_THREADS = "mime-media-threads"; const visible = { + status: { $ne: "wip" }, nuked: { $ne: true }, deleted: { $ne: true }, private: { $ne: true }, diff --git a/system/backend/painting-wips.mjs b/system/backend/painting-wips.mjs new file mode 100644 --- /dev/null +++ b/system/backend/painting-wips.mjs @@ -0,0 +1,211 @@ +import { createHash, timingSafeEqual } from "node:crypto"; +import { gunzipSync } from "node:zlib"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { GridFSBucket } from "mongodb"; +import { generateUniqueCode } from "./generate-short-code.mjs"; + +const HOUR = 60 * 60 * 1000; +const MAX_STATE_BYTES = 32 * 1024 * 1024; +const idPattern = /^[a-zA-Z0-9_-]{20,80}$/; +const keyPattern = /^[a-zA-Z0-9_-]{32,100}$/; +const digest = (value) => createHash("sha256").update(value).digest("hex"); + +export class PaintingWipError extends Error { + constructor(status, message) { super(message); this.status = status; } +} +const fail = (status, message) => { throw new PaintingWipError(status, message); }; + +export function paintingWipExpired(painting, now = Date.now()) { + return painting?.status === "wip" && painting.wip?.steps === 0 && + new Date(painting.wip.expiresAt).getTime() <= now; +} + +export function canEditPaintingWip(painting, user, key) { + if (!painting?.wip) return false; + if (painting.user && user?.sub === painting.user) return true; + if (painting.user || !keyPattern.test(key || "")) return false; + const expected = Buffer.from(painting.wip.editorHash, "hex"); + const supplied = Buffer.from(digest(key), "hex"); + return expected.length === supplied.length && timingSafeEqual(expected, supplied); +} + +export function paintingWipMetadata(painting) { + return { + id: painting.wip.id, code: painting.code, status: painting.status, + revision: painting.wip.revision, steps: painting.wip.steps, + width: painting.wip.width, height: painting.wip.height, + parent: painting.wip.parent || null, updatedAt: painting.updatedAt, + }; +} + +function statePayload(encoded) { + if (typeof encoded !== "string" || encoded.length > Math.ceil(MAX_STATE_BYTES * 4 / 3) || + !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) fail(413, "Painting state is too large or invalid"); + const data = Buffer.from(encoded, "base64"); + let state; + try { state = JSON.parse(gunzipSync(data, { maxOutputLength: 128 * 1024 * 1024 })); } + catch { fail(400, "Invalid painting state"); } + const piece = state?.piece; + if (state?.format !== "aesthetic.computer/painting-state" || state.version !== 1 || + piece?.schema !== "aesthetic.computer/nopaint-piece" || piece.version !== 1 || + !Number.isInteger(piece.width) || !Number.isInteger(piece.height) || + piece.width < 1 || piece.height < 1 || piece.width * piece.height > 8 * 1024 * 1024 || + !Array.isArray(piece.layers) || !piece.layers.length || piece.layers.length > 8192) fail(400, "Invalid painting dimensions or layers"); + function pixels(value, length) { + return value && typeof value.$pixels === "string" && Number.isSafeInteger(length) && length >= 0 && + /^[A-Za-z0-9+/]*={0,2}$/.test(value.$pixels) && Buffer.from(value.$pixels, "base64").length === length; + } + if (!pixels(piece.composite?.pixels, piece.width * piece.height * 4)) fail(400, "Invalid painting pixels"); + for (const layer of piece.layers) { + const p = layer?.pixels; + if (!p || !["composite", "overlay"].includes(p.mode) || !layer.id || !layer.code?.source || + !Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 0 || p.height < 0 || + !pixels(p.data, p.width * p.height * 4)) fail(400, "Invalid painting layer"); + } + return { data, hash: digest(data), width: piece.width, height: piece.height, layers: piece.layers.length }; +} + +// Repository operations keep the lifecycle testable without a live database. +export function createPaintingWipService(repo, now = () => Date.now()) { + async function find(code) { + if (typeof code !== "string" || !/^[A-Za-z0-9]{3,12}$/.test(code)) fail(400, "Invalid painting code"); + const painting = await repo.find({ code }); + if (!painting?.wip || paintingWipExpired(painting, now())) fail(404, "Painting not found"); + return painting; + } + function authorize(painting, user, key) { + if (!canEditPaintingWip(painting, user, key)) fail(403, "This painting belongs to another editor"); + } + return { + async create(input, user) { + if (!idPattern.test(input.id || "") || !keyPattern.test(input.key || "")) fail(400, "Invalid editor identity"); + if (![input.width, input.height].every((n) => Number.isInteger(n) && n > 0) || + input.width * input.height > 8 * 1024 * 1024) fail(400, "Invalid painting dimensions"); + if (!Number.isInteger(input.initialLayers) || input.initialLayers < 1 || input.initialLayers > 8192) fail(400, "Invalid initial steps"); + const existing = await repo.find({ "wip.id": input.id }); + if (existing) { + authorize(existing, user, input.key); + if (paintingWipExpired(existing, now())) fail(410, "This empty painting has expired"); + return paintingWipMetadata(existing); + } + if (input.parent && (typeof input.parent !== "string" || !/^[A-Za-z0-9]{3,12}$/.test(input.parent) || + !await repo.find({ code: input.parent }))) fail(404, "Starting painting not found"); + const date = new Date(now()); + for (let attempt = 0; attempt < 8; attempt++) { + const painting = { + code: await repo.code(), slug: `wip/${input.id}`, status: "wip", when: date, updatedAt: date, + bucket: user ? "user-aesthetic-computer" : "art-aesthetic-computer", + ...(user?.sub ? { user: user.sub } : {}), + wip: { id: input.id, editorHash: digest(input.key), revision: 0, steps: 0, + width: input.width, height: input.height, initialLayers: input.initialLayers || 1, + parent: input.parent || null, expiresAt: new Date(now() + HOUR), snapshot: null }, + }; + try { await repo.insert(painting); return paintingWipMetadata(painting); } + catch (error) { + if (error.code !== 11000) throw error; + const raced = await repo.find({ "wip.id": input.id }); + if (raced) { authorize(raced, user, input.key); return paintingWipMetadata(raced); } + } + } + fail(503, "Could not allocate a painting code"); + }, + async read(code, includeState = false, user, key) { + let painting = await find(code); + let state = null; + if (includeState && painting.wip.snapshot) { + try { state = (await repo.readState(painting.wip.snapshot)).toString("base64"); } + catch { + // A successful save may have reclaimed the snapshot after our read. + painting = await find(code); + state = (await repo.readState(painting.wip.snapshot)).toString("base64"); + } + } + return { ...paintingWipMetadata(painting), canEdit: painting.status === "wip" && canEditPaintingWip(painting, user, key), + ...(includeState ? { state } : {}) }; + }, + async save(input, user) { + const painting = await find(input.code); + authorize(painting, user, input.key); + if (painting.status !== "wip") fail(409, "This painting is Done. Start a new painting from it."); + const state = statePayload(input.state); + // Retrying a response that was lost does not add a revision. + if (state.hash === painting.wip.hash) return paintingWipMetadata(painting); + if (input.revision !== painting.wip.revision) fail(409, "This painting changed in another tab. Reload it before continuing."); + const snapshot = await repo.writeState(state.data); + const wip = { ...painting.wip, revision: painting.wip.revision + 1, + snapshot, hash: state.hash, width: state.width, height: state.height, + steps: Math.max(0, state.layers - painting.wip.initialLayers) }; + if (wip.steps > 0) delete wip.expiresAt; + const update = { wip, updatedAt: new Date(now()) }; + const changed = await repo.update({ code: input.code, status: "wip", "wip.revision": input.revision }, update); + if (!changed) { await repo.deleteState(snapshot); fail(409, "This painting changed in another tab"); } + if (painting.wip.snapshot) await repo.deleteState(painting.wip.snapshot).catch(() => {}); + return paintingWipMetadata({ ...painting, ...update }); + }, + async seal(input, user, slug) { + if (typeof slug !== "string" || !slug.length || slug.length > 512) fail(400, "Invalid painting slug"); + const painting = await find(input.code); + authorize(painting, user, input.key); + if (painting.status === "done") return { code: painting.code, slug: painting.slug, paintingId: String(painting._id) }; + if (!painting.wip.snapshot || input.revision !== painting.wip.revision) fail(409, "Save the latest painting state before Done"); + const wip = { ...painting.wip }; + delete wip.expiresAt; + const changed = await repo.update({ code: input.code, status: "wip", "wip.revision": input.revision }, + { status: "done", slug, wip, updatedAt: new Date(now()), completedAt: new Date(now()), + bucket: user ? "user-aesthetic-computer" : "art-aesthetic-computer", + ...(user?.sub ? { user: user.sub } : {}) }); + if (!changed) fail(409, "This painting changed before Done"); + return { code: painting.code, slug, paintingId: String(painting._id), newlyDone: true }; + }, + }; +} + +export async function mongoPaintingWips(db) { + const paintings = db.collection("paintings"); + await paintings.createIndex({ "wip.id": 1 }, { unique: true, sparse: true }); + await paintings.createIndex({ code: 1 }, { unique: true, sparse: true }); + await paintings.createIndex({ status: 1, "wip.expiresAt": 1 }); + const files = new GridFSBucket(db, { bucketName: "painting-states" }); + const repo = { + find: (query) => paintings.findOne(query), + code: () => generateUniqueCode(paintings), + insert: (record) => paintings.insertOne(record), + update: async (query, fields) => (await paintings.updateOne(query, { $set: fields })).modifiedCount === 1, + writeState: async (buffer) => { + const stream = files.openUploadStream("painting-state.json.gz", { metadata: { createdAt: new Date() } }); + await pipeline(Readable.from([buffer]), stream); + return stream.id; + }, + readState: async (id) => { + const chunks = []; + for await (const chunk of files.openDownloadStream(id)) chunks.push(chunk); + return Buffer.concat(chunks); + }, + deleteState: (id) => files.delete(id), + }; + return { service: createPaintingWipService(repo), repo, paintings }; +} + +// Like tape drafts, reclaim a bounded batch as new WIP traffic arrives. +export async function pruneEmptyPaintingWips(db) { + const { paintings, repo } = await mongoPaintingWips(db); + const expired = await paintings.find({ status: "wip", "wip.steps": 0, "wip.expiresAt": { $lte: new Date() } }).limit(20).toArray(); + for (const painting of expired) { + const deleted = await paintings.deleteOne({ _id: painting._id, status: "wip", "wip.steps": 0, "wip.revision": painting.wip.revision }); + if (deleted.deletedCount && painting.wip.snapshot) await repo.deleteState(painting.wip.snapshot).catch(() => {}); + } +} + +export async function paintingWipPixels(db, painting) { + if (paintingWipExpired(painting)) fail(404, "Painting not found"); + if (!painting.wip.snapshot) { + return { width: painting.wip.width, height: painting.wip.height, + pixels: Buffer.alloc(painting.wip.width * painting.wip.height * 4, 255) }; + } + const { repo } = await mongoPaintingWips(db); + const data = await repo.readState(painting.wip.snapshot); + const state = JSON.parse(gunzipSync(data, { maxOutputLength: 128 * 1024 * 1024 })); + return { width: state.piece.width, height: state.piece.height, + pixels: Buffer.from(state.piece.composite.pixels.$pixels, "base64") }; +} diff --git a/system/backend/seal-painting-wip.mjs b/system/backend/seal-painting-wip.mjs new file mode 100644 --- /dev/null +++ b/system/backend/seal-painting-wip.mjs @@ -0,0 +1,15 @@ +import { mongoPaintingWips } from "./painting-wips.mjs"; +import { createMediaRecord, MediaTypes } from "./media-atproto.mjs"; + +export async function sealPaintingWip(database, body, user) { + const { service, paintings } = await mongoPaintingWips(database.db); + const result = await service.seal(body.wip, user, body.slug); + if (result.newlyDone) { + try { + const record = await paintings.findOne({ code: result.code }); + const atproto = await createMediaRecord(database, MediaTypes.PAINTING, record, { userSub: user?.sub || null }); + if (atproto?.rkey) await paintings.updateOne({ _id: record._id }, { $set: { "atproto.rkey": atproto.rkey } }); + } catch (error) { console.warn("Painting federation:", error.message); } + } + return result; +} diff --git a/system/netlify/functions/media-collection.js b/system/netlify/functions/media-collection.js --- a/system/netlify/functions/media-collection.js +++ b/system/netlify/functions/media-collection.js @@ -45,7 +45,7 @@ // Query the media collection for the specific user. // (Ignoring the `nuked` flag.) const media = await mediaCollection - .find({ user: userSub, nuked: { $ne: true } }) + .find({ user: userSub, nuked: { $ne: true }, status: { $ne: "wip" } }) .toArray(); // Only expect `painting` and `piece` for now. 23.10.12.22.32 diff --git a/system/netlify/functions/painting-code.mjs b/system/netlify/functions/painting-code.mjs --- a/system/netlify/functions/painting-code.mjs +++ b/system/netlify/functions/painting-code.mjs @@ -2,12 +2,14 @@ // Painting Code Lookup API // Returns painting slug by short code (e.g., "k3d" or "WDv") import { connect } from "../../backend/database.mjs"; +import { paintingWipExpired } from "../../backend/painting-wips.mjs"; function respond(statusCode, body) { return { statusCode, headers: { "Content-Type": "application/json", + "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "GET, OPTIONS", @@ -36,10 +38,10 @@ // Look up painting by code const painting = await paintings.findOne( { code }, - { projection: { slug: 1, code: 1, user: 1, nuked: 1, _id: 0 } } + { projection: { slug: 1, code: 1, user: 1, nuked: 1, status: 1, "wip.steps": 1, "wip.expiresAt": 1, _id: 0 } } ); - if (!painting) { + if (!painting || paintingWipExpired(painting)) { await database.disconnect(); return respond(404, { error: "Painting not found" }); } @@ -62,6 +64,7 @@ return respond(200, { slug: painting.slug, code: painting.code, handle: handle, + status: painting.status || "done", nuked: painting.nuked || false, discussion: `/mime/#/media/painting/${encodeURIComponent(painting.code)}`, }); diff --git a/system/netlify/functions/painting-wip.mjs b/system/netlify/functions/painting-wip.mjs new file mode 100644 --- /dev/null +++ b/system/netlify/functions/painting-wip.mjs @@ -0,0 +1,42 @@ +import { authorize } from "../../backend/authorization.mjs"; +import { connect } from "../../backend/database.mjs"; +import { respond } from "../../backend/http.mjs"; +import { mongoPaintingWips, pruneEmptyPaintingWips } from "../../backend/painting-wips.mjs"; + +const headers = { "Cache-Control": "no-store", "Access-Control-Allow-Methods": "GET, POST, OPTIONS" }; +let lastCleanup = 0; + +export async function handler(event) { + if (event.httpMethod === "OPTIONS") return respond(200, {}, headers); + if (!["GET", "POST"].includes(event.httpMethod)) return respond(405, { error: "Method not allowed" }, headers); + try { + let user = null; + if (event.headers?.authorization || event.headers?.Authorization) { + user = await authorize(event.headers); + if (!user) return respond(401, { error: "Sign in again to edit your painting" }, headers); + } + const { db } = await connect(); + const { service } = await mongoPaintingWips(db); + if (event.httpMethod === "GET") { + const query = event.queryStringParameters || {}; + return respond(200, await service.read(query.code, query.state === "1", user), headers); + } + let input; + try { input = JSON.parse(event.body || "{}"); } + catch { return respond(400, { error: "Invalid JSON" }, headers); } + let result; + if (input.action === "create") { + if (Date.now() - lastCleanup > 60000) { + lastCleanup = Date.now(); + await pruneEmptyPaintingWips(db); + } + result = await service.create(input, user); + } else if (input.action === "save") result = await service.save(input, user); + else if (input.action === "read") result = await service.read(input.code, Boolean(input.state), user, input.key); + else return respond(400, { error: "Unknown painting action" }, headers); + return respond(200, result, headers); + } catch (error) { + if (!error.status) console.error("Painting WIP failed:", error.message); + return respond(error.status || 500, { error: error.status ? error.message : "Could not save the painting" }, headers); + } +} diff --git a/system/netlify/functions/pixel.js b/system/netlify/functions/pixel.js --- a/system/netlify/functions/pixel.js +++ b/system/netlify/functions/pixel.js @@ -24,7 +24,8 @@ import { readFileSync, mkdirSync, copyFileSync, existsSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { execSync } from "child_process"; -import { respond } from "../../backend/http.mjs"; +import { respond } from "../../backend/http.mjs"; +import { paintingWipPixels } from "../../backend/painting-wips.mjs"; const dev = process.env.CONTEXT === "dev"; let nopaintArchiveIds; @@ -80,7 +81,8 @@ // TODO: Eventually use a "-clear" option to keep the backdrop transparent. // 23.09.06.02.27 const resolution = pre.split("x").map((n) => parseInt(n)); let slug = params.slice(1).join("/"); - let imageUrl; // Declare imageUrl at function scope + let imageUrl; // Declare imageUrl at function scope + let wipImage; // Check for QR code parameter (for print files that need QR baked in) // Usage: /api/pixel/2700x1050:contain-clear/CODE.png?qr=mug~+CODE&via=kidlispcode @@ -140,7 +142,10 @@ const { connect } = await import("../../backend/database.mjs"); const database = await connect(); const painting = await database.db.collection('paintings').findOne({ code }); - if (painting) { + if (painting?.status === "wip") { + const picture = await paintingWipPixels(database.db, painting); + wipImage = await sharp(picture.pixels, { raw: { width: picture.width, height: picture.height, channels: 4 } }).png().toBuffer(); + } else if (painting) { // Build slug from painting data // Handle combined slugs (split to get image slug only) let imageSlug = painting.slug; @@ -164,8 +169,9 @@ console.log(`📦 No DB record for ${code}, trying guest bucket: ${imageUrl}`); } await database.disconnect(); - } catch (error) { - console.error(`❌ Error looking up painting code: ${error.message}`); + } catch (error) { + if (error.status === 404) return respond(404, { message: "Painting not found" }); + console.error(`❌ Error looking up painting code: ${error.message}`); // Fall back to guest bucket on DB error too imageUrl = `https://art-aesthetic-computer.sfo3.digitaloceanspaces.com/${code}.png`; console.log(`📦 DB error, trying guest bucket: ${imageUrl}`); @@ -173,15 +179,15 @@ } } // Fall back to constructing URL from slug if not set by code lookup - if (!imageUrl) { + if (!imageUrl && !wipImage) { imageUrl = `https://${event.headers["host"]}/media/${slug}`; } - if (!imageUrl) return respond(400, { message: "Image URL not provided." }); + if (!imageUrl && !wipImage) return respond(400, { message: "Image URL not provided." }); try { const { got } = await import("got"); - const response = await got(imageUrl, { + const response = wipImage ? { body: wipImage } : await got(imageUrl, { responseType: "buffer", https: { rejectUnauthorized: !dev, @@ -520,7 +526,8 @@ return { statusCode: 200, headers: { "Content-Type": "image/png", - "Content-Length": buffer.length.toString(), + "Content-Length": buffer.length.toString(), + ...(wipImage ? { "Cache-Control": "no-store" } : {}), }, body: buffer.toString("base64"), ttl: 60, diff --git a/system/netlify/functions/track-media-stream.mjs b/system/netlify/functions/track-media-stream.mjs --- a/system/netlify/functions/track-media-stream.mjs +++ b/system/netlify/functions/track-media-stream.mjs @@ -12,6 +12,7 @@ import { generateUniqueCode } from "../../backend/generate-short-code.mjs"; import { createMediaRecord, MediaTypes } from "../../backend/media-atproto.mjs"; import { S3Client, PutObjectAclCommand } from "@aws-sdk/client-s3"; import { stream } from "@netlify/functions"; +import { sealPaintingWip } from "../../backend/seal-painting-wip.mjs"; const MAX_TAPE_DURATION = 30; const dev = process.env.CONTEXT === "dev"; @@ -81,6 +82,10 @@ } await send("progress", { stage: "database", message: "Connecting to database..." }); database = await connect(); + if (body.ext === "png" && body.wip) { + await send("complete", await sealPaintingWip(database, body, user)); + return; + } let type; let metadata; diff --git a/system/netlify/functions/track-media.mjs b/system/netlify/functions/track-media.mjs --- a/system/netlify/functions/track-media.mjs +++ b/system/netlify/functions/track-media.mjs @@ -12,6 +12,7 @@ import { authorize, getHandleOrEmail } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; import { respond } from "../../backend/http.mjs"; import { generateUniqueCode } from "../../backend/generate-short-code.mjs"; +import { sealPaintingWip } from "../../backend/seal-painting-wip.mjs"; import { createMediaRecord, deleteMediaRecord, MediaTypes } from "../../backend/media-atproto.mjs"; import { S3Client, PutObjectAclCommand } from "@aws-sdk/client-s3"; import { publishProfileEvent } from "../../backend/profile-stream.mjs"; @@ -67,6 +68,10 @@ } } const database = await connect(); + if (event.httpMethod === "POST" && body.ext === "png" && body.wip) { + try { return respond(200, await sealPaintingWip(database, body, user)); } + catch (error) { return respond(error.status || 500, { error: error.message }); } + } let type, metadata; if (body.ext === "png") { diff --git a/system/netlify/functions/tv.mjs b/system/netlify/functions/tv.mjs --- a/system/netlify/functions/tv.mjs +++ b/system/netlify/functions/tv.mjs @@ -38,7 +38,7 @@ async function fetchPaintings(db, { limit }) { const collection = db.collection("paintings"); const pipeline = [ - { $match: { nuked: { $ne: true } } }, + { $match: { nuked: { $ne: true }, status: { $ne: "wip" } } }, { $sort: { when: -1 } }, { $limit: limit }, { diff --git a/system/netlify/functions/update-painting-slug.mjs b/system/netlify/functions/update-painting-slug.mjs --- a/system/netlify/functions/update-painting-slug.mjs +++ b/system/netlify/functions/update-painting-slug.mjs @@ -34,7 +34,7 @@ const collection = database.db.collection("paintings"); // Find the painting by oldSlug (anonymous paintings have no user field) const result = await collection.updateOne( - { slug: oldSlug, user: null }, + { slug: oldSlug, user: null, "wip.id": { $exists: false } }, { $set: { slug: newSlug } } ); 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 @@ -15432,9 +15432,9 @@ } if (type === "upload") { // Extract recordingSlug if present in content - const { recordingSlug, ...uploadData } = content; + const { recordingSlug, metadata, ...uploadData } = content; console.log("🔍 UPLOAD MESSAGE HANDLER: recordingSlug=", recordingSlug, "content keys=", Object.keys(content)); - receivedUpload(uploadData, "upload", null, recordingSlug); + receivedUpload(uploadData, "upload", metadata, recordingSlug); return; } @@ -21449,6 +21449,7 @@ } const options = { method: "POST", headers }; const body = { slug, ext }; + if (ext === "png" && metadata?.paintingWip) body.wip = metadata.paintingWip; console.log(`🔍 PRE-CHECK: recordingSlug=${recordingSlug}, userMedia=${userMedia}, ext=${ext}`); diff --git a/system/public/aesthetic.computer/disks/nopaint.mjs b/system/public/aesthetic.computer/disks/nopaint.mjs --- a/system/public/aesthetic.computer/disks/nopaint.mjs +++ b/system/public/aesthetic.computer/disks/nopaint.mjs @@ -42,7 +42,10 @@ recoverNoPaintPiece, reconcileNoPaintPiece, } from "../lib/nopaint-pieces.mjs"; import { createNoPaintRecording } from "../lib/nopaint-recording.mjs"; +import { decodePaintingState } from "../lib/painting-state.mjs"; +import { PaintingWipSync, paintingWipEditor, readPaintingWip, loadPaintingWipEditors } from "../lib/painting-wip.mjs"; import { timestamp } from "../lib/num.mjs"; +import { buttonLabelSize, paintDecisionButton } from "../lib/nopaint-buttons.mjs"; // Keep this iteration focused on Line. Other recovered brushes remain // available as pieces and can rejoin the conductor in a later pass. @@ -90,6 +93,7 @@ let decisions = []; let saveCount = 0; let lastDownload = null; let testApi = null; +let wipSync = null; let testChannel = null; let archiveOrigin = null; let paintingResolution = null; @@ -385,47 +389,6 @@ }); } } -// Rasterize the shared AC letters once, then scale their pixels and strokes -// together. Both buttons use the same whole-pixel scale. -const buttonLabelBitmaps = new Map(); - -function buttonLabelBitmap($, label) { - const key = `${$.typeface.name}:${label}`; - if (!buttonLabelBitmaps.has(key)) { - if (![...label].every((letter) => $.typeface.glyphs[letter])) return null; - buttonLabelBitmaps.set(key, $.painting( - $.text.width(label) + 2, $.typeface.blockHeight + 2, - (p) => p.wipe(0, 0, 0, 0).ink(255).write(label, { x: 1, y: 1 }), - )); - } - return buttonLabelBitmaps.get(key); -} - -function buttonLabelSize($, button, label) { - return Math.max(1, Math.floor(Math.min( - button.box.h * 0.72 / ($.typeface.blockHeight + 2), - button.box.w * 0.94 / ($.text.width(label) + 2), - ))); -} - -function paintDecisionButton($, button, label, flavor = "no", labelSize) { - const active = button.down || button.over; - const fill = flavor === "paint" || flavor === "done" - ? active ? [18, 103, 46] : [26, 127, 58] - : flavor === "back" - ? active ? [146, 62, 6] : [175, 78, 10] - : active ? [155, 20, 34] : [185, 30, 43]; - $.ink(fill) - .box(button.box, "fill") - .ink(255) - .box(button.box, "outline"); - const bitmap = buttonLabelBitmap($, label); - if (!bitmap) return; - $.paste(bitmap, - Math.round(button.box.x + (button.box.w - bitmap.width * labelSize) / 2), - Math.round(button.box.y + (button.box.h - bitmap.height * labelSize) / 2), - labelSize); -} function paintOriginalCursor($) { if (!cursorSheet || !cursorPoint) return; @@ -619,6 +582,23 @@ function persistPiece({ store, system }) { store[NOPAINT_PIECE_STORE_KEY] = system.nopaint.piece; store.persist(NOPAINT_PIECE_STORE_KEY, "local:db"); + if (wipSync?.pieceId === system.nopaint.piece.id) wipSync.queue(system.nopaint.piece); +} + +function beginWip(api, restored) { + const changed = (changed) => { + if (wipSync !== changed) return; + testApi?.needsPaint(); + publishTestState(); + }; + const previous = api.system.nopaint.wipSync; + const sync = !restored && previous?.pieceId === api.system.nopaint.piece.id && previous.editor.status === "wip" + ? previous : new PaintingWipSync(api, api.system.nopaint.piece, changed, restored); + sync.api = api; + sync.onChange = changed; + wipSync = sync; + api.system.nopaint.wipSync = sync; + sync.queue(api.system.nopaint.piece); } const NOPAINT_MIN_SIZE = 8; @@ -822,6 +802,8 @@ code: completionCode, error: completionError, stayedInNoPaint: true, }, + wip: wipSync ? { code: wipSync.editor.code, revision: wipSync.editor.revision, + status: wipSync.editor.status, saving: wipSync.status, error: wipSync.error || null } : null, audio: { ready: [...cueSamples.keys()], brushReady: [...brushCueSamples.keys()], @@ -870,6 +852,7 @@ piece: piece ? { schema: piece.schema, version: piece.version, id: piece.id, + parent: piece.parent || null, layerCount: piece.layers.length, compositeFingerprint: paintingFingerprint(piece.composite), lastLayer: lastLayer ? { @@ -918,7 +901,37 @@ } } // 🥾 Boot -function boot({ colon, debug, hud, net, num, params, query = {}, screen, store, system, ui, ...api }) { +async function boot({ colon, debug, hud, net, num, params, query = {}, screen, store, system, ui, ...api }) { + wipSync = null; + await loadPaintingWipEditors(store); + let restoredEditor = null; + let startingPiece = null; + const sourceCode = ["resume", "from"].includes(params[0]) ? params[1] : null; + const resuming = params[0] === "resume"; + if (sourceCode) { + const context = { ...api, net, num, store, system, screen }; + try { + const loaded = await readPaintingWip(context, sourceCode); + if (resuming && (!loaded.canEdit || loaded.status !== "wip")) return api.jump(`wip~${sourceCode}`); + if (loaded.state) startingPiece = await decodePaintingState(loaded.state); + if (resuming) { + const { state, canEdit, ...editor } = loaded; + restoredEditor = { ...editor, key: paintingWipEditor(store, sourceCode)?.key }; + } + } catch (error) { + if (resuming || error.status !== 404) throw error; + // Finished paintings made before WIPs still make valid starting canvases. + const response = await fetch(`/api/painting-code?code=${encodeURIComponent(sourceCode)}`); + if (!response.ok) throw new Error("Starting painting not found"); + const metadata = await response.json(); + const loaded = await api.get.painting(sourceCode).by(metadata.handle || "anon"); + startingPiece = createNoPaintPiece({ seed: sourceCode, ...loaded.img, role: "fork" }); + } + if (!startingPiece) throw new Error("This painting has not saved its first canvas yet"); + const canvas = startingPiece.composite; + system.nopaint.replace(context, { ...canvas, pixels: new Uint8ClampedArray(canvas.pixels) }, "nopaint:start"); + system.nopaint.piece = startingPiece; + } // The runtime may rewrite the visible route before the piece boots. The // Navigation Timing entry retains the original tutorial/test URL. const navigationURL = initialNavigationURL(); @@ -954,6 +967,7 @@ Boolean(freshFromId) || Boolean(requestedSize) || (Object.hasOwn(query, "fresh") && !["0", "false", "no", "off"].includes(String(query.fresh).toLowerCase())); + if (sourceCode) freshStart = false; // Size tokens consume the params; seeds then come from the colon or query. const seedTokens = requestedSize ? [...colon] : [...colon, ...params]; const launchSeed = seedTokens.find((value) => @@ -966,6 +980,10 @@ sessionSeed = numericSeed || seedFrom( requestedSeed || `${num.timestamp()}-${num.randIntRange(0, 0x7fffffff)}`, ); random = seededRandom(sessionSeed); + if (startingPiece && !resuming) { + system.nopaint.piece = createNoPaintPiece({ seed: sessionSeed, ...startingPiece.composite, role: "fork" }); + system.nopaint.piece.parent = sourceCode; + } proposal = null; proposalFrame = 0; proposalNumber = 0; @@ -1035,6 +1053,14 @@ ); persistPiece({ store, system }); substrateFresh = false; } + const sealedEditor = paintingWipEditor(store, system.nopaint.piece.id); + if (!resuming && sealedEditor?.status === "done") { + sessionSeed = seedFrom(`${sessionSeed}:fork:${timestamp()}`); + random = seededRandom(sessionSeed); + initializePiece({ ...api, store, system }, sessionSeed, "fork"); + system.nopaint.piece.parent = sealedEditor.code; + persistPiece({ store, system }); + } store["painting:resolution-lock"] = true; store.persist("painting:resolution-lock", "local:db"); testApi = { ...api, hud, net, screen, store, system }; @@ -1101,7 +1127,8 @@ net.rewrite(archiveId ? `/nopaint~archive~${archiveId}` : `/nopaint:${sessionSeed}`); installTestHook(debug); chooseProposal(testApi); publishTestState(); - if (archiveId) loadArchivePainting(testApi, archiveId); + if (archiveId) await loadArchivePainting(testApi, archiveId); + beginWip(testApi, restoredEditor); } // 🧮 Sim @@ -1257,7 +1284,7 @@ function paintUploadProgress($, bar) { const margin = Math.max(12, Math.round(bar.w * 0.08)); const width = bar.w - margin * 2; const saving = completionPart === "image" && completionProgress >= 1; - const label = saving ? "Saving..." + const label = completionPart === "draft" ? "Saving WIP..." : saving ? "Saving..." : `Uploading ${completionPart === "steps" ? "steps " : ""}${Math.round(completionProgress * 100)}%`; const size = Math.max(1, Math.floor(Math.min(bar.h / 32, width / (label.length * 8)))); $.ink(255).write(label, { x: margin, y: bar.y + Math.round(bar.h * 0.2), size }); @@ -1309,14 +1336,15 @@ if (!showingSavedPainting) { $.ink(10, 10, 12, 210).box(0, 0, surface.w, merryBarHeight); $.ink(92, 220, 128, 235).box(0, 0, Math.round(surface.w * merryRemaining), merryBarHeight); } - const definition = proposalDefinition(proposal.kind); $.ink(18).box(bar, "fill"); if (completionBusy) { paintUploadProgress($, bar); return false; } - $.ink(255, 180).write(completionCode ? `Saved #${completionCode}` - : completionError || definition?.label || proposal.kind, + $.ink(255).write(completionCode ? `Done #${completionCode}` + : completionError || wipSync?.error || (wipSync?.editor.code + ? `WIP #${wipSync.editor.code}${wipSync.status === "saving" ? " Saving..." : ""}` + : "Starting WIP..."), { x: 8, y: merryBarHeight + 6 }); positionButtons($.screen); @@ -1351,6 +1379,9 @@ $.needsPaint(); publishTestState(); try { + completionPart = "draft"; + await wipSync.flush($.system.nopaint.piece); + completionPart = "steps"; const record = preserveRecording($); const reportProgress = (progress) => { completionProgress = Math.max(0, Math.min(1, Number(progress) || 0)); @@ -1370,9 +1401,11 @@ }; // Authenticated AC paintings pair the PNG and ZIP by this timestamp; // anonymous paintings link the separately assigned storage slugs. const filename = `painting-${record.at(-1).timestamp}.png`; - const data = await $.upload(filename, painting, reportProgress, undefined, zipped.slug); + const data = await $.upload(filename, painting, reportProgress, undefined, zipped.slug, + { paintingWip: wipSync.reference() }); if (!data?.code) throw new Error("Painting upload completed without a code"); completionCode = data.code; + wipSync.sealed(); completionProgress = 1; } catch (error) { completionError = error?.message || "Upload failed"; @@ -1414,6 +1447,7 @@ sessionSeed = seedFrom(`${sessionSeed}:${doneCount}:${$.num.timestamp()}`); random = seededRandom(sessionSeed); cutFreshSubstrate($, sessionSeed); initializePiece($, sessionSeed); + beginWip($); delete $.store["painting:code"]; $.store.delete?.("painting:code", "local:db"); archiveOrigin = null; @@ -1712,6 +1746,7 @@ function leave($) { // AC brushes append their next strokes to this same recording. Keeping the // last accepted frame supplies the boundary for the next No Paint visit. if ($?.system?.nopaint?.piece) preserveRecording($); + if (wipSync?.editor.status === "wip") wipSync.flush($.system.nopaint.piece).catch(() => {}); stopBrushCue(); testApi?.cursor?.("native"); if (typeof window !== "undefined") delete window.__acNoPaintTest; diff --git a/system/public/aesthetic.computer/disks/painting.mjs b/system/public/aesthetic.computer/disks/painting.mjs --- a/system/public/aesthetic.computer/disks/painting.mjs +++ b/system/public/aesthetic.computer/disks/painting.mjs @@ -38,6 +38,7 @@ let printBtn, // Sticker button. downloadBtn, // Download button. slug; // A url to the loaded image for printing. let discussBtn; +let forkBtn; let menuBtn; // A context (...) button that appears for the owner. let nukeBtn; // A button inside of the context menu to hide / delete the media. let menuOpen = false; @@ -91,12 +92,14 @@ query, hash, handle: getHandle, dom: { html }, - send, - store, + send, + store, + jump: apiJump, }) { showMode = colon[0] === "show"; // A special lightbox mode with no bottom bar. menuBtn = null; discussBtn = null; + forkBtn = null; paintingCode = undefined; nukeBtn = null; menuOpen = false; @@ -285,7 +288,8 @@ if (!response.ok) { console.warn(`⚠️ Painting code lookup failed: #${normalized}`, response.status); return null; } - const data = await response.json(); + const data = await response.json(); + if (data?.status === "wip") return data; if (data?.slug && data?.handle) { return { slug: data.slug, @@ -305,7 +309,11 @@ return null; } - async function loadPaintingFromMetadata(record, { aliasCodes = [] } = {}) { + async function loadPaintingFromMetadata(record, { aliasCodes = [] } = {}) { + if (record?.status === "wip") { + apiJump(`wip~${record.code}`); + return; + } if (!record || !record.slug || !record.handle) { console.error("❌ Invalid painting metadata", record); return; @@ -597,8 +605,12 @@ if (paintingCode && !isNuked && !showMode) { if (!discussBtn) discussBtn = new ui.TextButton("Comment", { center: "x", bottom: 6, screen }); discussBtn.reposition({ center: "x", bottom: 6, screen }); discussBtn.paint({ ink }); + if (!forkBtn) forkBtn = new ui.TextButton("Paint with", { center: "x", bottom: btnBar + 7, screen }); + forkBtn.reposition({ center: "x", bottom: btnBar + 7, screen }); + forkBtn.paint({ ink }); } else { discussBtn = null; + forkBtn = null; } //mintBtn?.paint({ ink }); printBtn?.reposition({ right: 6, bottom: 6, screen }); @@ -812,8 +824,12 @@ net, notice, user, canShare, - download, -}) { + download, + jump, +}) { + let forking = false; + forkBtn?.act(e, () => { forking = true; jump(`nopaint~from~${paintingCode}`); }); + if (forking || forkBtn?.down) return; menuBtn?.act(e, () => (menuOpen = !menuOpen)); if (!menuOpen) { diff --git a/system/public/aesthetic.computer/disks/prompt.mjs b/system/public/aesthetic.computer/disks/prompt.mjs --- a/system/public/aesthetic.computer/disks/prompt.mjs +++ b/system/public/aesthetic.computer/disks/prompt.mjs @@ -2524,6 +2524,19 @@ if (destination === "u" || slug === "yes!") destination = "upload"; // ^ "yes!" is always an upload. let filename; // Used in painting upload. let recordingSlug; + let paintingWip; + try { + if (destination === "upload") { + setProgressPhase("SAVING WIP"); + paintingWip = await system.nopaint.syncWip($); + if (paintingWip?.editor.status === "done") { + paintingCompletionBusy = false; + send({ type: "keyboard:unlock" }); + jump(`painting~${paintingWip.editor.code}`); + return true; + } + await paintingWip?.flush(system.nopaint.piece); + } if (system.nopaint.recording) { console.log("🖌️ Saving recording:", destination); @@ -2565,6 +2578,18 @@ flashColor = [255, 0, 0]; console.warn("🖌️ No recording to save!"); } + } catch (err) { + console.error("Painting preparation failed:", err); + notice(err.message || "Could not save the painting. Try Done again.", ["red"]); + progressTrick = null; + progressBar = -1; + setProgressPhase(); + progressPercentage = 0; + paintingCompletionBusy = false; + send({ type: "keyboard:unlock" }); + return true; + } + // Always upload a PNG. if (destination === "upload") { console.log("🖼️ Uploading painting..."); @@ -2588,7 +2613,8 @@ progressBar = -2; // Special value for pulsing animation progressPercentage = -1; // Hide percentage } needsPaint(); // Update display during upload - }, undefined, recordingSlug); // Pass bucket as undefined (use auth), recordingSlug as 5th param + }, undefined, recordingSlug, paintingWip ? { paintingWip: paintingWip.reference() } : undefined); + paintingWip?.sealed(); console.log("🪄 Painting uploaded:", filename, data); if (store["painting:tags"]) { delete store["painting:tags"]; @@ -2625,6 +2651,8 @@ makeFlash($); return true; // Prevent default - we handled the upload } catch (err) { console.error("🪄 Painting upload failed:", err); + send({ type: "keyboard:unlock" }); + notice(err.message || "Upload failed. Try Done again.", ["red"]); flashColor = [255, 0, 0]; progressBar = -1; setProgressPhase(); diff --git a/system/public/aesthetic.computer/disks/wip.mjs b/system/public/aesthetic.computer/disks/wip.mjs new file mode 100644 --- /dev/null +++ b/system/public/aesthetic.computer/disks/wip.mjs @@ -0,0 +1,67 @@ +// WIP, 26.09.15 +// Watch a painting in progress, continue your own, or start a new one from it. +import { readPaintingWip } from "../lib/painting-wip.mjs"; +import { decodePaintingState } from "../lib/painting-state.mjs"; +import { buttonLabelSize, paintDecisionButton } from "../lib/nopaint-buttons.mjs"; + +let code, painting, state, error, timer, active, edit, fork, testChannel; + +function boot($) { + code = $.params[0]; + painting = state = error = null; + active = true; + if ($.debug && typeof BroadcastChannel !== "undefined") testChannel = new BroadcastChannel("ac-nopaint-test"); + edit = new $.ui.Button(); + fork = new $.ui.Button(); + $.hud.label(" ", [0, 0, 0, 0]); + $.net.rewrite(`/#${code}`); + async function refresh() { + try { + const next = await readPaintingWip($, code, false); + if (!active) return; + if (next.status === "done") { $.jump(`painting~${code}`); return; } + if (!painting || next.revision !== state?.revision) { + const full = await readPaintingWip($, code, true); + if (!active) return; + if (full.state) painting = (await decodePaintingState(full.state)).composite; + } + state = next; + error = null; + } catch (failure) { if (active) error = failure.message; } + if (active) { $.needsPaint(); timer = setTimeout(refresh, 2000); } + } + refresh(); +} + +function paint($) { + $.wipe(18); + const barHeight = Math.max(56, Math.floor($.screen.height * 0.2)); + const barY = $.screen.height - barHeight; + if (painting) { + const scale = Math.min($.screen.width / painting.width, (barY - 22) / painting.height); + $.paste(painting, Math.floor(($.screen.width - painting.width * scale) / 2), + 22 + Math.floor((barY - 22 - painting.height * scale) / 2), scale); + } + $.ink(255).write(error || `WIP #${code}`, { x: 6, y: 6 }); + const canEdit = state?.canEdit; + const editWidth = canEdit ? Math.floor($.screen.width * 0.38) : 0; + Object.assign(edit, { box: new $.geo.Box(0, barY, editWidth, barHeight) }); + Object.assign(fork, { box: new $.geo.Box(canEdit ? editWidth + 4 : 0, barY, + $.screen.width - (canEdit ? editWidth + 4 : 0), barHeight) }); + const controls = [...(canEdit ? [[edit, "Edit", "back"]] : []), [fork, "Paint with", "paint"]]; + const size = Math.min(...controls.map(([button, label]) => buttonLabelSize($, button, label))); + for (const [button, label, flavor] of controls) paintDecisionButton($, button, label, flavor, size); + testChannel?.postMessage({ version: "wip", ready: Boolean(painting), code, + revision: state?.revision, canEdit: Boolean(canEdit), error, + controls: { ...(canEdit ? { edit: { ...edit.box } } : {}), fork: { ...fork.box } }, + layout: { screenResolution: { width: $.screen.width, height: $.screen.height } } }); + return true; +} + +function act($) { + if (state?.canEdit) edit.act($.event, () => $.jump(`nopaint~resume~${code}`)); + if (painting) fork.act($.event, () => $.jump(`nopaint~from~${code}`)); +} + +function leave() { active = false; clearTimeout(timer); testChannel?.close(); testChannel = null; } +export { boot, paint, act, leave }; diff --git a/system/public/aesthetic.computer/lib/disk-worker-manifest.json b/system/public/aesthetic.computer/lib/disk-worker-manifest.json --- a/system/public/aesthetic.computer/lib/disk-worker-manifest.json +++ b/system/public/aesthetic.computer/lib/disk-worker-manifest.json @@ -1,8 +1,8 @@ { - "filename": "disk.worker.a82ccefa80a7.mjs", - "sha256": "a82ccefa80a781e8ee222f6c3d077496574fb710ae99a8eb61a39e53fe491f6c", - "bytes": 1678923, - "sourceSha256": "6db150e5b5ed15d09135b312f6c43c783ff72b253508eafae5030ab20d4b8246", + "filename": "disk.worker.38fd5d036b2e.mjs", + "sha256": "38fd5d036b2e848918a48853d54150dec91ac486169dd16ade27c6cf2637a615", + "bytes": 1679870, + "sourceSha256": "37ba5251eda62f08c5adb5492e0875326aba3f4178dfb68107025c7d2c9dd0c8", "sources": [ "public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs", "public/aesthetic.computer/dep/gl-matrix/common.mjs", 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 @@ -1338,6 +1338,8 @@ // console.log("System painting:", system.painting); store.persist("painting", "local:db"); + system.nopaint.syncWip($).catch((error) => console.warn("Painting WIP:", error.message)); + // 🎨 Broadcast painting update to other tabs $commonApi.broadcastPaintingUpdate("updated", { source: "leave", @@ -1413,6 +1415,15 @@ }); } undoPosition = undoPaintings.length - 1; + if ($commonApi.system.nopaint.wipSync) { + clearTimeout($commonApi.system.nopaint.wipTimer); + const sync = $commonApi.system.nopaint.wipSync; + $commonApi.system.nopaint.wipTimer = setTimeout(() => { + if ($commonApi.system.nopaint.wipSync === sync) { + $commonApi.system.nopaint.syncWip().catch((error) => console.warn("Painting WIP:", error.message)); + } + }, 120); + } // Note: This could be extended to increase the size of the // undo stack, and images could be diffed? 23.01.31.01.30 @@ -3676,7 +3687,7 @@ }, // ***Actually*** upload a file to the server. // 📓 The file name can have `media-` which will sort it on the server into // a directory via `presigned-url.js`. - upload: async (filename, data, progress, bucket, recordingSlug) => { + upload: async (filename, data, progress, bucket, recordingSlug, metadata) => { const prom = new Promise((resolve, reject) => { serverUpload = { resolve, reject }; }); @@ -3685,7 +3696,7 @@ serverUploadProgressReporter?.(0); console.log("Uploading:", filename, { width: data?.width, height: data?.height }); - send({ type: "upload", content: { filename, data, bucket, recordingSlug } }); + send({ type: "upload", content: { filename, data, bucket, recordingSlug, metadata } }); return prom; }, code: { @@ -3899,6 +3910,11 @@ // act: nopaint_act, buffer: null, // An overlapping brush buffer that gets drawn on top of the // painting. piece: null, // Canonical code + pixel layer stack for No Paint paintings. + syncWip: async (api = cachedAPI) => { + if (!api) return null; + const { syncACPaintingWip } = await import("./painting-wip.mjs"); + return syncACPaintingWip(api); + }, recording: false, record: [], // Store a recording here. gestureRecord: [], // Store the active gesture. @@ -12025,7 +12041,7 @@ const filename = content.data.filename || ""; const ext = filename.split(".").pop(); // Only track paintings and pieces in database - if (ext === "png" || ext === "mjs" || ext === "lisp" || ext === "lua") { + if (!content.data.code && (ext === "png" || ext === "mjs" || ext === "lisp" || ext === "lua")) { try { // Call track-media POST to create database record with short code const trackResponse = await $commonApi.net.userRequest("POST", "/api/track-media", { @@ -14293,6 +14309,9 @@ }); if (system === "nopaint") nopaint_boot({ ...$api, params: $api.params, colon: $api.colon }); await boot($api); + if (system === "nopaint" && !/^nopaint(?:[:~]|$)/.test($api.slug || "")) { + sys.nopaint.syncWip($api).catch((error) => console.warn("Painting WIP:", error.message)); + } const bootEndTime = performance.now(); diskTimings.bootComplete = Math.round(bootEndTime - diskTimingStart); // Silent: boot() completed diff --git a/system/public/aesthetic.computer/lib/disk.worker.38fd5d036b2e.mjs b/system/public/aesthetic.computer/lib/disk.worker.38fd5d036b2e.mjs new file mode 100644 --- /dev/null +++ b/system/public/aesthetic.computer/lib/disk.worker.38fd5d036b2e.mjs @@ -0,0 +1,47920 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// public/aesthetic.computer/dep/gl-matrix/quat.mjs +var quat_exports = {}; +__export(quat_exports, { + add: () => add4, + calculateW: () => calculateW, + clone: () => clone4, + conjugate: () => conjugate, + copy: () => copy4, + create: () => create4, + dot: () => dot3, + equals: () => equals4, + exactEquals: () => exactEquals4, + exp: () => exp, + fromEuler: () => fromEuler, + fromMat3: () => fromMat3, + fromValues: () => fromValues4, + getAngle: () => getAngle, + getAxisAngle: () => getAxisAngle, + identity: () => identity2, + invert: () => invert2, + len: () => len3, + length: () => length3, + lerp: () => lerp3, + ln: () => ln, + mul: () => mul4, + multiply: () => multiply4, + normalize: () => normalize3, + pow: () => pow, + random: () => random3, + rotateX: () => rotateX2, + rotateY: () => rotateY2, + rotateZ: () => rotateZ2, + rotationTo: () => rotationTo, + scale: () => scale4, + set: () => set4, + setAxes: () => setAxes, + setAxisAngle: () => setAxisAngle, + slerp: () => slerp, + sqlerp: () => sqlerp, + sqrLen: () => sqrLen3, + squaredLength: () => squaredLength3, + str: () => str4 +}); + +// public/aesthetic.computer/dep/gl-matrix/common.mjs +var EPSILON = 1e-6; +var ARRAY_TYPE = typeof Float32Array !== "undefined" ? Float32Array : Array; +var RANDOM = Math.random; +var degree = Math.PI / 180; +if (!Math.hypot) Math.hypot = function() { + var y = 0, i2 = arguments.length; + while (i2--) { + y += arguments[i2] * arguments[i2]; + } + return Math.sqrt(y); +}; + +// public/aesthetic.computer/dep/gl-matrix/mat3.mjs +var mat3_exports = {}; +__export(mat3_exports, { + add: () => add, + adjoint: () => adjoint, + clone: () => clone, + copy: () => copy, + create: () => create, + determinant: () => determinant, + equals: () => equals, + exactEquals: () => exactEquals, + frob: () => frob, + fromMat2d: () => fromMat2d, + fromMat4: () => fromMat4, + fromQuat: () => fromQuat, + fromRotation: () => fromRotation, + fromScaling: () => fromScaling, + fromTranslation: () => fromTranslation, + fromValues: () => fromValues, + identity: () => identity, + invert: () => invert, + mul: () => mul, + multiply: () => multiply, + multiplyScalar: () => multiplyScalar, + multiplyScalarAndAdd: () => multiplyScalarAndAdd, + normalFromMat4: () => normalFromMat4, + projection: () => projection, + rotate: () => rotate, + scale: () => scale, + set: () => set, + str: () => str, + sub: () => sub, + subtract: () => subtract, + translate: () => translate, + transpose: () => transpose +}); +function create() { + var out = new ARRAY_TYPE(9); + if (ARRAY_TYPE != Float32Array) { + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[5] = 0; + out[6] = 0; + out[7] = 0; + } + out[0] = 1; + out[4] = 1; + out[8] = 1; + return out; +} +function fromMat4(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[4]; + out[4] = a2[5]; + out[5] = a2[6]; + out[6] = a2[8]; + out[7] = a2[9]; + out[8] = a2[10]; + return out; +} +function clone(a2) { + var out = new ARRAY_TYPE(9); + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + out[4] = a2[4]; + out[5] = a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[8] = a2[8]; + return out; +} +function copy(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + out[4] = a2[4]; + out[5] = a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[8] = a2[8]; + return out; +} +function fromValues(m00, m01, m02, m10, m11, m12, m20, m21, m22) { + var out = new ARRAY_TYPE(9); + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m10; + out[4] = m11; + out[5] = m12; + out[6] = m20; + out[7] = m21; + out[8] = m22; + return out; +} +function set(out, m00, m01, m02, m10, m11, m12, m20, m21, m22) { + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m10; + out[4] = m11; + out[5] = m12; + out[6] = m20; + out[7] = m21; + out[8] = m22; + return out; +} +function identity(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} +function transpose(out, a2) { + if (out === a2) { + var a01 = a2[1], a02 = a2[2], a12 = a2[5]; + out[1] = a2[3]; + out[2] = a2[6]; + out[3] = a01; + out[5] = a2[7]; + out[6] = a02; + out[7] = a12; + } else { + out[0] = a2[0]; + out[1] = a2[3]; + out[2] = a2[6]; + out[3] = a2[1]; + out[4] = a2[4]; + out[5] = a2[7]; + out[6] = a2[2]; + out[7] = a2[5]; + out[8] = a2[8]; + } + return out; +} +function invert(out, a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2]; + var a10 = a2[3], a11 = a2[4], a12 = a2[5]; + var a20 = a2[6], a21 = a2[7], a22 = a2[8]; + var b01 = a22 * a11 - a12 * a21; + var b11 = -a22 * a10 + a12 * a20; + var b21 = a21 * a10 - a11 * a20; + var det = a00 * b01 + a01 * b11 + a02 * b21; + if (!det) { + return null; + } + det = 1 / det; + out[0] = b01 * det; + out[1] = (-a22 * a01 + a02 * a21) * det; + out[2] = (a12 * a01 - a02 * a11) * det; + out[3] = b11 * det; + out[4] = (a22 * a00 - a02 * a20) * det; + out[5] = (-a12 * a00 + a02 * a10) * det; + out[6] = b21 * det; + out[7] = (-a21 * a00 + a01 * a20) * det; + out[8] = (a11 * a00 - a01 * a10) * det; + return out; +} +function adjoint(out, a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2]; + var a10 = a2[3], a11 = a2[4], a12 = a2[5]; + var a20 = a2[6], a21 = a2[7], a22 = a2[8]; + out[0] = a11 * a22 - a12 * a21; + out[1] = a02 * a21 - a01 * a22; + out[2] = a01 * a12 - a02 * a11; + out[3] = a12 * a20 - a10 * a22; + out[4] = a00 * a22 - a02 * a20; + out[5] = a02 * a10 - a00 * a12; + out[6] = a10 * a21 - a11 * a20; + out[7] = a01 * a20 - a00 * a21; + out[8] = a00 * a11 - a01 * a10; + return out; +} +function determinant(a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2]; + var a10 = a2[3], a11 = a2[4], a12 = a2[5]; + var a20 = a2[6], a21 = a2[7], a22 = a2[8]; + return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20); +} +function multiply(out, a2, b2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2]; + var a10 = a2[3], a11 = a2[4], a12 = a2[5]; + var a20 = a2[6], a21 = a2[7], a22 = a2[8]; + var b00 = b2[0], b01 = b2[1], b02 = b2[2]; + var b10 = b2[3], b11 = b2[4], b12 = b2[5]; + var b20 = b2[6], b21 = b2[7], b22 = b2[8]; + out[0] = b00 * a00 + b01 * a10 + b02 * a20; + out[1] = b00 * a01 + b01 * a11 + b02 * a21; + out[2] = b00 * a02 + b01 * a12 + b02 * a22; + out[3] = b10 * a00 + b11 * a10 + b12 * a20; + out[4] = b10 * a01 + b11 * a11 + b12 * a21; + out[5] = b10 * a02 + b11 * a12 + b12 * a22; + out[6] = b20 * a00 + b21 * a10 + b22 * a20; + out[7] = b20 * a01 + b21 * a11 + b22 * a21; + out[8] = b20 * a02 + b21 * a12 + b22 * a22; + return out; +} +function translate(out, a2, v2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a10 = a2[3], a11 = a2[4], a12 = a2[5], a20 = a2[6], a21 = a2[7], a22 = a2[8], x = v2[0], y = v2[1]; + out[0] = a00; + out[1] = a01; + out[2] = a02; + out[3] = a10; + out[4] = a11; + out[5] = a12; + out[6] = x * a00 + y * a10 + a20; + out[7] = x * a01 + y * a11 + a21; + out[8] = x * a02 + y * a12 + a22; + return out; +} +function rotate(out, a2, rad) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a10 = a2[3], a11 = a2[4], a12 = a2[5], a20 = a2[6], a21 = a2[7], a22 = a2[8], s2 = Math.sin(rad), c4 = Math.cos(rad); + out[0] = c4 * a00 + s2 * a10; + out[1] = c4 * a01 + s2 * a11; + out[2] = c4 * a02 + s2 * a12; + out[3] = c4 * a10 - s2 * a00; + out[4] = c4 * a11 - s2 * a01; + out[5] = c4 * a12 - s2 * a02; + out[6] = a20; + out[7] = a21; + out[8] = a22; + return out; +} +function scale(out, a2, v2) { + var x = v2[0], y = v2[1]; + out[0] = x * a2[0]; + out[1] = x * a2[1]; + out[2] = x * a2[2]; + out[3] = y * a2[3]; + out[4] = y * a2[4]; + out[5] = y * a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[8] = a2[8]; + return out; +} +function fromTranslation(out, v2) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = v2[0]; + out[7] = v2[1]; + out[8] = 1; + return out; +} +function fromRotation(out, rad) { + var s2 = Math.sin(rad), c4 = Math.cos(rad); + out[0] = c4; + out[1] = s2; + out[2] = 0; + out[3] = -s2; + out[4] = c4; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} +function fromScaling(out, v2) { + out[0] = v2[0]; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = v2[1]; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} +function fromMat2d(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = 0; + out[3] = a2[2]; + out[4] = a2[3]; + out[5] = 0; + out[6] = a2[4]; + out[7] = a2[5]; + out[8] = 1; + return out; +} +function fromQuat(out, q) { + var x = q[0], y = q[1], z = q[2], w = q[3]; + var x2 = x + x; + var y2 = y + y; + var z2 = z + z; + var xx = x * x2; + var yx = y * x2; + var yy = y * y2; + var zx = z * x2; + var zy = z * y2; + var zz = z * z2; + var wx = w * x2; + var wy = w * y2; + var wz = w * z2; + out[0] = 1 - yy - zz; + out[3] = yx - wz; + out[6] = zx + wy; + out[1] = yx + wz; + out[4] = 1 - xx - zz; + out[7] = zy - wx; + out[2] = zx - wy; + out[5] = zy + wx; + out[8] = 1 - xx - yy; + return out; +} +function normalFromMat4(out, a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a10 = a2[4], a11 = a2[5], a12 = a2[6], a13 = a2[7]; + var a20 = a2[8], a21 = a2[9], a22 = a2[10], a23 = a2[11]; + var a30 = a2[12], a31 = a2[13], a32 = a2[14], a33 = a2[15]; + var b00 = a00 * a11 - a01 * a10; + var b01 = a00 * a12 - a02 * a10; + var b02 = a00 * a13 - a03 * a10; + var b03 = a01 * a12 - a02 * a11; + var b04 = a01 * a13 - a03 * a11; + var b05 = a02 * a13 - a03 * a12; + var b06 = a20 * a31 - a21 * a30; + var b07 = a20 * a32 - a22 * a30; + var b08 = a20 * a33 - a23 * a30; + var b09 = a21 * a32 - a22 * a31; + var b10 = a21 * a33 - a23 * a31; + var b11 = a22 * a33 - a23 * a32; + var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + if (!det) { + return null; + } + det = 1 / det; + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + return out; +} +function projection(out, width2, height2) { + out[0] = 2 / width2; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = -2 / height2; + out[5] = 0; + out[6] = -1; + out[7] = 1; + out[8] = 1; + return out; +} +function str(a2) { + return "mat3(" + a2[0] + ", " + a2[1] + ", " + a2[2] + ", " + a2[3] + ", " + a2[4] + ", " + a2[5] + ", " + a2[6] + ", " + a2[7] + ", " + a2[8] + ")"; +} +function frob(a2) { + return Math.hypot(a2[0], a2[1], a2[2], a2[3], a2[4], a2[5], a2[6], a2[7], a2[8]); +} +function add(out, a2, b2) { + out[0] = a2[0] + b2[0]; + out[1] = a2[1] + b2[1]; + out[2] = a2[2] + b2[2]; + out[3] = a2[3] + b2[3]; + out[4] = a2[4] + b2[4]; + out[5] = a2[5] + b2[5]; + out[6] = a2[6] + b2[6]; + out[7] = a2[7] + b2[7]; + out[8] = a2[8] + b2[8]; + return out; +} +function subtract(out, a2, b2) { + out[0] = a2[0] - b2[0]; + out[1] = a2[1] - b2[1]; + out[2] = a2[2] - b2[2]; + out[3] = a2[3] - b2[3]; + out[4] = a2[4] - b2[4]; + out[5] = a2[5] - b2[5]; + out[6] = a2[6] - b2[6]; + out[7] = a2[7] - b2[7]; + out[8] = a2[8] - b2[8]; + return out; +} +function multiplyScalar(out, a2, b2) { + out[0] = a2[0] * b2; + out[1] = a2[1] * b2; + out[2] = a2[2] * b2; + out[3] = a2[3] * b2; + out[4] = a2[4] * b2; + out[5] = a2[5] * b2; + out[6] = a2[6] * b2; + out[7] = a2[7] * b2; + out[8] = a2[8] * b2; + return out; +} +function multiplyScalarAndAdd(out, a2, b2, scale7) { + out[0] = a2[0] + b2[0] * scale7; + out[1] = a2[1] + b2[1] * scale7; + out[2] = a2[2] + b2[2] * scale7; + out[3] = a2[3] + b2[3] * scale7; + out[4] = a2[4] + b2[4] * scale7; + out[5] = a2[5] + b2[5] * scale7; + out[6] = a2[6] + b2[6] * scale7; + out[7] = a2[7] + b2[7] * scale7; + out[8] = a2[8] + b2[8] * scale7; + return out; +} +function exactEquals(a2, b2) { + return a2[0] === b2[0] && a2[1] === b2[1] && a2[2] === b2[2] && a2[3] === b2[3] && a2[4] === b2[4] && a2[5] === b2[5] && a2[6] === b2[6] && a2[7] === b2[7] && a2[8] === b2[8]; +} +function equals(a2, b2) { + var a0 = a2[0], a1 = a2[1], a22 = a2[2], a3 = a2[3], a4 = a2[4], a5 = a2[5], a6 = a2[6], a7 = a2[7], a8 = a2[8]; + var b0 = b2[0], b1 = b2[1], b22 = b2[2], b3 = b2[3], b4 = b2[4], b5 = b2[5], b6 = b2[6], b7 = b2[7], b8 = b2[8]; + return Math.abs(a0 - b0) <= EPSILON * Math.max(1, Math.abs(a0), Math.abs(b0)) && Math.abs(a1 - b1) <= EPSILON * Math.max(1, Math.abs(a1), Math.abs(b1)) && Math.abs(a22 - b22) <= EPSILON * Math.max(1, Math.abs(a22), Math.abs(b22)) && Math.abs(a3 - b3) <= EPSILON * Math.max(1, Math.abs(a3), Math.abs(b3)) && Math.abs(a4 - b4) <= EPSILON * Math.max(1, Math.abs(a4), Math.abs(b4)) && Math.abs(a5 - b5) <= EPSILON * Math.max(1, Math.abs(a5), Math.abs(b5)) && Math.abs(a6 - b6) <= EPSILON * Math.max(1, Math.abs(a6), Math.abs(b6)) && Math.abs(a7 - b7) <= EPSILON * Math.max(1, Math.abs(a7), Math.abs(b7)) && Math.abs(a8 - b8) <= EPSILON * Math.max(1, Math.abs(a8), Math.abs(b8)); +} +var mul = multiply; +var sub = subtract; + +// public/aesthetic.computer/dep/gl-matrix/vec3.mjs +var vec3_exports = {}; +__export(vec3_exports, { + add: () => add2, + angle: () => angle, + bezier: () => bezier, + ceil: () => ceil, + clone: () => clone2, + copy: () => copy2, + create: () => create2, + cross: () => cross, + dist: () => dist, + distance: () => distance, + div: () => div, + divide: () => divide, + dot: () => dot, + equals: () => equals2, + exactEquals: () => exactEquals2, + floor: () => floor, + forEach: () => forEach, + fromValues: () => fromValues2, + hermite: () => hermite, + inverse: () => inverse, + len: () => len, + length: () => length, + lerp: () => lerp, + max: () => max, + min: () => min, + mul: () => mul2, + multiply: () => multiply2, + negate: () => negate, + normalize: () => normalize, + random: () => random, + rotateX: () => rotateX, + rotateY: () => rotateY, + rotateZ: () => rotateZ, + round: () => round, + scale: () => scale2, + scaleAndAdd: () => scaleAndAdd, + set: () => set2, + sqrDist: () => sqrDist, + sqrLen: () => sqrLen, + squaredDistance: () => squaredDistance, + squaredLength: () => squaredLength, + str: () => str2, + sub: () => sub2, + subtract: () => subtract2, + transformMat3: () => transformMat3, + transformMat4: () => transformMat4, + transformQuat: () => transformQuat, + zero: () => zero +}); +function create2() { + var out = new ARRAY_TYPE(3); + if (ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + } + return out; +} +function clone2(a2) { + var out = new ARRAY_TYPE(3); + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + return out; +} +function length(a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + return Math.hypot(x, y, z); +} +function fromValues2(x, y, z) { + var out = new ARRAY_TYPE(3); + out[0] = x; + out[1] = y; + out[2] = z; + return out; +} +function copy2(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + return out; +} +function set2(out, x, y, z) { + out[0] = x; + out[1] = y; + out[2] = z; + return out; +} +function add2(out, a2, b2) { + out[0] = a2[0] + b2[0]; + out[1] = a2[1] + b2[1]; + out[2] = a2[2] + b2[2]; + return out; +} +function subtract2(out, a2, b2) { + out[0] = a2[0] - b2[0]; + out[1] = a2[1] - b2[1]; + out[2] = a2[2] - b2[2]; + return out; +} +function multiply2(out, a2, b2) { + out[0] = a2[0] * b2[0]; + out[1] = a2[1] * b2[1]; + out[2] = a2[2] * b2[2]; + return out; +} +function divide(out, a2, b2) { + out[0] = a2[0] / b2[0]; + out[1] = a2[1] / b2[1]; + out[2] = a2[2] / b2[2]; + return out; +} +function ceil(out, a2) { + out[0] = Math.ceil(a2[0]); + out[1] = Math.ceil(a2[1]); + out[2] = Math.ceil(a2[2]); + return out; +} +function floor(out, a2) { + out[0] = Math.floor(a2[0]); + out[1] = Math.floor(a2[1]); + out[2] = Math.floor(a2[2]); + return out; +} +function min(out, a2, b2) { + out[0] = Math.min(a2[0], b2[0]); + out[1] = Math.min(a2[1], b2[1]); + out[2] = Math.min(a2[2], b2[2]); + return out; +} +function max(out, a2, b2) { + out[0] = Math.max(a2[0], b2[0]); + out[1] = Math.max(a2[1], b2[1]); + out[2] = Math.max(a2[2], b2[2]); + return out; +} +function round(out, a2) { + out[0] = Math.round(a2[0]); + out[1] = Math.round(a2[1]); + out[2] = Math.round(a2[2]); + return out; +} +function scale2(out, a2, b2) { + out[0] = a2[0] * b2; + out[1] = a2[1] * b2; + out[2] = a2[2] * b2; + return out; +} +function scaleAndAdd(out, a2, b2, scale7) { + out[0] = a2[0] + b2[0] * scale7; + out[1] = a2[1] + b2[1] * scale7; + out[2] = a2[2] + b2[2] * scale7; + return out; +} +function distance(a2, b2) { + var x = b2[0] - a2[0]; + var y = b2[1] - a2[1]; + var z = b2[2] - a2[2]; + return Math.hypot(x, y, z); +} +function squaredDistance(a2, b2) { + var x = b2[0] - a2[0]; + var y = b2[1] - a2[1]; + var z = b2[2] - a2[2]; + return x * x + y * y + z * z; +} +function squaredLength(a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + return x * x + y * y + z * z; +} +function negate(out, a2) { + out[0] = -a2[0]; + out[1] = -a2[1]; + out[2] = -a2[2]; + return out; +} +function inverse(out, a2) { + out[0] = 1 / a2[0]; + out[1] = 1 / a2[1]; + out[2] = 1 / a2[2]; + return out; +} +function normalize(out, a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + var len5 = x * x + y * y + z * z; + if (len5 > 0) { + len5 = 1 / Math.sqrt(len5); + } + out[0] = a2[0] * len5; + out[1] = a2[1] * len5; + out[2] = a2[2] * len5; + return out; +} +function dot(a2, b2) { + return a2[0] * b2[0] + a2[1] * b2[1] + a2[2] * b2[2]; +} +function cross(out, a2, b2) { + var ax = a2[0], ay = a2[1], az = a2[2]; + var bx = b2[0], by = b2[1], bz = b2[2]; + out[0] = ay * bz - az * by; + out[1] = az * bx - ax * bz; + out[2] = ax * by - ay * bx; + return out; +} +function lerp(out, a2, b2, t2) { + var ax = a2[0]; + var ay = a2[1]; + var az = a2[2]; + out[0] = ax + t2 * (b2[0] - ax); + out[1] = ay + t2 * (b2[1] - ay); + out[2] = az + t2 * (b2[2] - az); + return out; +} +function hermite(out, a2, b2, c4, d2, t2) { + var factorTimes2 = t2 * t2; + var factor1 = factorTimes2 * (2 * t2 - 3) + 1; + var factor2 = factorTimes2 * (t2 - 2) + t2; + var factor3 = factorTimes2 * (t2 - 1); + var factor4 = factorTimes2 * (3 - 2 * t2); + out[0] = a2[0] * factor1 + b2[0] * factor2 + c4[0] * factor3 + d2[0] * factor4; + out[1] = a2[1] * factor1 + b2[1] * factor2 + c4[1] * factor3 + d2[1] * factor4; + out[2] = a2[2] * factor1 + b2[2] * factor2 + c4[2] * factor3 + d2[2] * factor4; + return out; +} +function bezier(out, a2, b2, c4, d2, t2) { + var inverseFactor = 1 - t2; + var inverseFactorTimesTwo = inverseFactor * inverseFactor; + var factorTimes2 = t2 * t2; + var factor1 = inverseFactorTimesTwo * inverseFactor; + var factor2 = 3 * t2 * inverseFactorTimesTwo; + var factor3 = 3 * factorTimes2 * inverseFactor; + var factor4 = factorTimes2 * t2; + out[0] = a2[0] * factor1 + b2[0] * factor2 + c4[0] * factor3 + d2[0] * factor4; + out[1] = a2[1] * factor1 + b2[1] * factor2 + c4[1] * factor3 + d2[1] * factor4; + out[2] = a2[2] * factor1 + b2[2] * factor2 + c4[2] * factor3 + d2[2] * factor4; + return out; +} +function random(out, scale7) { + scale7 = scale7 || 1; + var r2 = RANDOM() * 2 * Math.PI; + var z = RANDOM() * 2 - 1; + var zScale = Math.sqrt(1 - z * z) * scale7; + out[0] = Math.cos(r2) * zScale; + out[1] = Math.sin(r2) * zScale; + out[2] = z * scale7; + return out; +} +function transformMat4(out, a2, m) { + var x = a2[0], y = a2[1], z = a2[2]; + var w = m[3] * x + m[7] * y + m[11] * z + m[15]; + w = w || 1; + out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w; + out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w; + out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w; + return out; +} +function transformMat3(out, a2, m) { + var x = a2[0], y = a2[1], z = a2[2]; + out[0] = x * m[0] + y * m[3] + z * m[6]; + out[1] = x * m[1] + y * m[4] + z * m[7]; + out[2] = x * m[2] + y * m[5] + z * m[8]; + return out; +} +function transformQuat(out, a2, q) { + var qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + var x = a2[0], y = a2[1], z = a2[2]; + var uvx = qy * z - qz * y, uvy = qz * x - qx * z, uvz = qx * y - qy * x; + var uuvx = qy * uvz - qz * uvy, uuvy = qz * uvx - qx * uvz, uuvz = qx * uvy - qy * uvx; + var w2 = qw * 2; + uvx *= w2; + uvy *= w2; + uvz *= w2; + uuvx *= 2; + uuvy *= 2; + uuvz *= 2; + out[0] = x + uvx + uuvx; + out[1] = y + uvy + uuvy; + out[2] = z + uvz + uuvz; + return out; +} +function rotateX(out, a2, b2, rad) { + var p = [], r2 = []; + p[0] = a2[0] - b2[0]; + p[1] = a2[1] - b2[1]; + p[2] = a2[2] - b2[2]; + r2[0] = p[0]; + r2[1] = p[1] * Math.cos(rad) - p[2] * Math.sin(rad); + r2[2] = p[1] * Math.sin(rad) + p[2] * Math.cos(rad); + out[0] = r2[0] + b2[0]; + out[1] = r2[1] + b2[1]; + out[2] = r2[2] + b2[2]; + return out; +} +function rotateY(out, a2, b2, rad) { + var p = [], r2 = []; + p[0] = a2[0] - b2[0]; + p[1] = a2[1] - b2[1]; + p[2] = a2[2] - b2[2]; + r2[0] = p[2] * Math.sin(rad) + p[0] * Math.cos(rad); + r2[1] = p[1]; + r2[2] = p[2] * Math.cos(rad) - p[0] * Math.sin(rad); + out[0] = r2[0] + b2[0]; + out[1] = r2[1] + b2[1]; + out[2] = r2[2] + b2[2]; + return out; +} +function rotateZ(out, a2, b2, rad) { + var p = [], r2 = []; + p[0] = a2[0] - b2[0]; + p[1] = a2[1] - b2[1]; + p[2] = a2[2] - b2[2]; + r2[0] = p[0] * Math.cos(rad) - p[1] * Math.sin(rad); + r2[1] = p[0] * Math.sin(rad) + p[1] * Math.cos(rad); + r2[2] = p[2]; + out[0] = r2[0] + b2[0]; + out[1] = r2[1] + b2[1]; + out[2] = r2[2] + b2[2]; + return out; +} +function angle(a2, b2) { + var ax = a2[0], ay = a2[1], az = a2[2], bx = b2[0], by = b2[1], bz = b2[2], mag1 = Math.sqrt(ax * ax + ay * ay + az * az), mag2 = Math.sqrt(bx * bx + by * by + bz * bz), mag = mag1 * mag2, cosine = mag && dot(a2, b2) / mag; + return Math.acos(Math.min(Math.max(cosine, -1), 1)); +} +function zero(out) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + return out; +} +function str2(a2) { + return "vec3(" + a2[0] + ", " + a2[1] + ", " + a2[2] + ")"; +} +function exactEquals2(a2, b2) { + return a2[0] === b2[0] && a2[1] === b2[1] && a2[2] === b2[2]; +} +function equals2(a2, b2) { + var a0 = a2[0], a1 = a2[1], a22 = a2[2]; + var b0 = b2[0], b1 = b2[1], b22 = b2[2]; + return Math.abs(a0 - b0) <= EPSILON * Math.max(1, Math.abs(a0), Math.abs(b0)) && Math.abs(a1 - b1) <= EPSILON * Math.max(1, Math.abs(a1), Math.abs(b1)) && Math.abs(a22 - b22) <= EPSILON * Math.max(1, Math.abs(a22), Math.abs(b22)); +} +var sub2 = subtract2; +var mul2 = multiply2; +var div = divide; +var dist = distance; +var sqrDist = squaredDistance; +var len = length; +var sqrLen = squaredLength; +var forEach = (function() { + var vec = create2(); + return function(a2, stride, offset, count, fn, arg) { + var i2, l2; + if (!stride) { + stride = 3; + } + if (!offset) { + offset = 0; + } + if (count) { + l2 = Math.min(count * stride + offset, a2.length); + } else { + l2 = a2.length; + } + for (i2 = offset; i2 < l2; i2 += stride) { + vec[0] = a2[i2]; + vec[1] = a2[i2 + 1]; + vec[2] = a2[i2 + 2]; + fn(vec, vec, arg); + a2[i2] = vec[0]; + a2[i2 + 1] = vec[1]; + a2[i2 + 2] = vec[2]; + } + return a2; + }; +})(); + +// public/aesthetic.computer/dep/gl-matrix/vec4.mjs +var vec4_exports = {}; +__export(vec4_exports, { + add: () => add3, + ceil: () => ceil2, + clone: () => clone3, + copy: () => copy3, + create: () => create3, + cross: () => cross2, + dist: () => dist2, + distance: () => distance2, + div: () => div2, + divide: () => divide2, + dot: () => dot2, + equals: () => equals3, + exactEquals: () => exactEquals3, + floor: () => floor2, + forEach: () => forEach2, + fromValues: () => fromValues3, + inverse: () => inverse2, + len: () => len2, + length: () => length2, + lerp: () => lerp2, + max: () => max2, + min: () => min2, + mul: () => mul3, + multiply: () => multiply3, + negate: () => negate2, + normalize: () => normalize2, + random: () => random2, + round: () => round2, + scale: () => scale3, + scaleAndAdd: () => scaleAndAdd2, + set: () => set3, + sqrDist: () => sqrDist2, + sqrLen: () => sqrLen2, + squaredDistance: () => squaredDistance2, + squaredLength: () => squaredLength2, + str: () => str3, + sub: () => sub3, + subtract: () => subtract3, + transformMat4: () => transformMat42, + transformQuat: () => transformQuat2, + zero: () => zero2 +}); +function create3() { + var out = new ARRAY_TYPE(4); + if (ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + } + return out; +} +function clone3(a2) { + var out = new ARRAY_TYPE(4); + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + return out; +} +function fromValues3(x, y, z, w) { + var out = new ARRAY_TYPE(4); + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +} +function copy3(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + return out; +} +function set3(out, x, y, z, w) { + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +} +function add3(out, a2, b2) { + out[0] = a2[0] + b2[0]; + out[1] = a2[1] + b2[1]; + out[2] = a2[2] + b2[2]; + out[3] = a2[3] + b2[3]; + return out; +} +function subtract3(out, a2, b2) { + out[0] = a2[0] - b2[0]; + out[1] = a2[1] - b2[1]; + out[2] = a2[2] - b2[2]; + out[3] = a2[3] - b2[3]; + return out; +} +function multiply3(out, a2, b2) { + out[0] = a2[0] * b2[0]; + out[1] = a2[1] * b2[1]; + out[2] = a2[2] * b2[2]; + out[3] = a2[3] * b2[3]; + return out; +} +function divide2(out, a2, b2) { + out[0] = a2[0] / b2[0]; + out[1] = a2[1] / b2[1]; + out[2] = a2[2] / b2[2]; + out[3] = a2[3] / b2[3]; + return out; +} +function ceil2(out, a2) { + out[0] = Math.ceil(a2[0]); + out[1] = Math.ceil(a2[1]); + out[2] = Math.ceil(a2[2]); + out[3] = Math.ceil(a2[3]); + return out; +} +function floor2(out, a2) { + out[0] = Math.floor(a2[0]); + out[1] = Math.floor(a2[1]); + out[2] = Math.floor(a2[2]); + out[3] = Math.floor(a2[3]); + return out; +} +function min2(out, a2, b2) { + out[0] = Math.min(a2[0], b2[0]); + out[1] = Math.min(a2[1], b2[1]); + out[2] = Math.min(a2[2], b2[2]); + out[3] = Math.min(a2[3], b2[3]); + return out; +} +function max2(out, a2, b2) { + out[0] = Math.max(a2[0], b2[0]); + out[1] = Math.max(a2[1], b2[1]); + out[2] = Math.max(a2[2], b2[2]); + out[3] = Math.max(a2[3], b2[3]); + return out; +} +function round2(out, a2) { + out[0] = Math.round(a2[0]); + out[1] = Math.round(a2[1]); + out[2] = Math.round(a2[2]); + out[3] = Math.round(a2[3]); + return out; +} +function scale3(out, a2, b2) { + out[0] = a2[0] * b2; + out[1] = a2[1] * b2; + out[2] = a2[2] * b2; + out[3] = a2[3] * b2; + return out; +} +function scaleAndAdd2(out, a2, b2, scale7) { + out[0] = a2[0] + b2[0] * scale7; + out[1] = a2[1] + b2[1] * scale7; + out[2] = a2[2] + b2[2] * scale7; + out[3] = a2[3] + b2[3] * scale7; + return out; +} +function distance2(a2, b2) { + var x = b2[0] - a2[0]; + var y = b2[1] - a2[1]; + var z = b2[2] - a2[2]; + var w = b2[3] - a2[3]; + return Math.hypot(x, y, z, w); +} +function squaredDistance2(a2, b2) { + var x = b2[0] - a2[0]; + var y = b2[1] - a2[1]; + var z = b2[2] - a2[2]; + var w = b2[3] - a2[3]; + return x * x + y * y + z * z + w * w; +} +function length2(a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + var w = a2[3]; + return Math.hypot(x, y, z, w); +} +function squaredLength2(a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + var w = a2[3]; + return x * x + y * y + z * z + w * w; +} +function negate2(out, a2) { + out[0] = -a2[0]; + out[1] = -a2[1]; + out[2] = -a2[2]; + out[3] = -a2[3]; + return out; +} +function inverse2(out, a2) { + out[0] = 1 / a2[0]; + out[1] = 1 / a2[1]; + out[2] = 1 / a2[2]; + out[3] = 1 / a2[3]; + return out; +} +function normalize2(out, a2) { + var x = a2[0]; + var y = a2[1]; + var z = a2[2]; + var w = a2[3]; + var len5 = x * x + y * y + z * z + w * w; + if (len5 > 0) { + len5 = 1 / Math.sqrt(len5); + } + out[0] = x * len5; + out[1] = y * len5; + out[2] = z * len5; + out[3] = w * len5; + return out; +} +function dot2(a2, b2) { + return a2[0] * b2[0] + a2[1] * b2[1] + a2[2] * b2[2] + a2[3] * b2[3]; +} +function cross2(out, u2, v2, w) { + var A = v2[0] * w[1] - v2[1] * w[0], B = v2[0] * w[2] - v2[2] * w[0], C = v2[0] * w[3] - v2[3] * w[0], D2 = v2[1] * w[2] - v2[2] * w[1], E = v2[1] * w[3] - v2[3] * w[1], F = v2[2] * w[3] - v2[3] * w[2]; + var G = u2[0]; + var H = u2[1]; + var I2 = u2[2]; + var J = u2[3]; + out[0] = H * F - I2 * E + J * D2; + out[1] = -(G * F) + I2 * C - J * B; + out[2] = G * E - H * C + J * A; + out[3] = -(G * D2) + H * B - I2 * A; + return out; +} +function lerp2(out, a2, b2, t2) { + var ax = a2[0]; + var ay = a2[1]; + var az = a2[2]; + var aw = a2[3]; + out[0] = ax + t2 * (b2[0] - ax); + out[1] = ay + t2 * (b2[1] - ay); + out[2] = az + t2 * (b2[2] - az); + out[3] = aw + t2 * (b2[3] - aw); + return out; +} +function random2(out, scale7) { + scale7 = scale7 || 1; + var v1, v2, v3, v4; + var s1, s2; + do { + v1 = RANDOM() * 2 - 1; + v2 = RANDOM() * 2 - 1; + s1 = v1 * v1 + v2 * v2; + } while (s1 >= 1); + do { + v3 = RANDOM() * 2 - 1; + v4 = RANDOM() * 2 - 1; + s2 = v3 * v3 + v4 * v4; + } while (s2 >= 1); + var d2 = Math.sqrt((1 - s1) / s2); + out[0] = scale7 * v1; + out[1] = scale7 * v2; + out[2] = scale7 * v3 * d2; + out[3] = scale7 * v4 * d2; + return out; +} +function transformMat42(out, a2, m) { + var x = a2[0], y = a2[1], z = a2[2], w = a2[3]; + out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; + out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; + out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; + out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; + return out; +} +function transformQuat2(out, a2, q) { + var x = a2[0], y = a2[1], z = a2[2]; + var qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + var ix = qw * x + qy * z - qz * y; + var iy = qw * y + qz * x - qx * z; + var iz = qw * z + qx * y - qy * x; + var iw = -qx * x - qy * y - qz * z; + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; + out[3] = a2[3]; + return out; +} +function zero2(out) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + return out; +} +function str3(a2) { + return "vec4(" + a2[0] + ", " + a2[1] + ", " + a2[2] + ", " + a2[3] + ")"; +} +function exactEquals3(a2, b2) { + return a2[0] === b2[0] && a2[1] === b2[1] && a2[2] === b2[2] && a2[3] === b2[3]; +} +function equals3(a2, b2) { + var a0 = a2[0], a1 = a2[1], a22 = a2[2], a3 = a2[3]; + var b0 = b2[0], b1 = b2[1], b22 = b2[2], b3 = b2[3]; + return Math.abs(a0 - b0) <= EPSILON * Math.max(1, Math.abs(a0), Math.abs(b0)) && Math.abs(a1 - b1) <= EPSILON * Math.max(1, Math.abs(a1), Math.abs(b1)) && Math.abs(a22 - b22) <= EPSILON * Math.max(1, Math.abs(a22), Math.abs(b22)) && Math.abs(a3 - b3) <= EPSILON * Math.max(1, Math.abs(a3), Math.abs(b3)); +} +var sub3 = subtract3; +var mul3 = multiply3; +var div2 = divide2; +var dist2 = distance2; +var sqrDist2 = squaredDistance2; +var len2 = length2; +var sqrLen2 = squaredLength2; +var forEach2 = (function() { + var vec = create3(); + return function(a2, stride, offset, count, fn, arg) { + var i2, l2; + if (!stride) { + stride = 4; + } + if (!offset) { + offset = 0; + } + if (count) { + l2 = Math.min(count * stride + offset, a2.length); + } else { + l2 = a2.length; + } + for (i2 = offset; i2 < l2; i2 += stride) { + vec[0] = a2[i2]; + vec[1] = a2[i2 + 1]; + vec[2] = a2[i2 + 2]; + vec[3] = a2[i2 + 3]; + fn(vec, vec, arg); + a2[i2] = vec[0]; + a2[i2 + 1] = vec[1]; + a2[i2 + 2] = vec[2]; + a2[i2 + 3] = vec[3]; + } + return a2; + }; +})(); + +// public/aesthetic.computer/dep/gl-matrix/quat.mjs +function create4() { + var out = new ARRAY_TYPE(4); + if (ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + } + out[3] = 1; + return out; +} +function identity2(out) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +} +function setAxisAngle(out, axis, rad) { + rad = rad * 0.5; + var s2 = Math.sin(rad); + out[0] = s2 * axis[0]; + out[1] = s2 * axis[1]; + out[2] = s2 * axis[2]; + out[3] = Math.cos(rad); + return out; +} +function getAxisAngle(out_axis, q) { + var rad = Math.acos(q[3]) * 2; + var s2 = Math.sin(rad / 2); + if (s2 > EPSILON) { + out_axis[0] = q[0] / s2; + out_axis[1] = q[1] / s2; + out_axis[2] = q[2] / s2; + } else { + out_axis[0] = 1; + out_axis[1] = 0; + out_axis[2] = 0; + } + return rad; +} +function getAngle(a2, b2) { + var dotproduct = dot3(a2, b2); + return Math.acos(2 * dotproduct * dotproduct - 1); +} +function multiply4(out, a2, b2) { + var ax = a2[0], ay = a2[1], az = a2[2], aw = a2[3]; + var bx = b2[0], by = b2[1], bz = b2[2], bw = b2[3]; + out[0] = ax * bw + aw * bx + ay * bz - az * by; + out[1] = ay * bw + aw * by + az * bx - ax * bz; + out[2] = az * bw + aw * bz + ax * by - ay * bx; + out[3] = aw * bw - ax * bx - ay * by - az * bz; + return out; +} +function rotateX2(out, a2, rad) { + rad *= 0.5; + var ax = a2[0], ay = a2[1], az = a2[2], aw = a2[3]; + var bx = Math.sin(rad), bw = Math.cos(rad); + out[0] = ax * bw + aw * bx; + out[1] = ay * bw + az * bx; + out[2] = az * bw - ay * bx; + out[3] = aw * bw - ax * bx; + return out; +} +function rotateY2(out, a2, rad) { + rad *= 0.5; + var ax = a2[0], ay = a2[1], az = a2[2], aw = a2[3]; + var by = Math.sin(rad), bw = Math.cos(rad); + out[0] = ax * bw - az * by; + out[1] = ay * bw + aw * by; + out[2] = az * bw + ax * by; + out[3] = aw * bw - ay * by; + return out; +} +function rotateZ2(out, a2, rad) { + rad *= 0.5; + var ax = a2[0], ay = a2[1], az = a2[2], aw = a2[3]; + var bz = Math.sin(rad), bw = Math.cos(rad); + out[0] = ax * bw + ay * bz; + out[1] = ay * bw - ax * bz; + out[2] = az * bw + aw * bz; + out[3] = aw * bw - az * bz; + return out; +} +function calculateW(out, a2) { + var x = a2[0], y = a2[1], z = a2[2]; + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = Math.sqrt(Math.abs(1 - x * x - y * y - z * z)); + return out; +} +function exp(out, a2) { + var x = a2[0], y = a2[1], z = a2[2], w = a2[3]; + var r2 = Math.sqrt(x * x + y * y + z * z); + var et = Math.exp(w); + var s2 = r2 > 0 ? et * Math.sin(r2) / r2 : 0; + out[0] = x * s2; + out[1] = y * s2; + out[2] = z * s2; + out[3] = et * Math.cos(r2); + return out; +} +function ln(out, a2) { + var x = a2[0], y = a2[1], z = a2[2], w = a2[3]; + var r2 = Math.sqrt(x * x + y * y + z * z); + var t2 = r2 > 0 ? Math.atan2(r2, w) / r2 : 0; + out[0] = x * t2; + out[1] = y * t2; + out[2] = z * t2; + out[3] = 0.5 * Math.log(x * x + y * y + z * z + w * w); + return out; +} +function pow(out, a2, b2) { + ln(out, a2); + scale4(out, out, b2); + exp(out, out); + return out; +} +function slerp(out, a2, b2, t2) { + var ax = a2[0], ay = a2[1], az = a2[2], aw = a2[3]; + var bx = b2[0], by = b2[1], bz = b2[2], bw = b2[3]; + var omega, cosom, sinom, scale0, scale1; + cosom = ax * bx + ay * by + az * bz + aw * bw; + if (cosom < 0) { + cosom = -cosom; + bx = -bx; + by = -by; + bz = -bz; + bw = -bw; + } + if (1 - cosom > EPSILON) { + omega = Math.acos(cosom); + sinom = Math.sin(omega); + scale0 = Math.sin((1 - t2) * omega) / sinom; + scale1 = Math.sin(t2 * omega) / sinom; + } else { + scale0 = 1 - t2; + scale1 = t2; + } + out[0] = scale0 * ax + scale1 * bx; + out[1] = scale0 * ay + scale1 * by; + out[2] = scale0 * az + scale1 * bz; + out[3] = scale0 * aw + scale1 * bw; + return out; +} +function random3(out) { + var u1 = RANDOM(); + var u2 = RANDOM(); + var u3 = RANDOM(); + var sqrt1MinusU1 = Math.sqrt(1 - u1); + var sqrtU1 = Math.sqrt(u1); + out[0] = sqrt1MinusU1 * Math.sin(2 * Math.PI * u2); + out[1] = sqrt1MinusU1 * Math.cos(2 * Math.PI * u2); + out[2] = sqrtU1 * Math.sin(2 * Math.PI * u3); + out[3] = sqrtU1 * Math.cos(2 * Math.PI * u3); + return out; +} +function invert2(out, a2) { + var a0 = a2[0], a1 = a2[1], a22 = a2[2], a3 = a2[3]; + var dot5 = a0 * a0 + a1 * a1 + a22 * a22 + a3 * a3; + var invDot = dot5 ? 1 / dot5 : 0; + out[0] = -a0 * invDot; + out[1] = -a1 * invDot; + out[2] = -a22 * invDot; + out[3] = a3 * invDot; + return out; +} +function conjugate(out, a2) { + out[0] = -a2[0]; + out[1] = -a2[1]; + out[2] = -a2[2]; + out[3] = a2[3]; + return out; +} +function fromMat3(out, m) { + var fTrace = m[0] + m[4] + m[8]; + var fRoot; + if (fTrace > 0) { + fRoot = Math.sqrt(fTrace + 1); + out[3] = 0.5 * fRoot; + fRoot = 0.5 / fRoot; + out[0] = (m[5] - m[7]) * fRoot; + out[1] = (m[6] - m[2]) * fRoot; + out[2] = (m[1] - m[3]) * fRoot; + } else { + var i2 = 0; + if (m[4] > m[0]) i2 = 1; + if (m[8] > m[i2 * 3 + i2]) i2 = 2; + var j = (i2 + 1) % 3; + var k = (i2 + 2) % 3; + fRoot = Math.sqrt(m[i2 * 3 + i2] - m[j * 3 + j] - m[k * 3 + k] + 1); + out[i2] = 0.5 * fRoot; + fRoot = 0.5 / fRoot; + out[3] = (m[j * 3 + k] - m[k * 3 + j]) * fRoot; + out[j] = (m[j * 3 + i2] + m[i2 * 3 + j]) * fRoot; + out[k] = (m[k * 3 + i2] + m[i2 * 3 + k]) * fRoot; + } + return out; +} +function fromEuler(out, x, y, z) { + var halfToRad = 0.5 * Math.PI / 180; + x *= halfToRad; + y *= halfToRad; + z *= halfToRad; + var sx = Math.sin(x); + var cx = Math.cos(x); + var sy = Math.sin(y); + var cy = Math.cos(y); + var sz = Math.sin(z); + var cz = Math.cos(z); + out[0] = sx * cy * cz - cx * sy * sz; + out[1] = cx * sy * cz + sx * cy * sz; + out[2] = cx * cy * sz - sx * sy * cz; + out[3] = cx * cy * cz + sx * sy * sz; + return out; +} +function str4(a2) { + return "quat(" + a2[0] + ", " + a2[1] + ", " + a2[2] + ", " + a2[3] + ")"; +} +var clone4 = clone3; +var fromValues4 = fromValues3; +var copy4 = copy3; +var set4 = set3; +var add4 = add3; +var mul4 = multiply4; +var scale4 = scale3; +var dot3 = dot2; +var lerp3 = lerp2; +var length3 = length2; +var len3 = length3; +var squaredLength3 = squaredLength2; +var sqrLen3 = squaredLength3; +var normalize3 = normalize2; +var exactEquals4 = exactEquals3; +var equals4 = equals3; +var rotationTo = (function() { + var tmpvec3 = create2(); + var xUnitVec3 = fromValues2(1, 0, 0); + var yUnitVec3 = fromValues2(0, 1, 0); + return function(out, a2, b2) { + var dot5 = dot(a2, b2); + if (dot5 < -0.999999) { + cross(tmpvec3, xUnitVec3, a2); + if (len(tmpvec3) < 1e-6) cross(tmpvec3, yUnitVec3, a2); + normalize(tmpvec3, tmpvec3); + setAxisAngle(out, tmpvec3, Math.PI); + return out; + } else if (dot5 > 0.999999) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; + } else { + cross(tmpvec3, a2, b2); + out[0] = tmpvec3[0]; + out[1] = tmpvec3[1]; + out[2] = tmpvec3[2]; + out[3] = 1 + dot5; + return normalize3(out, out); + } + }; +})(); +var sqlerp = (function() { + var temp1 = create4(); + var temp2 = create4(); + return function(out, a2, b2, c4, d2, t2) { + slerp(temp1, a2, d2, t2); + slerp(temp2, b2, c4, t2); + slerp(out, temp1, temp2, 2 * t2 * (1 - t2)); + return out; + }; +})(); +var setAxes = (function() { + var matr = create(); + return function(out, view, right, up) { + matr[0] = right[0]; + matr[3] = right[1]; + matr[6] = right[2]; + matr[1] = up[0]; + matr[4] = up[1]; + matr[7] = up[2]; + matr[2] = -view[0]; + matr[5] = -view[1]; + matr[8] = -view[2]; + return normalize3(out, fromMat3(out, matr)); + }; +})(); + +// public/aesthetic.computer/dep/gl-matrix/mat4.mjs +var mat4_exports = {}; +__export(mat4_exports, { + add: () => add5, + adjoint: () => adjoint2, + clone: () => clone5, + copy: () => copy5, + create: () => create5, + determinant: () => determinant2, + equals: () => equals5, + exactEquals: () => exactEquals5, + frob: () => frob2, + fromQuat: () => fromQuat3, + fromQuat2: () => fromQuat2, + fromRotation: () => fromRotation2, + fromRotationTranslation: () => fromRotationTranslation, + fromRotationTranslationScale: () => fromRotationTranslationScale, + fromRotationTranslationScaleOrigin: () => fromRotationTranslationScaleOrigin, + fromScaling: () => fromScaling2, + fromTranslation: () => fromTranslation2, + fromValues: () => fromValues5, + fromXRotation: () => fromXRotation, + fromYRotation: () => fromYRotation, + fromZRotation: () => fromZRotation, + frustum: () => frustum, + getRotation: () => getRotation, + getScaling: () => getScaling, + getTranslation: () => getTranslation, + identity: () => identity3, + invert: () => invert3, + lookAt: () => lookAt, + mul: () => mul5, + multiply: () => multiply5, + multiplyScalar: () => multiplyScalar2, + multiplyScalarAndAdd: () => multiplyScalarAndAdd2, + ortho: () => ortho, + orthoNO: () => orthoNO, + orthoZO: () => orthoZO, + perspective: () => perspective, + perspectiveFromFieldOfView: () => perspectiveFromFieldOfView, + perspectiveNO: () => perspectiveNO, + perspectiveZO: () => perspectiveZO, + rotate: () => rotate2, + rotateX: () => rotateX3, + rotateY: () => rotateY3, + rotateZ: () => rotateZ3, + scale: () => scale5, + set: () => set5, + str: () => str5, + sub: () => sub4, + subtract: () => subtract4, + targetTo: () => targetTo, + translate: () => translate2, + transpose: () => transpose2 +}); +function create5() { + var out = new ARRAY_TYPE(16); + if (ARRAY_TYPE != Float32Array) { + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + } + out[0] = 1; + out[5] = 1; + out[10] = 1; + out[15] = 1; + return out; +} +function clone5(a2) { + var out = new ARRAY_TYPE(16); + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + out[4] = a2[4]; + out[5] = a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[8] = a2[8]; + out[9] = a2[9]; + out[10] = a2[10]; + out[11] = a2[11]; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + return out; +} +function copy5(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + out[4] = a2[4]; + out[5] = a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[8] = a2[8]; + out[9] = a2[9]; + out[10] = a2[10]; + out[11] = a2[11]; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + return out; +} +function fromValues5(m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33) { + var out = new ARRAY_TYPE(16); + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m03; + out[4] = m10; + out[5] = m11; + out[6] = m12; + out[7] = m13; + out[8] = m20; + out[9] = m21; + out[10] = m22; + out[11] = m23; + out[12] = m30; + out[13] = m31; + out[14] = m32; + out[15] = m33; + return out; +} +function set5(out, m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33) { + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m03; + out[4] = m10; + out[5] = m11; + out[6] = m12; + out[7] = m13; + out[8] = m20; + out[9] = m21; + out[10] = m22; + out[11] = m23; + out[12] = m30; + out[13] = m31; + out[14] = m32; + out[15] = m33; + return out; +} +function identity3(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function transpose2(out, a2) { + if (out === a2) { + var a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a12 = a2[6], a13 = a2[7]; + var a23 = a2[11]; + out[1] = a2[4]; + out[2] = a2[8]; + out[3] = a2[12]; + out[4] = a01; + out[6] = a2[9]; + out[7] = a2[13]; + out[8] = a02; + out[9] = a12; + out[11] = a2[14]; + out[12] = a03; + out[13] = a13; + out[14] = a23; + } else { + out[0] = a2[0]; + out[1] = a2[4]; + out[2] = a2[8]; + out[3] = a2[12]; + out[4] = a2[1]; + out[5] = a2[5]; + out[6] = a2[9]; + out[7] = a2[13]; + out[8] = a2[2]; + out[9] = a2[6]; + out[10] = a2[10]; + out[11] = a2[14]; + out[12] = a2[3]; + out[13] = a2[7]; + out[14] = a2[11]; + out[15] = a2[15]; + } + return out; +} +function invert3(out, a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a10 = a2[4], a11 = a2[5], a12 = a2[6], a13 = a2[7]; + var a20 = a2[8], a21 = a2[9], a22 = a2[10], a23 = a2[11]; + var a30 = a2[12], a31 = a2[13], a32 = a2[14], a33 = a2[15]; + var b00 = a00 * a11 - a01 * a10; + var b01 = a00 * a12 - a02 * a10; + var b02 = a00 * a13 - a03 * a10; + var b03 = a01 * a12 - a02 * a11; + var b04 = a01 * a13 - a03 * a11; + var b05 = a02 * a13 - a03 * a12; + var b06 = a20 * a31 - a21 * a30; + var b07 = a20 * a32 - a22 * a30; + var b08 = a20 * a33 - a23 * a30; + var b09 = a21 * a32 - a22 * a31; + var b10 = a21 * a33 - a23 * a31; + var b11 = a22 * a33 - a23 * a32; + var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + if (!det) { + return null; + } + det = 1 / det; + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; + out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; + out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; + out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; + out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; + out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; + out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + return out; +} +function adjoint2(out, a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a10 = a2[4], a11 = a2[5], a12 = a2[6], a13 = a2[7]; + var a20 = a2[8], a21 = a2[9], a22 = a2[10], a23 = a2[11]; + var a30 = a2[12], a31 = a2[13], a32 = a2[14], a33 = a2[15]; + out[0] = a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22); + out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22)); + out[2] = a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12); + out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12)); + out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22)); + out[5] = a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22); + out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12)); + out[7] = a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12); + out[8] = a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21); + out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21)); + out[10] = a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11); + out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11)); + out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21)); + out[13] = a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21); + out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11)); + out[15] = a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11); + return out; +} +function determinant2(a2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a10 = a2[4], a11 = a2[5], a12 = a2[6], a13 = a2[7]; + var a20 = a2[8], a21 = a2[9], a22 = a2[10], a23 = a2[11]; + var a30 = a2[12], a31 = a2[13], a32 = a2[14], a33 = a2[15]; + var b00 = a00 * a11 - a01 * a10; + var b01 = a00 * a12 - a02 * a10; + var b02 = a00 * a13 - a03 * a10; + var b03 = a01 * a12 - a02 * a11; + var b04 = a01 * a13 - a03 * a11; + var b05 = a02 * a13 - a03 * a12; + var b06 = a20 * a31 - a21 * a30; + var b07 = a20 * a32 - a22 * a30; + var b08 = a20 * a33 - a23 * a30; + var b09 = a21 * a32 - a22 * a31; + var b10 = a21 * a33 - a23 * a31; + var b11 = a22 * a33 - a23 * a32; + return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; +} +function multiply5(out, a2, b2) { + var a00 = a2[0], a01 = a2[1], a02 = a2[2], a03 = a2[3]; + var a10 = a2[4], a11 = a2[5], a12 = a2[6], a13 = a2[7]; + var a20 = a2[8], a21 = a2[9], a22 = a2[10], a23 = a2[11]; + var a30 = a2[12], a31 = a2[13], a32 = a2[14], a33 = a2[15]; + var b0 = b2[0], b1 = b2[1], b22 = b2[2], b3 = b2[3]; + out[0] = b0 * a00 + b1 * a10 + b22 * a20 + b3 * a30; + out[1] = b0 * a01 + b1 * a11 + b22 * a21 + b3 * a31; + out[2] = b0 * a02 + b1 * a12 + b22 * a22 + b3 * a32; + out[3] = b0 * a03 + b1 * a13 + b22 * a23 + b3 * a33; + b0 = b2[4]; + b1 = b2[5]; + b22 = b2[6]; + b3 = b2[7]; + out[4] = b0 * a00 + b1 * a10 + b22 * a20 + b3 * a30; + out[5] = b0 * a01 + b1 * a11 + b22 * a21 + b3 * a31; + out[6] = b0 * a02 + b1 * a12 + b22 * a22 + b3 * a32; + out[7] = b0 * a03 + b1 * a13 + b22 * a23 + b3 * a33; + b0 = b2[8]; + b1 = b2[9]; + b22 = b2[10]; + b3 = b2[11]; + out[8] = b0 * a00 + b1 * a10 + b22 * a20 + b3 * a30; + out[9] = b0 * a01 + b1 * a11 + b22 * a21 + b3 * a31; + out[10] = b0 * a02 + b1 * a12 + b22 * a22 + b3 * a32; + out[11] = b0 * a03 + b1 * a13 + b22 * a23 + b3 * a33; + b0 = b2[12]; + b1 = b2[13]; + b22 = b2[14]; + b3 = b2[15]; + out[12] = b0 * a00 + b1 * a10 + b22 * a20 + b3 * a30; + out[13] = b0 * a01 + b1 * a11 + b22 * a21 + b3 * a31; + out[14] = b0 * a02 + b1 * a12 + b22 * a22 + b3 * a32; + out[15] = b0 * a03 + b1 * a13 + b22 * a23 + b3 * a33; + return out; +} +function translate2(out, a2, v2) { + var x = v2[0], y = v2[1], z = v2[2]; + var a00, a01, a02, a03; + var a10, a11, a12, a13; + var a20, a21, a22, a23; + if (a2 === out) { + out[12] = a2[0] * x + a2[4] * y + a2[8] * z + a2[12]; + out[13] = a2[1] * x + a2[5] * y + a2[9] * z + a2[13]; + out[14] = a2[2] * x + a2[6] * y + a2[10] * z + a2[14]; + out[15] = a2[3] * x + a2[7] * y + a2[11] * z + a2[15]; + } else { + a00 = a2[0]; + a01 = a2[1]; + a02 = a2[2]; + a03 = a2[3]; + a10 = a2[4]; + a11 = a2[5]; + a12 = a2[6]; + a13 = a2[7]; + a20 = a2[8]; + a21 = a2[9]; + a22 = a2[10]; + a23 = a2[11]; + out[0] = a00; + out[1] = a01; + out[2] = a02; + out[3] = a03; + out[4] = a10; + out[5] = a11; + out[6] = a12; + out[7] = a13; + out[8] = a20; + out[9] = a21; + out[10] = a22; + out[11] = a23; + out[12] = a00 * x + a10 * y + a20 * z + a2[12]; + out[13] = a01 * x + a11 * y + a21 * z + a2[13]; + out[14] = a02 * x + a12 * y + a22 * z + a2[14]; + out[15] = a03 * x + a13 * y + a23 * z + a2[15]; + } + return out; +} +function scale5(out, a2, v2) { + var x = v2[0], y = v2[1], z = v2[2]; + out[0] = a2[0] * x; + out[1] = a2[1] * x; + out[2] = a2[2] * x; + out[3] = a2[3] * x; + out[4] = a2[4] * y; + out[5] = a2[5] * y; + out[6] = a2[6] * y; + out[7] = a2[7] * y; + out[8] = a2[8] * z; + out[9] = a2[9] * z; + out[10] = a2[10] * z; + out[11] = a2[11] * z; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + return out; +} +function rotate2(out, a2, rad, axis) { + var x = axis[0], y = axis[1], z = axis[2]; + var len5 = Math.hypot(x, y, z); + var s2, c4, t2; + var a00, a01, a02, a03; + var a10, a11, a12, a13; + var a20, a21, a22, a23; + var b00, b01, b02; + var b10, b11, b12; + var b20, b21, b22; + if (len5 < EPSILON) { + return null; + } + len5 = 1 / len5; + x *= len5; + y *= len5; + z *= len5; + s2 = Math.sin(rad); + c4 = Math.cos(rad); + t2 = 1 - c4; + a00 = a2[0]; + a01 = a2[1]; + a02 = a2[2]; + a03 = a2[3]; + a10 = a2[4]; + a11 = a2[5]; + a12 = a2[6]; + a13 = a2[7]; + a20 = a2[8]; + a21 = a2[9]; + a22 = a2[10]; + a23 = a2[11]; + b00 = x * x * t2 + c4; + b01 = y * x * t2 + z * s2; + b02 = z * x * t2 - y * s2; + b10 = x * y * t2 - z * s2; + b11 = y * y * t2 + c4; + b12 = z * y * t2 + x * s2; + b20 = x * z * t2 + y * s2; + b21 = y * z * t2 - x * s2; + b22 = z * z * t2 + c4; + out[0] = a00 * b00 + a10 * b01 + a20 * b02; + out[1] = a01 * b00 + a11 * b01 + a21 * b02; + out[2] = a02 * b00 + a12 * b01 + a22 * b02; + out[3] = a03 * b00 + a13 * b01 + a23 * b02; + out[4] = a00 * b10 + a10 * b11 + a20 * b12; + out[5] = a01 * b10 + a11 * b11 + a21 * b12; + out[6] = a02 * b10 + a12 * b11 + a22 * b12; + out[7] = a03 * b10 + a13 * b11 + a23 * b12; + out[8] = a00 * b20 + a10 * b21 + a20 * b22; + out[9] = a01 * b20 + a11 * b21 + a21 * b22; + out[10] = a02 * b20 + a12 * b21 + a22 * b22; + out[11] = a03 * b20 + a13 * b21 + a23 * b22; + if (a2 !== out) { + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + } + return out; +} +function rotateX3(out, a2, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + var a10 = a2[4]; + var a11 = a2[5]; + var a12 = a2[6]; + var a13 = a2[7]; + var a20 = a2[8]; + var a21 = a2[9]; + var a22 = a2[10]; + var a23 = a2[11]; + if (a2 !== out) { + out[0] = a2[0]; + out[1] = a2[1]; + out[2] = a2[2]; + out[3] = a2[3]; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + } + out[4] = a10 * c4 + a20 * s2; + out[5] = a11 * c4 + a21 * s2; + out[6] = a12 * c4 + a22 * s2; + out[7] = a13 * c4 + a23 * s2; + out[8] = a20 * c4 - a10 * s2; + out[9] = a21 * c4 - a11 * s2; + out[10] = a22 * c4 - a12 * s2; + out[11] = a23 * c4 - a13 * s2; + return out; +} +function rotateY3(out, a2, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + var a00 = a2[0]; + var a01 = a2[1]; + var a02 = a2[2]; + var a03 = a2[3]; + var a20 = a2[8]; + var a21 = a2[9]; + var a22 = a2[10]; + var a23 = a2[11]; + if (a2 !== out) { + out[4] = a2[4]; + out[5] = a2[5]; + out[6] = a2[6]; + out[7] = a2[7]; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + } + out[0] = a00 * c4 - a20 * s2; + out[1] = a01 * c4 - a21 * s2; + out[2] = a02 * c4 - a22 * s2; + out[3] = a03 * c4 - a23 * s2; + out[8] = a00 * s2 + a20 * c4; + out[9] = a01 * s2 + a21 * c4; + out[10] = a02 * s2 + a22 * c4; + out[11] = a03 * s2 + a23 * c4; + return out; +} +function rotateZ3(out, a2, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + var a00 = a2[0]; + var a01 = a2[1]; + var a02 = a2[2]; + var a03 = a2[3]; + var a10 = a2[4]; + var a11 = a2[5]; + var a12 = a2[6]; + var a13 = a2[7]; + if (a2 !== out) { + out[8] = a2[8]; + out[9] = a2[9]; + out[10] = a2[10]; + out[11] = a2[11]; + out[12] = a2[12]; + out[13] = a2[13]; + out[14] = a2[14]; + out[15] = a2[15]; + } + out[0] = a00 * c4 + a10 * s2; + out[1] = a01 * c4 + a11 * s2; + out[2] = a02 * c4 + a12 * s2; + out[3] = a03 * c4 + a13 * s2; + out[4] = a10 * c4 - a00 * s2; + out[5] = a11 * c4 - a01 * s2; + out[6] = a12 * c4 - a02 * s2; + out[7] = a13 * c4 - a03 * s2; + return out; +} +function fromTranslation2(out, v2) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = v2[0]; + out[13] = v2[1]; + out[14] = v2[2]; + out[15] = 1; + return out; +} +function fromScaling2(out, v2) { + out[0] = v2[0]; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = v2[1]; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = v2[2]; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function fromRotation2(out, rad, axis) { + var x = axis[0], y = axis[1], z = axis[2]; + var len5 = Math.hypot(x, y, z); + var s2, c4, t2; + if (len5 < EPSILON) { + return null; + } + len5 = 1 / len5; + x *= len5; + y *= len5; + z *= len5; + s2 = Math.sin(rad); + c4 = Math.cos(rad); + t2 = 1 - c4; + out[0] = x * x * t2 + c4; + out[1] = y * x * t2 + z * s2; + out[2] = z * x * t2 - y * s2; + out[3] = 0; + out[4] = x * y * t2 - z * s2; + out[5] = y * y * t2 + c4; + out[6] = z * y * t2 + x * s2; + out[7] = 0; + out[8] = x * z * t2 + y * s2; + out[9] = y * z * t2 - x * s2; + out[10] = z * z * t2 + c4; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function fromXRotation(out, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = c4; + out[6] = s2; + out[7] = 0; + out[8] = 0; + out[9] = -s2; + out[10] = c4; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function fromYRotation(out, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + out[0] = c4; + out[1] = 0; + out[2] = -s2; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = s2; + out[9] = 0; + out[10] = c4; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function fromZRotation(out, rad) { + var s2 = Math.sin(rad); + var c4 = Math.cos(rad); + out[0] = c4; + out[1] = s2; + out[2] = 0; + out[3] = 0; + out[4] = -s2; + out[5] = c4; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function fromRotationTranslation(out, q, v2) { + var x = q[0], y = q[1], z = q[2], w = q[3]; + var x2 = x + x; + var y2 = y + y; + var z2 = z + z; + var xx = x * x2; + var xy = x * y2; + var xz = x * z2; + var yy = y * y2; + var yz = y * z2; + var zz = z * z2; + var wx = w * x2; + var wy = w * y2; + var wz = w * z2; + out[0] = 1 - (yy + zz); + out[1] = xy + wz; + out[2] = xz - wy; + out[3] = 0; + out[4] = xy - wz; + out[5] = 1 - (xx + zz); + out[6] = yz + wx; + out[7] = 0; + out[8] = xz + wy; + out[9] = yz - wx; + out[10] = 1 - (xx + yy); + out[11] = 0; + out[12] = v2[0]; + out[13] = v2[1]; + out[14] = v2[2]; + out[15] = 1; + return out; +} +function fromQuat2(out, a2) { + var translation = new ARRAY_TYPE(3); + var bx = -a2[0], by = -a2[1], bz = -a2[2], bw = a2[3], ax = a2[4], ay = a2[5], az = a2[6], aw = a2[7]; + var magnitude = bx * bx + by * by + bz * bz + bw * bw; + if (magnitude > 0) { + translation[0] = (ax * bw + aw * bx + ay * bz - az * by) * 2 / magnitude; + translation[1] = (ay * bw + aw * by + az * bx - ax * bz) * 2 / magnitude; + translation[2] = (az * bw + aw * bz + ax * by - ay * bx) * 2 / magnitude; + } else { + translation[0] = (ax * bw + aw * bx + ay * bz - az * by) * 2; + translation[1] = (ay * bw + aw * by + az * bx - ax * bz) * 2; + translation[2] = (az * bw + aw * bz + ax * by - ay * bx) * 2; + } + fromRotationTranslation(out, a2, translation); + return out; +} +function getTranslation(out, mat) { + out[0] = mat[12]; + out[1] = mat[13]; + out[2] = mat[14]; + return out; +} +function getScaling(out, mat) { + var m11 = mat[0]; + var m12 = mat[1]; + var m13 = mat[2]; + var m21 = mat[4]; + var m22 = mat[5]; + var m23 = mat[6]; + var m31 = mat[8]; + var m32 = mat[9]; + var m33 = mat[10]; + out[0] = Math.hypot(m11, m12, m13); + out[1] = Math.hypot(m21, m22, m23); + out[2] = Math.hypot(m31, m32, m33); + return out; +} +function getRotation(out, mat) { + var scaling = new ARRAY_TYPE(3); + getScaling(scaling, mat); + var is1 = 1 / scaling[0]; + var is2 = 1 / scaling[1]; + var is3 = 1 / scaling[2]; + var sm11 = mat[0] * is1; + var sm12 = mat[1] * is2; + var sm13 = mat[2] * is3; + var sm21 = mat[4] * is1; + var sm22 = mat[5] * is2; + var sm23 = mat[6] * is3; + var sm31 = mat[8] * is1; + var sm32 = mat[9] * is2; + var sm33 = mat[10] * is3; + var trace = sm11 + sm22 + sm33; + var S = 0; + if (trace > 0) { + S = Math.sqrt(trace + 1) * 2; + out[3] = 0.25 * S; + out[0] = (sm23 - sm32) / S; + out[1] = (sm31 - sm13) / S; + out[2] = (sm12 - sm21) / S; + } else if (sm11 > sm22 && sm11 > sm33) { + S = Math.sqrt(1 + sm11 - sm22 - sm33) * 2; + out[3] = (sm23 - sm32) / S; + out[0] = 0.25 * S; + out[1] = (sm12 + sm21) / S; + out[2] = (sm31 + sm13) / S; + } else if (sm22 > sm33) { + S = Math.sqrt(1 + sm22 - sm11 - sm33) * 2; + out[3] = (sm31 - sm13) / S; + out[0] = (sm12 + sm21) / S; + out[1] = 0.25 * S; + out[2] = (sm23 + sm32) / S; + } else { + S = Math.sqrt(1 + sm33 - sm11 - sm22) * 2; + out[3] = (sm12 - sm21) / S; + out[0] = (sm31 + sm13) / S; + out[1] = (sm23 + sm32) / S; + out[2] = 0.25 * S; + } + return out; +} +function fromRotationTranslationScale(out, q, v2, s2) { + var x = q[0], y = q[1], z = q[2], w = q[3]; + var x2 = x + x; + var y2 = y + y; + var z2 = z + z; + var xx = x * x2; + var xy = x * y2; + var xz = x * z2; + var yy = y * y2; + var yz = y * z2; + var zz = z * z2; + var wx = w * x2; + var wy = w * y2; + var wz = w * z2; + var sx = s2[0]; + var sy = s2[1]; + var sz = s2[2]; + out[0] = (1 - (yy + zz)) * sx; + out[1] = (xy + wz) * sx; + out[2] = (xz - wy) * sx; + out[3] = 0; + out[4] = (xy - wz) * sy; + out[5] = (1 - (xx + zz)) * sy; + out[6] = (yz + wx) * sy; + out[7] = 0; + out[8] = (xz + wy) * sz; + out[9] = (yz - wx) * sz; + out[10] = (1 - (xx + yy)) * sz; + out[11] = 0; + out[12] = v2[0]; + out[13] = v2[1]; + out[14] = v2[2]; + out[15] = 1; + return out; +} +function fromRotationTranslationScaleOrigin(out, q, v2, s2, o2) { + var x = q[0], y = q[1], z = q[2], w = q[3]; + var x2 = x + x; + var y2 = y + y; + var z2 = z + z; + var xx = x * x2; + var xy = x * y2; + var xz = x * z2; + var yy = y * y2; + var yz = y * z2; + var zz = z * z2; + var wx = w * x2; + var wy = w * y2; + var wz = w * z2; + var sx = s2[0]; + var sy = s2[1]; + var sz = s2[2]; + var ox = o2[0]; + var oy = o2[1]; + var oz = o2[2]; + var out0 = (1 - (yy + zz)) * sx; + var out1 = (xy + wz) * sx; + var out2 = (xz - wy) * sx; + var out4 = (xy - wz) * sy; + var out5 = (1 - (xx + zz)) * sy; + var out6 = (yz + wx) * sy; + var out8 = (xz + wy) * sz; + var out9 = (yz - wx) * sz; + var out10 = (1 - (xx + yy)) * sz; + out[0] = out0; + out[1] = out1; + out[2] = out2; + out[3] = 0; + out[4] = out4; + out[5] = out5; + out[6] = out6; + out[7] = 0; + out[8] = out8; + out[9] = out9; + out[10] = out10; + out[11] = 0; + out[12] = v2[0] + ox - (out0 * ox + out4 * oy + out8 * oz); + out[13] = v2[1] + oy - (out1 * ox + out5 * oy + out9 * oz); + out[14] = v2[2] + oz - (out2 * ox + out6 * oy + out10 * oz); + out[15] = 1; + return out; +} +function fromQuat3(out, q) { + var x = q[0], y = q[1], z = q[2], w = q[3]; + var x2 = x + x; + var y2 = y + y; + var z2 = z + z; + var xx = x * x2; + var yx = y * x2; + var yy = y * y2; + var zx = z * x2; + var zy = z * y2; + var zz = z * z2; + var wx = w * x2; + var wy = w * y2; + var wz = w * z2; + out[0] = 1 - yy - zz; + out[1] = yx + wz; + out[2] = zx - wy; + out[3] = 0; + out[4] = yx - wz; + out[5] = 1 - xx - zz; + out[6] = zy + wx; + out[7] = 0; + out[8] = zx + wy; + out[9] = zy - wx; + out[10] = 1 - xx - yy; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function frustum(out, left, right, bottom, top, near, far) { + var rl = 1 / (right - left); + var tb = 1 / (top - bottom); + var nf = 1 / (near - far); + out[0] = near * 2 * rl; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = near * 2 * tb; + out[6] = 0; + out[7] = 0; + out[8] = (right + left) * rl; + out[9] = (top + bottom) * tb; + out[10] = (far + near) * nf; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = far * near * 2 * nf; + out[15] = 0; + return out; +} +function perspectiveNO(out, fovy, aspect, near, far) { + var f2 = 1 / Math.tan(fovy / 2), nf; + out[0] = f2 / aspect; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = f2; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[15] = 0; + if (far != null && far !== Infinity) { + nf = 1 / (near - far); + out[10] = (far + near) * nf; + out[14] = 2 * far * near * nf; + } else { + out[10] = -1; + out[14] = -2 * near; + } + return out; +} +var perspective = perspectiveNO; +function perspectiveZO(out, fovy, aspect, near, far) { + var f2 = 1 / Math.tan(fovy / 2), nf; + out[0] = f2 / aspect; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = f2; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[15] = 0; + if (far != null && far !== Infinity) { + nf = 1 / (near - far); + out[10] = far * nf; + out[14] = far * near * nf; + } else { + out[10] = -1; + out[14] = -near; + } + return out; +} +function perspectiveFromFieldOfView(out, fov, near, far) { + var upTan = Math.tan(fov.upDegrees * Math.PI / 180); + var downTan = Math.tan(fov.downDegrees * Math.PI / 180); + var leftTan = Math.tan(fov.leftDegrees * Math.PI / 180); + var rightTan = Math.tan(fov.rightDegrees * Math.PI / 180); + var xScale = 2 / (leftTan + rightTan); + var yScale = 2 / (upTan + downTan); + out[0] = xScale; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = yScale; + out[6] = 0; + out[7] = 0; + out[8] = -((leftTan - rightTan) * xScale * 0.5); + out[9] = (upTan - downTan) * yScale * 0.5; + out[10] = far / (near - far); + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = far * near / (near - far); + out[15] = 0; + return out; +} +function orthoNO(out, left, right, bottom, top, near, far) { + var lr = 1 / (left - right); + var bt = 1 / (bottom - top); + var nf = 1 / (near - far); + out[0] = -2 * lr; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = -2 * bt; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 2 * nf; + out[11] = 0; + out[12] = (left + right) * lr; + out[13] = (top + bottom) * bt; + out[14] = (far + near) * nf; + out[15] = 1; + return out; +} +var ortho = orthoNO; +function orthoZO(out, left, right, bottom, top, near, far) { + var lr = 1 / (left - right); + var bt = 1 / (bottom - top); + var nf = 1 / (near - far); + out[0] = -2 * lr; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = -2 * bt; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = nf; + out[11] = 0; + out[12] = (left + right) * lr; + out[13] = (top + bottom) * bt; + out[14] = near * nf; + out[15] = 1; + return out; +} +function lookAt(out, eye, center, up) { + var x0, x1, x2, y0, y1, y2, z0, z1, z2, len5; + var eyex = eye[0]; + var eyey = eye[1]; + var eyez = eye[2]; + var upx = up[0]; + var upy = up[1]; + var upz = up[2]; + var centerx = center[0]; + var centery = center[1]; + var centerz = center[2]; + if (Math.abs(eyex - centerx) < EPSILON && Math.abs(eyey - centery) < EPSILON && Math.abs(eyez - centerz) < EPSILON) { + return identity3(out); + } + z0 = eyex - centerx; + z1 = eyey - centery; + z2 = eyez - centerz; + len5 = 1 / Math.hypot(z0, z1, z2); + z0 *= len5; + z1 *= len5; + z2 *= len5; + x0 = upy * z2 - upz * z1; + x1 = upz * z0 - upx * z2; + x2 = upx * z1 - upy * z0; + len5 = Math.hypot(x0, x1, x2); + if (!len5) { + x0 = 0; + x1 = 0; + x2 = 0; + } else { + len5 = 1 / len5; + x0 *= len5; + x1 *= len5; + x2 *= len5; + } + y0 = z1 * x2 - z2 * x1; + y1 = z2 * x0 - z0 * x2; + y2 = z0 * x1 - z1 * x0; + len5 = Math.hypot(y0, y1, y2); + if (!len5) { + y0 = 0; + y1 = 0; + y2 = 0; + } else { + len5 = 1 / len5; + y0 *= len5; + y1 *= len5; + y2 *= len5; + } + out[0] = x0; + out[1] = y0; + out[2] = z0; + out[3] = 0; + out[4] = x1; + out[5] = y1; + out[6] = z1; + out[7] = 0; + out[8] = x2; + out[9] = y2; + out[10] = z2; + out[11] = 0; + out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); + out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); + out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); + out[15] = 1; + return out; +} +function targetTo(out, eye, target, up) { + var eyex = eye[0], eyey = eye[1], eyez = eye[2], upx = up[0], upy = up[1], upz = up[2]; + var z0 = eyex - target[0], z1 = eyey - target[1], z2 = eyez - target[2]; + var len5 = z0 * z0 + z1 * z1 + z2 * z2; + if (len5 > 0) { + len5 = 1 / Math.sqrt(len5); + z0 *= len5; + z1 *= len5; + z2 *= len5; + } + var x0 = upy * z2 - upz * z1, x1 = upz * z0 - upx * z2, x2 = upx * z1 - upy * z0; + len5 = x0 * x0 + x1 * x1 + x2 * x2; + if (len5 > 0) { + len5 = 1 / Math.sqrt(len5); + x0 *= len5; + x1 *= len5; + x2 *= len5; + } + out[0] = x0; + out[1] = x1; + out[2] = x2; + out[3] = 0; + out[4] = z1 * x2 - z2 * x1; + out[5] = z2 * x0 - z0 * x2; + out[6] = z0 * x1 - z1 * x0; + out[7] = 0; + out[8] = z0; + out[9] = z1; + out[10] = z2; + out[11] = 0; + out[12] = eyex; + out[13] = eyey; + out[14] = eyez; + out[15] = 1; + return out; +} +function str5(a2) { + return "mat4(" + a2[0] + ", " + a2[1] + ", " + a2[2] + ", " + a2[3] + ", " + a2[4] + ", " + a2[5] + ", " + a2[6] + ", " + a2[7] + ", " + a2[8] + ", " + a2[9] + ", " + a2[10] + ", " + a2[11] + ", " + a2[12] + ", " + a2[13] + ", " + a2[14] + ", " + a2[15] + ")"; +} +function frob2(a2) { + return Math.hypot( + a2[0], + a2[1], + a2[2], + a2[3], + a2[4], + a2[5], + a2[6], + a2[7], + a2[8], + a2[9], + a2[10], + a2[11], + a2[12], + a2[13], + a2[14], + a2[15] + ); +} +function add5(out, a2, b2) { + out[0] = a2[0] + b2[0]; + out[1] = a2[1] + b2[1]; + out[2] = a2[2] + b2[2]; + out[3] = a2[3] + b2[3]; + out[4] = a2[4] + b2[4]; + out[5] = a2[5] + b2[5]; + out[6] = a2[6] + b2[6]; + out[7] = a2[7] + b2[7]; + out[8] = a2[8] + b2[8]; + out[9] = a2[9] + b2[9]; + out[10] = a2[10] + b2[10]; + out[11] = a2[11] + b2[11]; + out[12] = a2[12] + b2[12]; + out[13] = a2[13] + b2[13]; + out[14] = a2[14] + b2[14]; + out[15] = a2[15] + b2[15]; + return out; +} +function subtract4(out, a2, b2) { + out[0] = a2[0] - b2[0]; + out[1] = a2[1] - b2[1]; + out[2] = a2[2] - b2[2]; + out[3] = a2[3] - b2[3]; + out[4] = a2[4] - b2[4]; + out[5] = a2[5] - b2[5]; + out[6] = a2[6] - b2[6]; + out[7] = a2[7] - b2[7]; + out[8] = a2[8] - b2[8]; + out[9] = a2[9] - b2[9]; + out[10] = a2[10] - b2[10]; + out[11] = a2[11] - b2[11]; + out[12] = a2[12] - b2[12]; + out[13] = a2[13] - b2[13]; + out[14] = a2[14] - b2[14]; + out[15] = a2[15] - b2[15]; + return out; +} +function multiplyScalar2(out, a2, b2) { + out[0] = a2[0] * b2; + out[1] = a2[1] * b2; + out[2] = a2[2] * b2; + out[3] = a2[3] * b2; + out[4] = a2[4] * b2; + out[5] = a2[5] * b2; + out[6] = a2[6] * b2; + out[7] = a2[7] * b2; + out[8] = a2[8] * b2; + out[9] = a2[9] * b2; + out[10] = a2[10] * b2; + out[11] = a2[11] * b2; + out[12] = a2[12] * b2; + out[13] = a2[13] * b2; + out[14] = a2[14] * b2; + out[15] = a2[15] * b2; + return out; +} +function multiplyScalarAndAdd2(out, a2, b2, scale7) { + out[0] = a2[0] + b2[0] * scale7; + out[1] = a2[1] + b2[1] * scale7; + out[2] = a2[2] + b2[2] * scale7; + out[3] = a2[3] + b2[3] * scale7; + out[4] = a2[4] + b2[4] * scale7; + out[5] = a2[5] + b2[5] * scale7; + out[6] = a2[6] + b2[6] * scale7; + out[7] = a2[7] + b2[7] * scale7; + out[8] = a2[8] + b2[8] * scale7; + out[9] = a2[9] + b2[9] * scale7; + out[10] = a2[10] + b2[10] * scale7; + out[11] = a2[11] + b2[11] * scale7; + out[12] = a2[12] + b2[12] * scale7; + out[13] = a2[13] + b2[13] * scale7; + out[14] = a2[14] + b2[14] * scale7; + out[15] = a2[15] + b2[15] * scale7; + return out; +} +function exactEquals5(a2, b2) { + return a2[0] === b2[0] && a2[1] === b2[1] && a2[2] === b2[2] && a2[3] === b2[3] && a2[4] === b2[4] && a2[5] === b2[5] && a2[6] === b2[6] && a2[7] === b2[7] && a2[8] === b2[8] && a2[9] === b2[9] && a2[10] === b2[10] && a2[11] === b2[11] && a2[12] === b2[12] && a2[13] === b2[13] && a2[14] === b2[14] && a2[15] === b2[15]; +} +function equals5(a2, b2) { + var a0 = a2[0], a1 = a2[1], a22 = a2[2], a3 = a2[3]; + var a4 = a2[4], a5 = a2[5], a6 = a2[6], a7 = a2[7]; + var a8 = a2[8], a9 = a2[9], a10 = a2[10], a11 = a2[11]; + var a12 = a2[12], a13 = a2[13], a14 = a2[14], a15 = a2[15]; + var b0 = b2[0], b1 = b2[1], b22 = b2[2], b3 = b2[3]; + var b4 = b2[4], b5 = b2[5], b6 = b2[6], b7 = b2[7]; + var b8 = b2[8], b9 = b2[9], b10 = b2[10], b11 = b2[11]; + var b12 = b2[12], b13 = b2[13], b14 = b2[14], b15 = b2[15]; + return Math.abs(a0 - b0) <= EPSILON * Math.max(1, Math.abs(a0), Math.abs(b0)) && Math.abs(a1 - b1) <= EPSILON * Math.max(1, Math.abs(a1), Math.abs(b1)) && Math.abs(a22 - b22) <= EPSILON * Math.max(1, Math.abs(a22), Math.abs(b22)) && Math.abs(a3 - b3) <= EPSILON * Math.max(1, Math.abs(a3), Math.abs(b3)) && Math.abs(a4 - b4) <= EPSILON * Math.max(1, Math.abs(a4), Math.abs(b4)) && Math.abs(a5 - b5) <= EPSILON * Math.max(1, Math.abs(a5), Math.abs(b5)) && Math.abs(a6 - b6) <= EPSILON * Math.max(1, Math.abs(a6), Math.abs(b6)) && Math.abs(a7 - b7) <= EPSILON * Math.max(1, Math.abs(a7), Math.abs(b7)) && Math.abs(a8 - b8) <= EPSILON * Math.max(1, Math.abs(a8), Math.abs(b8)) && Math.abs(a9 - b9) <= EPSILON * Math.max(1, Math.abs(a9), Math.abs(b9)) && Math.abs(a10 - b10) <= EPSILON * Math.max(1, Math.abs(a10), Math.abs(b10)) && Math.abs(a11 - b11) <= EPSILON * Math.max(1, Math.abs(a11), Math.abs(b11)) && Math.abs(a12 - b12) <= EPSILON * Math.max(1, Math.abs(a12), Math.abs(b12)) && Math.abs(a13 - b13) <= EPSILON * Math.max(1, Math.abs(a13), Math.abs(b13)) && Math.abs(a14 - b14) <= EPSILON * Math.max(1, Math.abs(a14), Math.abs(b14)) && Math.abs(a15 - b15) <= EPSILON * Math.max(1, Math.abs(a15), Math.abs(b15)); +} +var mul5 = multiply5; +var sub4 = subtract4; + +// public/aesthetic.computer/dep/gl-matrix/vec2.mjs +var vec2_exports = {}; +__export(vec2_exports, { + add: () => add6, + angle: () => angle2, + ceil: () => ceil3, + clone: () => clone6, + copy: () => copy6, + create: () => create6, + cross: () => cross3, + dist: () => dist3, + distance: () => distance3, + div: () => div3, + divide: () => divide3, + dot: () => dot4, + equals: () => equals6, + exactEquals: () => exactEquals6, + floor: () => floor3, + forEach: () => forEach3, + fromValues: () => fromValues6, + inverse: () => inverse3, + len: () => len4, + length: () => length4, + lerp: () => lerp4, + max: () => max3, + min: () => min3, + mul: () => mul6, + multiply: () => multiply6, + negate: () => negate3, + normalize: () => normalize4, + random: () => random4, + rotate: () => rotate3, + round: () => round3, + scale: () => scale6, + scaleAndAdd: () => scaleAndAdd3, + set: () => set6, + sqrDist: () => sqrDist3, + sqrLen: () => sqrLen4, + squaredDistance: () => squaredDistance3, + squaredLength: () => squaredLength4, + str: () => str6, + sub: () => sub5, + subtract: () => subtract5, + transformMat2: () => transformMat2, + transformMat2d: () => transformMat2d, + transformMat3: () => transformMat32, + transformMat4: () => transformMat43, + zero: () => zero3 +}); +function create6() { + var out = new ARRAY_TYPE(2); + if (ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + } + return out; +} +function clone6(a2) { + var out = new ARRAY_TYPE(2); + out[0] = a2[0]; + out[1] = a2[1]; + return out; +} +function fromValues6(x, y) { + var out = new ARRAY_TYPE(2); + out[0] = x; + out[1] = y; + return out; +} +function copy6(out, a2) { + out[0] = a2[0]; + out[1] = a2[1]; + return out; +} +function set6(out, x, y) { + out[0] = x; + out[1] = y; + return out; +} +function add6(out, a2, b2) { + out[0] = a2[0] + b2[0]; + out[1] = a2[1] + b2[1]; + return out; +} +function subtract5(out, a2, b2) { + out[0] = a2[0] - b2[0]; + out[1] = a2[1] - b2[1]; + return out; +} +function multiply6(out, a2, b2) { + out[0] = a2[0] * b2[0]; + out[1] = a2[1] * b2[1]; + return out; +} +function divide3(out, a2, b2) { + out[0] = a2[0] / b2[0]; + out[1] = a2[1] / b2[1]; + return out; +} +function ceil3(out, a2) { + out[0] = Math.ceil(a2[0]); + out[1] = Math.ceil(a2[1]); + return out; +} +function floor3(out, a2) { + out[0] = Math.floor(a2[0]); + out[1] = Math.floor(a2[1]); + return out; +} +function min3(out, a2, b2) { + out[0] = Math.min(a2[0], b2[0]); + out[1] = Math.min(a2[1], b2[1]); + return out; +} +function max3(out, a2, b2) { + out[0] = Math.max(a2[0], b2[0]); + out[1] = Math.max(a2[1], b2[1]); + return out; +} +function round3(out, a2) { + out[0] = Math.round(a2[0]); + out[1] = Math.round(a2[1]); + return out; +} +function scale6(out, a2, b2) { + out[0] = a2[0] * b2; + out[1] = a2[1] * b2; + return out; +} +function scaleAndAdd3(out, a2, b2, scale7) { + out[0] = a2[0] + b2[0] * scale7; + out[1] = a2[1] + b2[1] * scale7; + return out; +} +function distance3(a2, b2) { + var x = b2[0] - a2[0], y = b2[1] - a2[1]; + return Math.hypot(x, y); +} +function squaredDistance3(a2, b2) { + var x = b2[0] - a2[0], y = b2[1] - a2[1]; + return x * x + y * y; +} +function length4(a2) { + var x = a2[0], y = a2[1]; + return Math.hypot(x, y); +} +function squaredLength4(a2) { + var x = a2[0], y = a2[1]; + return x * x + y * y; +} +function negate3(out, a2) { + out[0] = -a2[0]; + out[1] = -a2[1]; + return out; +} +function inverse3(out, a2) { + out[0] = 1 / a2[0]; + out[1] = 1 / a2[1]; + return out; +} +function normalize4(out, a2) { + var x = a2[0], y = a2[1]; + var len5 = x * x + y * y; + if (len5 > 0) { + len5 = 1 / Math.sqrt(len5); + } + out[0] = a2[0] * len5; + out[1] = a2[1] * len5; + return out; +} +function dot4(a2, b2) { + return a2[0] * b2[0] + a2[1] * b2[1]; +} +function cross3(out, a2, b2) { + var z = a2[0] * b2[1] - a2[1] * b2[0]; + out[0] = out[1] = 0; + out[2] = z; + return out; +} +function lerp4(out, a2, b2, t2) { + var ax = a2[0], ay = a2[1]; + out[0] = ax + t2 * (b2[0] - ax); + out[1] = ay + t2 * (b2[1] - ay); + return out; +} +function random4(out, scale7) { + scale7 = scale7 || 1; + var r2 = RANDOM() * 2 * Math.PI; + out[0] = Math.cos(r2) * scale7; + out[1] = Math.sin(r2) * scale7; + return out; +} +function transformMat2(out, a2, m) { + var x = a2[0], y = a2[1]; + out[0] = m[0] * x + m[2] * y; + out[1] = m[1] * x + m[3] * y; + return out; +} +function transformMat2d(out, a2, m) { + var x = a2[0], y = a2[1]; + out[0] = m[0] * x + m[2] * y + m[4]; + out[1] = m[1] * x + m[3] * y + m[5]; + return out; +} +function transformMat32(out, a2, m) { + var x = a2[0], y = a2[1]; + out[0] = m[0] * x + m[3] * y + m[6]; + out[1] = m[1] * x + m[4] * y + m[7]; + return out; +} +function transformMat43(out, a2, m) { + var x = a2[0]; + var y = a2[1]; + out[0] = m[0] * x + m[4] * y + m[12]; + out[1] = m[1] * x + m[5] * y + m[13]; + return out; +} +function rotate3(out, a2, b2, rad) { + var p0 = a2[0] - b2[0], p1 = a2[1] - b2[1], sinC = Math.sin(rad), cosC = Math.cos(rad); + out[0] = p0 * cosC - p1 * sinC + b2[0]; + out[1] = p0 * sinC + p1 * cosC + b2[1]; + return out; +} +function angle2(a2, b2) { + var x1 = a2[0], y1 = a2[1], x2 = b2[0], y2 = b2[1], mag = Math.sqrt(x1 * x1 + y1 * y1) * Math.sqrt(x2 * x2 + y2 * y2), cosine = mag && (x1 * x2 + y1 * y2) / mag; + return Math.acos(Math.min(Math.max(cosine, -1), 1)); +} +function zero3(out) { + out[0] = 0; + out[1] = 0; + return out; +} +function str6(a2) { + return "vec2(" + a2[0] + ", " + a2[1] + ")"; +} +function exactEquals6(a2, b2) { + return a2[0] === b2[0] && a2[1] === b2[1]; +} +function equals6(a2, b2) { + var a0 = a2[0], a1 = a2[1]; + var b0 = b2[0], b1 = b2[1]; + return Math.abs(a0 - b0) <= EPSILON * Math.max(1, Math.abs(a0), Math.abs(b0)) && Math.abs(a1 - b1) <= EPSILON * Math.max(1, Math.abs(a1), Math.abs(b1)); +} +var len4 = length4; +var sub5 = subtract5; +var mul6 = multiply6; +var div3 = divide3; +var dist3 = distance3; +var sqrDist3 = squaredDistance3; +var sqrLen4 = squaredLength4; +var forEach3 = (function() { + var vec = create6(); + return function(a2, stride, offset, count, fn, arg) { + var i2, l2; + if (!stride) { + stride = 2; + } + if (!offset) { + offset = 0; + } + if (count) { + l2 = Math.min(count * stride + offset, a2.length); + } else { + l2 = a2.length; + } + for (i2 = offset; i2 < l2; i2 += stride) { + vec[0] = a2[i2]; + vec[1] = a2[i2 + 1]; + fn(vec, vec, arg); + a2[i2] = vec[0]; + a2[i2 + 1] = vec[1]; + } + return a2; + }; +})(); + +// public/aesthetic.computer/lib/num.mjs +var { + abs, + round: round4, + floor: floor4, + ceil: ceil4, + random: random5, + PI, + min: min4, + max: max4, + sqrt, + pow: pow2, + atan2, + sin, + cos +} = Math; +var p2 = { + // Turn two values into an {x, y} point. + of: function(x, y) { + return { x, y }; + }, + // Get the length of the point as a vector. + len: function(pA) { + return Math.hypot(pA.x, pA.y); + }, + // Normalize a vector to have a length of 1 + norm: function(p) { + let len5 = this.len(p); + return len5 === 0 ? { x: 0, y: 0 } : { x: p.x / len5, y: p.y / len5 }; + }, + // Check for the equality of two points. + eq: function(p1, p22) { + return p1.x === p22.x && p1.y === p22.y; + }, + // Mutably add P->in to P->out. + inc: function(pout, pin) { + pout.x += pin.x; + pout.y += pin.y; + return pout; + }, + // Mutably scale P->out by P->in. + scl: function(pout, pin) { + pout.x *= pin.x || pin; + pout.y *= pin.y || pin; + return pout; + }, + // Immutably add pA + pB. + add: function(pA, pB) { + return { + x: pA.x + pB.x, + y: pA.y + pB.y + }; + }, + // Immutably sub pA - pB. + sub: function(pA, pB) { + return { + x: pA.x - pB.x, + y: pA.y - pB.y + }; + }, + // Immutably rotate p by angle in radians. + rot(p, angle3) { + return { + x: p.x * cos(angle3) - p.y * sin(angle3), + y: p.x * sin(angle3) + p.y * cos(angle3) + }; + }, + // Immutably multiply pA * pB. + mul: function(pA, pB) { + return { + x: pA.x * (pB.x || pB), + y: pA.y * (pB.y || pB) + }; + }, + // Immutably divide pA / pB. + // If pA is a single number then this function expands it to an `{x, y}`. + // Note: Other library functions here could do the same. 2023.1.19 + div: function(pA, pB) { + if (typeof pA === "number") pA = { x: pA, y: pA }; + return { + x: pA.x / pB.x, + y: pA.y / pB.y + }; + }, + mid: function(pA, pB) { + return { + x: (pA.x + pB.x) / 2, + y: (pA.y + pB.y) / 2 + }; + }, + dist: function(pA, pB) { + return sqrt(pow2(pB.x - pA.x, 2) + pow2(pB.y - pA.y, 2)); + }, + angle: function(pA, pB) { + return atan2(pB.y - pA.y, pB.x - pA.x); + }, + dot: function(pA, pB) { + return pA.x * pB.x + pA.y * pB.y; + }, + floor: function(p) { + return { x: floor4(p.x), y: floor4(p.y) }; + } +}; +function add7(...args) { + let numbers; + if (Array.isArray(args[0])) { + numbers = args[0]; + } else { + numbers = args; + } + return numbers.reduce((acc, curr) => acc + curr, 0); +} +function midp(a2, b2) { + return [(a2[0] + b2[0]) / 2, (a2[1] + b2[1]) / 2]; +} +function number(maybeNumber) { + return typeof maybeNumber === "number"; +} +function intersects() { + let a2, b2, c4, d2, p, q, r2, s2; + if (arguments.length === 2 && typeof arguments[0] === "object" && typeof arguments[1] === "object") { + a2 = arguments[0].x0; + b2 = arguments[0].y0; + c4 = arguments[0].x1; + d2 = arguments[0].y1; + p = arguments[1].x0; + q = arguments[1].y0; + r2 = arguments[1].x1; + s2 = arguments[1].y1; + } else if (arguments.length === 8) { + [a2, b2, c4, d2, p, q, r2, s2] = arguments; + } else { + console.warn("Invalid intersection input."); + return false; + } + let det, lambda, gamma; + det = (c4 - a2) * (s2 - q) - (r2 - p) * (d2 - b2); + if (det === 0) { + return false; + } else { + lambda = ((s2 - q) * (r2 - a2) + (p - r2) * (s2 - b2)) / det; + gamma = ((b2 - d2) * (r2 - a2) + (c4 - a2) * (s2 - b2)) / det; + return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1; + } +} +function signedCeil(n2) { + return n2 < 0 ? Math.floor(n2) : Math.ceil(n2); +} +function signedFloor(val) { + return val < 0 ? Math.ceil(val) : Math.floor(val); +} +function wrap(n2, to) { + return (n2 / to - floor4(n2 / to)) * to; +} +function even(n2) { + return n2 % 2 === 0; +} +function odd(n2) { + return !even(n2); +} +function byteInterval17(i16) { + return min4(i16 * 16, 255); +} +function rand() { + return random5(); +} +function randInt(n2) { + return floor4(rand() * (n2 + 1)); +} +function anyKey(obj) { + const keys4 = Object.keys(obj); + return keys4[randInt(keys4.length - 1)]; +} +function randInd(arr) { + return randInt(arr.length - 1); +} +function randIntArr(n2, count) { + return Array(count).fill(n2).map(randInt); +} +function randIntRange(low, high) { + return low + randInt(high - low); +} +function rangedInts(ints) { + if (ints[0] === void 0) return; + return ints.map((str7) => { + if (str7.match(/^\d+-\d+$/)) { + const range = str7.split("-"); + return randIntRange(parseInt(range[0]), parseInt(range[1])); + } else { + if (str7 === "?") return randInt(255); + return parseInt(str7); + } + }); +} +function multiply7(operands, n2) { + if (Array.isArray(operands)) { + return operands.map((o2) => o2 * n2); + } else { + return operands * n2; + } +} +function dist4() { + let x1, y1, x2, y2; + if (arguments.length === 4) { + x1 = arguments[0]; + y1 = arguments[1]; + x2 = arguments[2]; + y2 = arguments[3]; + } else if (arguments.length === 2) { + x1 = arguments[0].x; + y1 = arguments[0].y; + x2 = arguments[1].x; + y2 = arguments[1].y; + } + const dx = x2 - x1; + const dy = y2 - y1; + return sqrt(dx * dx + dy * dy); +} +function dist3d(p1, p22) { + if (p1.buffer) p1 = p1.map((p) => p.toPrecision(4)); + if (p22.buffer) p22 = p22.map((p) => p.toPrecision(4)); + const dx = p1[0] - p22[0]; + const dy = p1[1] - p22[1]; + const dz = p1[2] - p22[2]; + return sqrt(dx * dx + dy * dy + dz * dz); +} +function radians(deg = 0) { + if (isNaN(deg)) deg = 0; + return deg * (PI / 180); +} +function degrees(rad) { + return rad * (180 / PI); +} +function clamp(value, low, high) { + return min4(max4(value, low), high); +} +function wave(phase) { + return sin(phase); +} +function lerp5(a2, b2, amount) { + return a2 + (b2 - a2) * clamp(amount, 0, 1); +} +function map(num, inMin, inMax, outMin, outMax) { + return (num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; +} +function perlin(x, y) { + var grad3 = [ + [1, 1, 0], + [-1, 1, 0], + [1, -1, 0], + [-1, -1, 0], + [1, 0, 1], + [-1, 0, 1], + [1, 0, -1], + [-1, 0, -1], + [0, 1, 1], + [0, -1, 1], + [0, 1, -1], + [0, -1, -1] + ]; + var p = []; + for (var i2 = 0; i2 < 256; i2++) { + p[i2] = Math.floor(Math.random() * 256); + } + var perm = []; + for (var i2 = 0; i2 < 512; i2++) { + perm[i2] = p[i2 & 255]; + } + function dot5(g, x3, y3) { + return g[0] * x3 + g[1] * y3; + } + var F2 = 0.5 * (Math.sqrt(3) - 1); + var s2 = (x + y) * F2; + var i2 = Math.floor(x + s2); + var j = Math.floor(y + s2); + var G2 = (3 - Math.sqrt(3)) / 6; + var t2 = (i2 + j) * G2; + var X0 = i2 - t2; + var Y0 = j - t2; + var x0 = x - X0; + var y0 = y - Y0; + var i1, j1; + if (x0 > y0) { + i1 = 1; + j1 = 0; + } else { + i1 = 0; + j1 = 1; + } + var x1 = x0 - i1 + G2; + var y1 = y0 - j1 + G2; + var x2 = x0 - 1 + 2 * G2; + var y2 = y0 - 1 + 2 * G2; + var ii = i2 & 255; + var jj = j & 255; + var gi0 = perm[ii + perm[jj]] % 12; + var gi1 = perm[ii + i1 + perm[jj + j1]] % 12; + var gi2 = perm[ii + 1 + perm[jj + 1]] % 12; + var t0 = 0.5 - x0 * x0 - y0 * y0; + var n0, n1, n2; + if (t0 < 0) n0 = 0; + else { + t0 *= t0; + n0 = t0 * t0 * dot5(grad3[gi0], x0, y0); + } + var t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 < 0) n1 = 0; + else { + t1 *= t1; + n1 = t1 * t1 * dot5(grad3[gi1], x1, y1); + } + var t22 = 0.5 - x2 * x2 - y2 * y2; + if (t22 < 0) n2 = 0; + else { + t22 *= t22; + n2 = t22 * t22 * dot5(grad3[gi2], x2, y2); + } + return 70 * (n0 + n1 + n2); +} +function arrMax(arr) { + return arr.reduce((top, current) => max4(top, current), -Infinity); +} +function arrCompress(arr, n2) { + return arr.filter((_, index) => (index + 1) % n2 === 0); +} +function timestamp() { + const d2 = /* @__PURE__ */ new Date(); + const pad = (n2, digits = 2) => n2.toString().padStart(digits, "0"); + return ` + ${d2.getFullYear()}. + ${pad(d2.getMonth() + 1)}. + ${pad(d2.getDate())}. + ${pad(d2.getHours())}. + ${pad(d2.getMinutes())}. + ${pad(d2.getSeconds())}. + ${pad(d2.getMilliseconds(), 3)}`.replace(/\s/g, ""); +} +var Track = class { + #values; + #result; + #quantize; + constructor(values2, result) { + this.#values = values2; + this.#result = result; + this.#quantize = Array.isArray(values2); + } + step(progress) { + if (this.#quantize) { + const index = min4( + floor4(progress * this.#values.length), + this.#values.length - 1 + ); + this.#result(this.#values[index]); + } else { + this.#result(lerp5(this.#values.from, this.#values.to, progress)); + } + } +}; +function cleanHexString(h) { + return h.replace("#", "").replace("0x", "").toUpperCase(); +} +function isHexString(h) { + h = cleanHexString(h); + const a2 = parseInt(h, 16); + return a2.toString(16) === h.toLowerCase(); +} +function parseColor(params) { + if (params.length === 0) return params; + const int = parseInt(params[0]); + if (!isNaN(int) || params.length > 2) { + if (params.length === 2 || params.length === 4) { + const alpha = calculateAlpha(params[params.length - 1]); + params[params.length - 1] = alpha.toString(); + } + return rangedInts(params); + } else { + let name = params[0].toLowerCase(); + let alpha = calculateAlpha(params[1]); + if (name === "?") name = anyKey(cssColors2); + if (name.startsWith("fade:")) { + return { + type: "fade", + fadeString: name, + alpha, + originalParams: params + }; + } + const indexColor = parseColorIndex(name); + if (indexColor) { + return [...indexColor, alpha]; + } + if (name in cssColors2) { + return [...cssColors2[name], alpha]; + } else if (name === "erase") { + return [-1, -1, -1, alpha]; + } else if (name === "rainbow") { + return ["rainbow", alpha]; + } else if (name === "zebra") { + return ["zebra", alpha]; + } else { + return [0, 0, 0, alpha]; + } + } +} +function calculateAlpha(alphaParam) { + if (alphaParam === "?") return randIntRange(5, 255); + let alpha = parseFloat(alphaParam); + if (alpha >= 0 && alpha <= 1) { + alpha = round4(alpha * 255); + } else { + alpha = rangedInts([alphaParam]) || 255; + } + return alpha; +} +var cssColors2 = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 134, 11], + darkgray: [169, 169, 169], + darkgrey: [169, 169, 169], + darkgreen: [0, 100, 0], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 250, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + grey: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgrey: [211, 211, 211], + lightgreen: [144, 238, 144], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 221], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [112, 128, 144], + slategrey: [112, 128, 144], + snow: [255, 250, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 50], + // Custom brown colors for AC + darkbrown: [101, 67, 33], + darkerbrown: [62, 39, 35], + darksienna: [139, 90, 43] +}; +var currentRainbowIndex = 0; +var frameRainbowColor = null; +var rainbowFrameAdvanced = false; +var rainbowColors = [ + cssColors2.red, + cssColors2.orange, + cssColors2.yellow, + cssColors2.green, + cssColors2.blue, + cssColors2.indigo, + cssColors2.violet +]; +function rainbow(offset = 0) { + if (!rainbowFrameAdvanced) { + currentRainbowIndex = (currentRainbowIndex + 1) % rainbowColors.length; + frameRainbowColor = rainbowColors[currentRainbowIndex].slice(); + rainbowFrameAdvanced = true; + } + const finalIndex = (currentRainbowIndex + offset) % rainbowColors.length; + const result = rainbowColors[finalIndex]; + return result.slice(); +} +var currentZebraIndex = 0; +var frameZebraColor = null; +var zebraFrameAdvanced = false; +var zebraColors = [ + cssColors2.black, + // [0, 0, 0] + cssColors2.white + // [255, 255, 255] +]; +function zebra(offset = 0) { + if (!zebraFrameAdvanced) { + currentZebraIndex = (currentZebraIndex + 1) % zebraColors.length; + frameZebraColor = zebraColors[currentZebraIndex].slice(); + zebraFrameAdvanced = true; + } + const finalIndex = (currentZebraIndex + offset) % zebraColors.length; + return zebraColors[finalIndex].slice(); +} +function resetZebraCache() { + zebraFrameAdvanced = false; + frameZebraColor = null; +} +function resetRainbowCache() { + rainbowFrameAdvanced = false; + frameRainbowColor = null; +} +function findColor(rgb) { + for (let name in cssColors2) { + if (cssColors2[name][0] === rgb[0] && cssColors2[name][1] === rgb[1] && cssColors2[name][2] === rgb[2]) { + return name; + } + } +} +var organizedColorIndex = [ + // 0-15: Standard 16 web colors (c0=black, c1=white) + "black", + "white", + "red", + "lime", + "blue", + "yellow", + "cyan", + "magenta", + "silver", + "gray", + "maroon", + "olive", + "green", + "purple", + "teal", + "navy", + // 17-32: Additional reds and pinks + "crimson", + "darkred", + "firebrick", + "indianred", + "lightcoral", + "salmon", + "darksalmon", + "lightsalmon", + "pink", + "lightpink", + "hotpink", + "deeppink", + "palevioletred", + "mediumvioletred", + "coral", + "tomato", + // 33-48: Oranges + "orange", + "darkorange", + "orangered", + "chocolate", + "saddlebrown", + "sienna", + "brown", + "rosybrown", + "sandybrown", + "goldenrod", + "darkgoldenrod", + "peru", + "burlywood", + "tan", + "navajowhite", + "bisque", + // 49-64: Yellows and golds + "gold", + "palegoldenrod", + "khaki", + "darkkhaki", + "moccasin", + "wheat", + "lemonchiffon", + "lightgoldenrodyellow", + "lightyellow", + "beige", + "cornsilk", + "blanchedalmond", + "papayawhip", + "antiquewhite", + "linen", + "oldlace", + // 65-80: Greens + "forestgreen", + "darkgreen", + "darkolivegreen", + "darkseagreen", + "limegreen", + "seagreen", + "mediumseagreen", + "springgreen", + "mediumspringgreen", + "palegreen", + "lightgreen", + "lawngreen", + "chartreuse", + "greenyellow", + "yellowgreen", + "olivedrab", + // 81-96: Blues and cyans + "aqua", + "darkturquoise", + "turquoise", + "mediumturquoise", + "paleturquoise", + "lightcyan", + "cadetblue", + "steelblue", + "lightsteelblue", + "powderblue", + "lightblue", + "skyblue", + "lightskyblue", + "deepskyblue", + "dodgerblue", + "cornflowerblue", + // 97-112: More blues + "royalblue", + "mediumblue", + "darkblue", + "midnightblue", + "slateblue", + "darkslateblue", + "mediumslateblue", + "mediumpurple", + "blueviolet", + "indigo", + "darkorchid", + "darkviolet", + "mediumorchid", + "thistle", + "plum", + "violet", + // 113-128: Purples and magentas + "orchid", + "fuchsia", + "darkmagenta", + "mediumvioletred", + "lavenderblush", + "mistyrose", + "lavender", + "ghostwhite", + "azure", + "aliceblue", + "mintcream", + "honeydew", + "seashell", + "ivory", + "floralwhite", + "snow", + // 129-144: Grays and remaining colors + "gainsboro", + "lightgray", + "lightgrey", + "darkgray", + "darkgrey", + "dimgray", + "dimgrey", + "lightslategray", + "lightslategrey", + "slategray", + "slategrey", + "darkslategray", + "darkslategrey", + "whitesmoke", + "rebeccapurple" +]; +var remainingColors = Object.keys(cssColors2).filter( + (color3) => !organizedColorIndex.includes(color3) +); +var completeColorIndex = [...organizedColorIndex, ...remainingColors]; +var paletteColors = { + 0: "rainbow", + // p0 = rainbow + 1: "zebra" + // p1 = zebra + // Add more palette colors here as needed +}; +function getPaletteByIndex(index) { + if (index in paletteColors) { + return paletteColors[index]; + } + return null; +} +var staticColorMap = { + // 0-15: Core web colors (never change these) + 0: [0, 0, 0], + // c0 = black + 1: [255, 255, 255], + // c1 = white + 2: [255, 0, 0], + // c2 = red + 3: [0, 255, 0], + // c3 = lime + 4: [0, 0, 255], + // c4 = blue + 5: [255, 255, 0], + // c5 = yellow + 6: [0, 255, 255], + // c6 = cyan + 7: [255, 0, 255], + // c7 = magenta + 8: [192, 192, 192], + // c8 = silver + 9: [128, 128, 128], + // c9 = gray + 10: [128, 0, 0], + // c10 = maroon + 11: [128, 128, 0], + // c11 = olive + 12: [0, 128, 0], + // c12 = green + 13: [128, 0, 128], + // c13 = purple + 14: [0, 128, 128], + // c14 = teal + 15: [0, 0, 128], + // c15 = navy + // 16-31: Red spectrum + 16: [220, 20, 60], + // c16 = crimson + 17: [139, 0, 0], + // c17 = darkred + 18: [178, 34, 34], + // c18 = firebrick + 19: [205, 92, 92], + // c19 = indianred + 20: [240, 128, 128], + // c20 = lightcoral + 21: [250, 128, 114], + // c21 = salmon + 22: [233, 150, 122], + // c22 = darksalmon + 23: [255, 160, 122], + // c23 = lightsalmon + 24: [255, 192, 203], + // c24 = pink + 25: [255, 182, 193], + // c25 = lightpink + 26: [255, 105, 180], + // c26 = hotpink + 27: [255, 20, 147], + // c27 = deeppink + 28: [219, 112, 147], + // c28 = palevioletred + 29: [199, 21, 133], + // c29 = mediumvioletred + 30: [255, 127, 80], + // c30 = coral + 31: [255, 99, 71], + // c31 = tomato + // 32-47: Orange spectrum + 32: [255, 69, 0], + // c32 = orangered + 33: [255, 140, 0], + // c33 = darkorange + 34: [255, 165, 0], + // c34 = orange + 35: [255, 215, 0], + // c35 = gold + 36: [255, 218, 185], + // c36 = peachpuff + 37: [255, 228, 196], + // c37 = bisque + 38: [255, 239, 213], + // c38 = papayawhip + 39: [255, 228, 181], + // c39 = moccasin + 40: [255, 222, 173], + // c40 = navajowhite + 41: [245, 222, 179], + // c41 = wheat + 42: [222, 184, 135], + // c42 = burlywood + 43: [210, 180, 140], + // c43 = tan + 44: [188, 143, 143], + // c44 = rosybrown + 45: [205, 133, 63], + // c45 = peru + 46: [244, 164, 96], + // c46 = sandybrown + 47: [160, 82, 45], + // c47 = saddlebrown + // 48-63: Yellow/Green spectrum + 48: [255, 248, 220], + // c48 = cornsilk + 49: [255, 255, 240], + // c49 = ivory + 50: [255, 250, 205], + // c50 = lemonchiffon + 51: [250, 250, 210], + // c51 = lightgoldenrodyellow + 52: [240, 230, 140], + // c52 = khaki + 53: [238, 232, 170], + // c53 = palegoldenrod + 54: [189, 183, 107], + // c54 = darkkhaki + 55: [154, 205, 50], + // c55 = yellowgreen + 56: [124, 252, 0], + // c56 = lawngreen + 57: [127, 255, 0], + // c57 = chartreuse + 58: [173, 255, 47], + // c58 = greenyellow + 59: [50, 205, 50], + // c59 = limegreen + 60: [152, 251, 152], + // c60 = palegreen + 61: [144, 238, 144], + // c61 = lightgreen + 62: [0, 250, 154], + // c62 = mediumspringgreen + 63: [0, 255, 127], + // c63 = springgreen + // 64-79: Green spectrum + 64: [46, 125, 50], + // c64 = forestgreen + 65: [34, 139, 34], + // c65 = forestgreen + 66: [0, 100, 0], + // c66 = darkgreen + 67: [85, 107, 47], + // c67 = darkolivegreen + 68: [107, 142, 35], + // c68 = olivedrab + 69: [102, 205, 170], + // c69 = mediumaquamarine + 70: [127, 255, 212], + // c70 = aquamarine + 71: [176, 196, 222], + // c71 = lightsteelblue + 72: [175, 238, 238], + // c72 = paleturquoise + 73: [0, 206, 209], + // c73 = darkturquoise + 74: [72, 209, 204], + // c74 = mediumturquoise + 75: [64, 224, 208], + // c75 = turquoise + 76: [0, 139, 139], + // c76 = darkcyan + 77: [95, 158, 160], + // c77 = cadetblue + 78: [70, 130, 180], + // c78 = steelblue + 79: [176, 224, 230], + // c79 = powderblue + // 80-95: Blue spectrum + 80: [173, 216, 230], + // c80 = lightblue + 81: [135, 206, 235], + // c81 = skyblue + 82: [135, 206, 250], + // c82 = lightskyblue + 83: [0, 191, 255], + // c83 = deepskyblue + 84: [30, 144, 255], + // c84 = dodgerblue + 85: [100, 149, 237], + // c85 = cornflowerblue + 86: [123, 104, 238], + // c86 = mediumslateblue + 87: [106, 90, 205], + // c87 = slateblue + 88: [72, 61, 139], + // c88 = darkslateblue + 89: [25, 25, 112], + // c89 = midnightblue + 90: [0, 0, 139], + // c90 = darkblue + 91: [0, 0, 205], + // c91 = mediumblue + 92: [65, 105, 225], + // c92 = royalblue + 93: [138, 43, 226], + // c93 = blueviolet + 94: [75, 0, 130], + // c94 = indigo + 95: [72, 0, 72], + // c95 = darkmagenta + // 96-111: Purple/Violet spectrum + 96: [153, 50, 204], + // c96 = darkorchid + 97: [186, 85, 211], + // c97 = mediumorchid + 98: [218, 112, 214], + // c98 = orchid + 99: [221, 160, 221], + // c99 = plum + 100: [238, 130, 238], + // c100 = violet + 101: [255, 0, 255], + // c101 = fuchsia (same as magenta) + 102: [208, 32, 144], + // c102 = violetred + 103: [199, 21, 133], + // c103 = mediumvioletred + 104: [219, 112, 147], + // c104 = palevioletred + 105: [255, 105, 180], + // c105 = hotpink + 106: [255, 20, 147], + // c106 = deeppink + 107: [220, 20, 60], + // c107 = crimson + 108: [139, 69, 19], + // c108 = saddlebrown + 109: [160, 82, 45], + // c109 = saddlebrown + 110: [205, 133, 63], + // c110 = peru + 111: [222, 184, 135], + // c111 = burlywood + // 112-127: Browns and earth tones + 112: [245, 245, 220], + // c112 = beige + 113: [255, 248, 220], + // c113 = cornsilk + 114: [255, 235, 205], + // c114 = blanchedalmond + 115: [245, 222, 179], + // c115 = wheat + 116: [255, 228, 181], + // c116 = moccasin + 117: [255, 218, 185], + // c117 = peachpuff + 118: [210, 180, 140], + // c118 = tan + 119: [188, 143, 143], + // c119 = rosybrown + 120: [244, 164, 96], + // c120 = sandybrown + 121: [205, 133, 63], + // c121 = peru + 122: [160, 82, 45], + // c122 = saddlebrown + 123: [139, 69, 19], + // c123 = saddlebrown + 124: [101, 67, 33], + // c124 = darkbrown + 125: [62, 39, 35], + // c125 = darkerbrown + 126: [139, 90, 43], + // c126 = darksienna + 127: [165, 42, 42] + // c127 = brown +}; +function parseColorIndex(indexString) { + if (typeof indexString === "string") { + if (indexString.startsWith("c")) { + const index = parseInt(indexString.substring(1), 10); + if (!isNaN(index) && staticColorMap[index]) { + const color3 = staticColorMap[index]; + if (Array.isArray(color3) && color3.length >= 3 && color3[0] !== void 0 && color3[1] !== void 0 && color3[2] !== void 0) { + return color3; + } + } + } else if (indexString.startsWith("p")) { + const index = parseInt(indexString.substring(1), 10); + if (!isNaN(index)) { + const palette = getPaletteByIndex(index); + if (palette === "rainbow") { + return ["rainbow"]; + } else if (palette === "zebra") { + return ["zebra"]; + } + } + } + } + return null; +} +function blend(dst, src, alphaIn = 1) { + if (src[3] === 0) return; + if (src[0] === -1) { + const normalizedAlpha = 1 - src[3] / 255; + dst[3] *= normalizedAlpha; + return; + } + if (src[3] === void 0) src[3] = 255; + const alpha = src[3] * alphaIn + 1; + const invAlpha = 256 - alpha; + dst[0] = alpha * src[0] + invAlpha * dst[0] >> 8; + dst[1] = alpha * src[1] + invAlpha * dst[1] >> 8; + dst[2] = alpha * src[2] + invAlpha * dst[2] >> 8; + dst[3] = dst[3] + alpha; + return dst; +} +function shiftRGB(a2, b2, step, mode = "lerp", range = 255) { + const low = range === 255 ? [1, 10] : [0.01, 0.05]; + const high = range === 255 ? [245, 250] : [0.92, 0.95]; + if (mode === "add" || mode === "subtract") { + if (mode === "subtract") step *= -1; + const shifted = [ + clamp(a2[0] + a2[0] * step, randIntRange(...low), randIntRange(...high)), + clamp(a2[1] + a2[1] * step, randIntRange(...low), randIntRange(...high)), + clamp(a2[2] + a2[2] * step, randIntRange(...low), randIntRange(...high)), + range + ]; + cc; + } else if (mode === "step") { + const shifted = [ + towards(a2[0], b2[0], step), + towards(a2[1], b2[1], step), + towards(a2[2], b2[2], step), + range + ]; + console.log(a2[0], b2[0], step); + return shifted; + } else { + const shifted = [ + lerp5(a2[0], b2[0], step), + lerp5(a2[1], b2[1], step), + lerp5(a2[2], b2[2], step), + range + ]; + return shifted; + } +} +function towards(from, to, by) { + return from < to ? min4(from + by, to) : max4(from - by, to); +} +function rgbToHexStr(r2, g, b2, prefix = "") { + return prefix + (1 << 24 | r2 << 16 | g << 8 | b2).toString(16).slice(1); +} +function hexToRgb(h) { + const int = typeof h === "string" ? parseInt(cleanHexString(h), 16) : h; + const out = [int >> 16 & 255, int >> 8 & 255, int & 255]; + return out; +} +function saturate(rgb, amount = 1) { + const hadAlpha = rgb.length === 4; + const alpha = rgb[3]; + rgb = rgb.slice(0, 3); + const grey = lightness(rgb) * 255; + const [low, mid, high] = getLowestMiddleHighest(rgb); + if (low.val === high.val) return rgb; + const saturationRange = round4(min4(255 - grey, grey)); + const maxChange = min4(255 - high.val, low.val); + const changeAmount = min4(saturationRange * amount, maxChange); + const middleValueRatio = (grey - mid.val) / (grey - high.val); + const out = []; + out[high.index] = round4(high.val + changeAmount); + out[low.index] = round4(low.val - changeAmount); + out[mid.index] = round4(grey + (out[high.index] - grey) * middleValueRatio); + if (hadAlpha) out[3] = alpha; + return out; +} +function desaturate(rgb, amount = 1) { + const hadAlpha = rgb.length === 4; + const alpha = rgb[3]; + rgb = rgb.slice(0, 3); + const [low, mid, high] = getLowestMiddleHighest(rgb); + const grey = lightness(rgb) * 255; + if (low.val === high.val) return rgb; + const saturationRange = round4(min4(255 - grey, grey)); + const maxChange = grey - low.val; + const changeAmount = min4(saturationRange * amount, maxChange); + const middleValueRatio = (grey - mid.val) / (grey - high.val); + const out = []; + out[high.index] = round4(high.val - changeAmount); + out[low.index] = round4(low.val + changeAmount); + out[mid.index] = round4(grey + (out[high.index] - grey) * middleValueRatio); + if (hadAlpha) out[3] = alpha; + return out; +} +function lightness(rgb) { + const highest = max4(...rgb); + const lowest = min4(...rgb); + return (highest + lowest) / 2 / 255; +} +function getLowestMiddleHighest(rgb) { + let high = { val: 0, index: -1 }; + let low = { val: Infinity, index: -1 }; + rgb.map((val, index) => { + if (val > high.val) { + high = { val, index }; + } + if (val < low.val) { + low = { val, index }; + } + }); + const mid = { index: 3 - high.index - low.index }; + mid.val = rgb[mid.index]; + return [low, mid, high]; +} +function rgbToHsl(r2, g, b2) { + r2 /= 255, g /= 255, b2 /= 255; + const max9 = Math.max(r2, g, b2), min10 = Math.min(r2, g, b2); + let h, s2, l2 = (max9 + min10) / 2; + if (max9 == min10) { + h = s2 = 0; + } else { + const d2 = max9 - min10; + s2 = l2 > 0.5 ? d2 / (2 - max9 - min10) : d2 / (max9 + min10); + switch (max9) { + case r2: + h = (g - b2) / d2 + (g < b2 ? 6 : 0); + break; + case g: + h = (b2 - r2) / d2 + 2; + break; + case b2: + h = (r2 - g) / d2 + 4; + break; + } + h /= 6; + } + return [h * 360, s2 * 100, l2 * 100]; +} +function hslToRgb(h, s2, l2) { + s2 /= 100; + l2 /= 100; + const c4 = (1 - Math.abs(2 * l2 - 1)) * s2; + const x = c4 * (1 - Math.abs(h / 60 % 2 - 1)); + const m = l2 - c4 / 2; + let r2 = 0, g = 0, b2 = 0; + if (0 <= h && h < 60) { + r2 = c4; + g = x; + b2 = 0; + } else if (60 <= h && h < 120) { + r2 = x; + g = c4; + b2 = 0; + } else if (120 <= h && h < 180) { + r2 = 0; + g = c4; + b2 = x; + } else if (180 <= h && h < 240) { + r2 = 0; + g = x; + b2 = c4; + } else if (240 <= h && h < 300) { + r2 = x; + g = 0; + b2 = c4; + } else if (300 <= h && h < 360) { + r2 = c4; + g = 0; + b2 = x; + } + r2 = Math.round((r2 + m) * 255); + g = Math.round((g + m) * 255); + b2 = Math.round((b2 + m) * 255); + return [r2, g, b2]; +} + +// public/aesthetic.computer/lib/help.mjs +var { floor: floor5 } = Math; +function choose() { + return arguments[randInt(arguments.length - 1)]; +} +function flip() { + return choose(true, false); +} +function every(obj, value) { + Object.keys(obj).forEach((k) => obj[k] = value); +} +function anyIndex(array) { + return randInt(array.length - 1); +} +function any(objOrArray) { + if (Array.isArray(objOrArray)) { + return objOrArray[anyIndex(objOrArray)]; + } else { + const keys4 = Object.keys(objOrArray); + return objOrArray[keys4[keys4.length * Math.random() << 0]]; + } +} +function shuffleInPlace(array) { + let currentIndex = array.length, randomIndex; + while (currentIndex != 0) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], + array[currentIndex] + ]; + } + return array; +} +function each(obj, fn) { + Object.entries(obj).forEach(([key, obj2]) => fn(obj2, key)); +} +function repeat(n2, fn) { + const reps = []; + for (let i2 = 0; i2 < floor5(n2); i2 += 1) reps.push(fn(i2)); + return reps; +} +function findKeyAndValue(obj, k, v2) { + return obj[Object.keys(obj).find((key) => obj[key][k] === v2)]; +} +function nonvalue(i2) { + return i2 === void 0 || i2 === null || isNaN(i2); +} +function resampleArray(inputArray, newLength) { + const inputLength = inputArray.length; + const outputArray = []; + for (let i2 = 0; i2 < newLength; i2++) { + const index = floor5(i2 / newLength * inputLength); + outputArray.push(inputArray[index]); + } + return outputArray; +} + +// public/aesthetic.computer/lib/geo.mjs +var { abs: abs2, cos: cos2, sin: sin2, floor: floor6, sqrt: sqrt2 } = Math; +var Circle = class { + x; + y; + radius; + constructor(x, y, radius = 8) { + this.x = x; + this.y = y; + this.radius = radius; + } + // Determines whether the circle is intersecting the given line. + // function ( line, ) + // Returns a random (x, y) point within the circle by recursively generating + // random points within a bounding box and checking to see if they are within + // the radius. + random() { + const sq = [-this.radius, this.radius]; + const np = { + x: this.x + randIntRange(...sq), + y: this.y + randIntRange(...sq) + }; + if (dist4(this.x, this.y, np.x, np.y) < this.radius) { + return np; + } else { + return this.random(this.radius); + } + } +}; +var Box = class _Box { + x = 0; + y = 0; + w = 1; + h = 1; + // Params: + // x, y, w, h + // x, y, s + // {x, y}, s + constructor() { + if (arguments.length === 4) { + this.x = arguments[0]; + this.y = arguments[1]; + this.w = arguments[2]; + this.h = arguments[3]; + } else if (arguments.length === 3) { + this.x = arguments[0]; + this.y = arguments[1]; + this.w = arguments[2]; + this.h = this.w; + } else if (arguments.length === 2) { + this.x = arguments[0].x; + this.y = arguments[0].y; + this.w = arguments[1]; + this.h = this.w; + } + if (this.w === 0) this.w = 1; + if (this.h === 0) this.h = 1; + } + // Yields a new box that is a copy of an existing old one. + static from(box2) { + if (Array.isArray(box2)) { + return new _Box(...box2); + } else { + return new _Box(box2.x, box2.y, box2.w || box2.width, box2.h || box2.height); + } + } + // Compute area of box. + get area() { + return abs2(this.w * this.h); + } + // Compute center point of box. + get center() { + return { + x: this.x + this.w / 2, + y: this.y + this.h / 2 + }; + } + // Yields a box where x, y is at the top left and w, h are positive. + get abs() { + let { x, y, w, h } = this; + if (w < 0) { + x += w + 1; + w = Math.abs(w); + } + if (h < 0) { + y += h + 1; + h = Math.abs(h); + } + return new _Box(x, y, w, h); + } + // The top side of the box. (same as y) + get top() { + return this.y; + } + // Calculates a y value, representing the bottom of the box. + // Note: Returns y if the height is 1. + get bottom() { + return this.h === 1 ? this.y : this.y + this.h; + } + // The left side of the box. (same as x) + get left() { + return this.x; + } + // Calculates an x value, representing the right of the box. + // Note: Returns x if the width is 1. + get right() { + return this.w === 1 ? this.x : this.x + this.w; + } + // All four corners. + get topLeft() { + return { x: this.left, y: this.top }; + } + get topRight() { + return { x: this.right, y: this.top }; + } + get bottomLeft() { + return { x: this.left, y: this.bottom }; + } + get bottomRight() { + return { x: this.right, y: this.bottom }; + } + // Scales a box and returns a copy. + scale(n2) { + return new _Box(this.x, this.y, this.w * n2, this.h * n2); + } + // Crops one box to another. + crop(toX, toY, toW, toH) { + let { x, y, w, h } = this; + if (x >= toW || y >= toH) return; + if (x < toX) { + w += x; + x = toX; + } + if (x + w > toW) w = toW - x; + if (y < toY) { + h += y; + y = toY; + } + if (y + h > toH) h = toH - y; + return new _Box(x, y, w, h); + } + // Moves the box by x and y. + move({ x, y }) { + this.x += x; + this.y += y; + } + // Returns true if this box contains the point {x, y}. + contains(point2 = { x: void 0, y: void 0 }) { + const { x, y } = point2; + return this.x <= x && x <= this.x + this.w && this.y <= y && y <= this.y + this.h; + } + // Returns true if this box contains NO points in `arr`. + containsNone(arr) { + if (!arr) return true; + return arr.every((o2) => this.contains(o2) === false); + } + // Returns true if this box contains the point `xy` from `arr`. + onlyContains(index, arr) { + const newArr = arr.slice(0, index).concat(arr.slice(index + 1)); + return this.contains(arr[index]) && this.containsNone(newArr); + } + // The opposite of contains. + misses(o2) { + return !this.contains(o2); + } + // Grow a box from the center by `n` units on every side. + // And returns a copy. + grow(n2) { + return new _Box(this.x - n2, this.y - n2, this.w + n2 * 2, this.h + n2 * 2); + } + // Checks whether two boxes are making contact and / or intersecting. + // 📓 This might be a little unperforant with tons of boxes. 23.06.29.12.48 + contact(box2) { + return box2 && (this.contains(box2.topLeft) || this.contains(box2.topRight) || this.contains(box2.bottomRight) || this.contains(box2.bottomLeft)); + } + // Returns true if the boxes match exactly. + equal(box2) { + return this.x === box2.x && this.y === box2.y && this.w === box2.w && this.h === box2.h; + } +}; +var Grid = class { + box; + scale; + // TODO: Could rotation eventually be added here? 2021.12.08.10.51 + transform; + #halfScale; + centerOffset; + constructor(x, y, w, h, s2 = 1) { + this.box = new Box(x, y, w, h); + this.scale = s2; + this.transform = { scale: this.scale }; + this.#halfScale = this.scale / 2; + this.centerOffset = floor6(this.#halfScale); + } + // Loop through every point in the grid, starting from the top left, and + // applying a callback. + each(fun) { + for (let x = 0; x < this.box.w; x += 1) { + for (let y = 0; y < this.box.h; y += 1) { + fun(x, y, x + y * this.box.w); + } + } + } + // Returns unscaled point `{x, y}` in `grid` for given display coordinate + // `pos`, or `false` if `pos` is outside of `grid`. + under({ x, y }, cb) { + const { scale: scale7, box: box2 } = this; + const gx = floor6((x - box2.x) / scale7); + const gy = floor6((y - box2.y) / scale7); + const gridSquare = { + x: box2.x + gx * scale7, + y: box2.y + gy * scale7, + w: scale7, + h: scale7, + gx, + gy, + in: this.scaled.contains({ x, y }) + }; + if (gridSquare.in && cb) cb(gridSquare); + return gridSquare; + } + // Returns floored display coordinates from local, untransformed ones. + get(x, y) { + return [ + floor6(this.box.x + x * this.scale), + floor6(this.box.y + y * this.scale) + ]; + } + // Yields the grid's transformed bounding box according to `scale`. + get scaled() { + return new Box( + this.box.x, + this.box.y, + this.box.w * this.scale, + this.box.h * this.scale + ); + } + // Yields the projected "center" x, y point of the whole grid. + middle() { + return this.get(floor6(this.box.w / 2), floor6(this.box.h / 2)); + } + center(x, y) { + const scaled = this.get(x, y); + scaled[0] += Math.floor(this.#halfScale); + scaled[1] += Math.floor(this.#halfScale); + return scaled; + } + // Yields an array of offset points that can be plotted to mark the center of + // each grid square. (Useful for editors, development and debugging.) + // Tries to find the exact center point, but if that doesn't exist then + // this function produces a 2x2 grid of pixels in the center. + get centers() { + const o2 = this.centerOffset; + let points = []; + if (this.#halfScale % 1 === 0.5 && this.#halfScale > 0.5) { + points.push({ x: o2, y: o2 }); + } else if (this.scale >= 4) { + points.push( + { x: o2, y: o2 }, + { x: o2 - 1, y: o2 - 1 }, + { x: o2 - 1, y: o2 }, + { x: o2, y: o2 - 1 } + ); + } + return points; + } +}; +var DirtyBox = class { + box; + #left; + #top; + #right; + #bottom; + soiled = false; + constructor() { + this.box = new Box(0, 0, 0); + } + soil({ x, y }) { + if (this.#left === void 0) { + this.#left = x; + this.#right = this.#left; + } + if (this.#top === void 0) { + this.#top = y; + this.#bottom = this.#top; + } + if (x < this.#left) this.#left = x; + if (y < this.#top) this.#top = y; + if (x > this.#right) this.#right = x; + if (y > this.#bottom) this.#bottom = y; + this.box.x = this.#left; + this.box.y = this.#top; + this.box.w = this.#right - this.#left + 1; + this.box.h = this.#bottom - this.#top + 1; + this.soiled = true; + } + // Crops pixels from an image and returns the new one. + // - `image` has { width, height, pixels } + crop(image) { + const b2 = this.croppedBox(image); + const p = image.pixels; + const newP = new Uint8ClampedArray(b2.w * b2.h * 4); + for (let row = 0; row < b2.h; row += 1) { + const index = (b2.x + (b2.y + row) * image.width) * 4; + newP.set(p.subarray(index, index + b2.w * 4), row * b2.w * 4); + } + return newP; + } + croppedBox(image) { + return this.box.crop(0, 0, image.width, image.height); + } +}; +function linePointsFromAngle(x1, y1, dist5, degrees2) { + const x2 = x1 + dist5 * cos2(radians(degrees2)); + const y2 = y1 + dist5 * sin2(radians(degrees2)); + return [x1, y1, x2, y2]; +} +function pointFrom(x, y, angle3, dist5) { + return [x + dist5 * cos2(radians(angle3)), y + dist5 * sin2(radians(angle3))]; +} +var Race = class { + pos; + step; + goal; + last; + speed; + dist = 0; + quantizer; + constructor(opts = { quantized: true }) { + this.speed = opts.speed || 20; + this.step = opts.step || 5e-3; + if (opts.quantized) this.quantizer = new Quantizer({ step: this.step }); + } + // Should also reset. + start(point2) { + if (point2.length === 2) point2.push(0, 0); + this.quantizer?.start(point2); + this.pos = clone3(point2); + this.goal = clone3(point2); + this.last = clone3(point2); + this.dist = 0; + } + to(point2 = this.goal) { + if (!point2) return; + if (point2.length === 2) point2.push(0, 0); + this.goal = point2; + if (!this.pos) return false; + let out; + if (this.quantizer) { + const newPos = lerp2( + create3(), + this.pos, + point2, + 0.01 * this.speed + ); + out = this.quantizer.to(newPos); + this.pos = newPos; + } else { + const newPos = lerp2( + create3(), + this.pos, + point2, + 0.01 * this.speed + ); + this.dist += dist2(this.pos, newPos); + this.pos = newPos; + if (this.dist >= this.step) { + out = { + last: clone3(this.last), + current: this.pos, + out: [this.last, this.pos] + }; + this.dist -= this.step; + this.last = this.pos; + } else { + out = { last: this.last, current: this.pos }; + } + } + return out; + } +}; +var Quantizer = class { + pos; + step; + dist = 0; + constructor(opts) { + this.step = opts.step; + } + // Returns an array of [lastPoint, nextPoint...] + to(point2) { + if (!this.pos) return false; + let out = []; + this.dist = dist2(this.pos, point2); + let lastPoint = this.pos; + while (this.dist >= this.step) { + const nextPoint = lerp2( + create3(), + lastPoint, + point2, + this.step / this.dist + ); + out.push(lastPoint, nextPoint); + lastPoint = nextPoint; + this.dist -= this.step; + } + this.pos = lastPoint; + return { last: this.pos, current: point2, out }; + } + start(point2) { + this.pos = clone3(point2); + } +}; + +// public/aesthetic.computer/dep/nanoid/nanoid.js +var random6 = (bytes) => crypto.getRandomValues(new Uint8Array(bytes)); +var customRandom = (alphabet2, defaultSize, getRandom) => { + let mask2 = (2 << Math.log(alphabet2.length - 1) / Math.LN2) - 1; + let step = -~(1.6 * mask2 * defaultSize / alphabet2.length); + return (size = defaultSize) => { + let id = ""; + while (true) { + let bytes = getRandom(step); + let j = step; + while (j--) { + id += alphabet2[bytes[j] & mask2] || ""; + if (id.length === size) return id; + } + } + }; +}; +var customAlphabet = (alphabet2, size = 21) => customRandom(alphabet2, size, random6); +var nanoid = (size = 21) => crypto.getRandomValues(new Uint8Array(size)).reduce((id, byte) => { + byte &= 63; + if (byte < 36) { + id += byte.toString(36); + } else if (byte < 62) { + id += (byte - 26).toString(36).toUpperCase(); + } else if (byte > 62) { + id += "-"; + } else { + id += "_"; + } + return id; +}, ""); + +// public/aesthetic.computer/lib/graph.mjs +var { round: round5, sign: mathSign, abs: abs3, ceil: ceil5, floor: floor7, sin: sin3, cos: cos3, min: min5, max: max5, sqrt: sqrt3, PI: PI2 } = Math; +var width; +var height; +var pixels; +var depthBuffer = []; +var depthEnabled = true; +function clearDepthBuffer() { + if (!depthEnabled) return; + const needed = width * height; + if (needed === 0) return; + if (depthBuffer.length !== needed) depthBuffer.length = needed; + depthBuffer.fill(Number.MAX_VALUE); +} +var writeBuffer = []; +var c = [255, 255, 255, 255]; +var c2 = null; +var panTranslation = { x: 0, y: 0 }; +var activeMask; +var skips = []; +var bitmapPixelCache = typeof WeakMap !== "undefined" ? /* @__PURE__ */ new WeakMap() : null; +var bitmapPixelCanvas = null; +var bitmapPixelCtx = null; +function getBitmapExtractionContext(width2, height2) { + if (typeof OffscreenCanvas !== "undefined") { + if (!(bitmapPixelCanvas instanceof OffscreenCanvas)) { + bitmapPixelCanvas = new OffscreenCanvas(width2, height2); + bitmapPixelCtx = bitmapPixelCanvas.getContext("2d"); + } + if (bitmapPixelCanvas.width !== width2) bitmapPixelCanvas.width = width2; + if (bitmapPixelCanvas.height !== height2) bitmapPixelCanvas.height = height2; + if (!bitmapPixelCtx) bitmapPixelCtx = bitmapPixelCanvas.getContext("2d"); + return bitmapPixelCtx; + } + if (typeof document !== "undefined") { + if (!bitmapPixelCanvas || typeof bitmapPixelCanvas.getContext !== "function") { + bitmapPixelCanvas = document.createElement("canvas"); + } + if (bitmapPixelCanvas.width !== width2) bitmapPixelCanvas.width = width2; + if (bitmapPixelCanvas.height !== height2) bitmapPixelCanvas.height = height2; + bitmapPixelCtx = bitmapPixelCanvas.getContext("2d"); + return bitmapPixelCtx; + } + return null; +} +function ensureBufferPixels(buffer) { + if (!buffer) return null; + if (buffer.pixels && buffer.pixels.length) return buffer.pixels; + if (bitmapPixelCache && bitmapPixelCache.has(buffer)) { + return bitmapPixelCache.get(buffer); + } + const isImageBitmap = typeof ImageBitmap !== "undefined" && buffer instanceof ImageBitmap; + const isImageData = typeof ImageData !== "undefined" && buffer instanceof ImageData; + if (isImageData) { + const pixels2 = buffer.data; + bitmapPixelCache?.set(buffer, pixels2); + return pixels2; + } + if (isImageBitmap) { + const ctx = getBitmapExtractionContext(buffer.width, buffer.height); + if (!ctx) return null; + ctx.clearRect(0, 0, buffer.width, buffer.height); + ctx.drawImage(buffer, 0, 0); + try { + const imageData = ctx.getImageData(0, 0, buffer.width, buffer.height); + const pixels2 = imageData.data; + bitmapPixelCache?.set(buffer, pixels2); + return pixels2; + } catch (err) { + console.warn("\u{1F3A8} Unable to extract pixels from ImageBitmap", err); + return null; + } + } + if (buffer.data && buffer.data.length) { + return buffer.data; + } + return null; +} +var forceReplaceMode = false; +function setForceReplaceMode(enabled = false) { + const previous = forceReplaceMode; + forceReplaceMode = !!enabled; + return previous; +} +function withForceReplaceMode(callback) { + const previous = setForceReplaceMode(true); + try { + return callback?.(); + } finally { + setForceReplaceMode(previous); + } +} +var fadeMode = false; +var fadeColors = []; +var fadeDirection = "horizontal"; +var fadeNeat = false; +var graphPerf2 = { + enabled: false, + functions: /* @__PURE__ */ new Map(), + lastFPS: 0, + // Track current FPS for frame skipping + track(name, duration) { + if (!this.enabled) return; + if (!this.functions.has(name)) { + this.functions.set(name, { count: 0, totalTime: 0, maxTime: 0 }); + } + const stats = this.functions.get(name); + stats.count++; + stats.totalTime += duration; + stats.maxTime = Math.max(stats.maxTime, duration); + if (duration > 15) { + console.warn(`\u{1F40C} SLOW GRAPH FUNCTION: ${name} took ${duration.toFixed(2)}ms`); + } + }, + reset() { + this.functions.clear(); + }, + getStats() { + const stats = []; + this.functions.forEach((data, name) => { + stats.push({ + name, + count: data.count, + totalTime: data.totalTime, + avgTime: data.totalTime / data.count, + maxTime: data.maxTime + }); + }); + return stats.sort((a2, b2) => b2.totalTime - a2.totalTime); + } +}; +if (typeof window !== "undefined") { + window.graphPerf = graphPerf2; +} +if (typeof globalThis !== "undefined") { + globalThis.graphPerf = graphPerf2; +} +var currentKidLispContext = null; +var currentRainbowColor = null; +var currentZebraColor = null; +var debug = false; +var matrixChunkyDebugCount = 0; +function matrixDebugEnabled() { + if (typeof window !== "undefined" && window?.acMatrixDebug) return true; + if (typeof globalThis !== "undefined" && globalThis?.acMatrixDebug) return true; + return false; +} +function inkFloodLoggingEnabled() { + if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_COLORS) return true; + if (typeof process !== "undefined" && process.env?.AC_LOG_INK_COLORS === "1") return true; + return false; +} +function inkFloodLogPrefix() { + let label = null; + if (typeof process !== "undefined" && process.env?.AC_LOG_INK_LABEL) { + label = process.env.AC_LOG_INK_LABEL; + } else if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_LABEL) { + label = globalThis.AC_LOG_INK_LABEL; + } + return label ? `[${label}] ` : ""; +} +function cloneColorForLog(color3) { + if (Array.isArray(color3)) return Array.from(color3); + return color3; +} +function cloneValueForLog(value) { + if (Array.isArray(value)) return Array.from(value); + if (value && typeof value === "object") { + try { + return JSON.parse(JSON.stringify(value)); + } catch (err) { + return String(value); + } + } + return value; +} +function isFadeColor(color3) { + return Array.isArray(color3) && color3.length > 0 && typeof color3[0] === "string" && color3[0].startsWith("fade:"); +} +function parseLocalFade(fadeType, alpha = 255) { + let processedFadeType = fadeType; + if (fadeType.includes("frame") && currentKidLispContext && currentKidLispContext.frameCount !== void 0) { + processedFadeType = fadeType.replace(/frame/g, currentKidLispContext.frameCount.toString()); + } + const colorNames = processedFadeType.split("-"); + const colors = []; + let rainbowBaseOffset = 0; + let zebraBaseOffset = 0; + for (let i2 = 0; i2 < colorNames.length; i2++) { + const colorName = colorNames[i2]; + if (colorName === "rainbow") { + const consecutiveOffset = i2 > 0 && colorNames[i2 - 1] === "rainbow" ? 1 : 0; + colors.push(["rainbow", 255, 255, 255, alpha, rainbowBaseOffset + consecutiveOffset]); + if (consecutiveOffset > 0) rainbowBaseOffset++; + } else if (colorName === "zebra") { + const consecutiveOffset = i2 > 0 && colorNames[i2 - 1] === "zebra" ? 1 : 0; + colors.push(["zebra", 255, 255, 255, alpha, zebraBaseOffset + consecutiveOffset]); + if (consecutiveOffset > 0) zebraBaseOffset++; + } else if (/^c\d+$/i.test(colorName)) { + const indexColor = parseColorIndex(colorName.toLowerCase()); + if (indexColor) { + colors.push([...indexColor.slice(0, 3), alpha]); + } + } else { + const color3 = cssColors2[colorName]; + if (color3) { + colors.push([...color3.slice(0, 3), alpha]); + } else { + colors.push([128, 128, 128, alpha]); + } + } + } + return colors.length >= 2 ? colors : null; +} +function parseFadeColor(color3) { + if (!isFadeColor(color3)) return null; + const fadeString = color3[0]; + const alpha = color3[1] || 255; + const parts = fadeString.split(":"); + if (parts.length < 2 || parts.length > 5 || parts[0] !== "fade") { + return null; + } + let isNeat = true; + let fadeType = parts[1]; + let direction = parts[2] || "horizontal"; + if (parts[1] === "dirty" && parts[2]) { + isNeat = false; + fadeType = parts[2]; + direction = parts[3] || "horizontal"; + } else if (parts[2] === "dirty") { + isNeat = false; + direction = "horizontal"; + } else if (parts[3] === "dirty") { + isNeat = false; + direction = parts[2]; + } else if (parts.includes("dirty")) { + isNeat = false; + const filteredParts = parts.filter((p) => p !== "dirty"); + if (filteredParts.length >= 2) { + fadeType = filteredParts[1]; + direction = filteredParts[2] || "horizontal"; + } + } else if (parts[1] === "neat" && parts[2]) { + isNeat = true; + fadeType = parts[2]; + direction = parts[3] || "horizontal"; + } else if (parts[2] === "neat") { + isNeat = true; + direction = "horizontal"; + } else if (parts[3] === "neat") { + isNeat = true; + direction = parts[2]; + } else if (parts.includes("neat")) { + isNeat = true; + const filteredParts = parts.filter((p) => p !== "neat"); + if (filteredParts.length >= 2) { + fadeType = filteredParts[1]; + direction = filteredParts[2] || "horizontal"; + } + } + const fadeColors2 = parseLocalFade(fadeType, alpha); + if (!fadeColors2) return null; + const evaluatedDirection = evaluateFadeDirection(direction); + return { + colors: fadeColors2, + direction: evaluatedDirection, + // Use evaluated direction (can be numeric angle) + neat: isNeat + // Local neat flag, no global state + }; +} +function getLocalFadeColor(t2, x = 0, y = 0, fadeInfo) { + if (!fadeInfo) { + return c.slice(); + } + const colors = Array.isArray(fadeInfo) ? fadeInfo : fadeInfo.colors; + if (!colors || colors.length < 2) { + return c.slice(); + } + t2 = Math.max(0, Math.min(1, t2)); + const segments = colors.length - 1; + const segment = Math.min(Math.floor(t2 * segments), segments - 1); + const localT = t2 * segments - segment; + const color1 = colors[segment]; + const color22 = colors[segment + 1] || colors[segment]; + if (!color1 || !color22) { + console.warn("\u{1F6A8} getLocalFadeColor: Invalid color data", { segment, colors, t: t2 }); + return c.slice(); + } + if (color1[0] === "zebra" || color22[0] === "zebra") { + if (color1[0] === "zebra" && color22[0] === "zebra") { + const offset1 = color1[5] || 0; + const offset2 = color22[5] || 0; + const zebra1 = zebra(offset1); + const zebra2 = zebra(offset2); + const alpha1 = color1[4] || 255; + const alpha2 = color22[4] || 255; + const alpha = Math.round(alpha1 + (alpha2 - alpha1) * localT); + return [ + Math.round(zebra1[0] + (zebra2[0] - zebra1[0]) * localT), + Math.round(zebra1[1] + (zebra2[1] - zebra1[1]) * localT), + Math.round(zebra1[2] + (zebra2[2] - zebra1[2]) * localT), + alpha + ]; + } else if (color1[0] === "zebra") { + const offset1 = color1[5] || 0; + const zebra1 = zebra(offset1); + const alpha1 = color1[4] || 255; + let color2RGB; + let alpha2; + if (color22[0] === "rainbow") { + const offset2 = color22[5] || 0; + color2RGB = rainbow(offset2); + alpha2 = color22[4] || 255; + } else { + color2RGB = [color22[0], color22[1], color22[2]]; + alpha2 = color22[3] || 255; + } + return [ + Math.round(zebra1[0] + (color2RGB[0] - zebra1[0]) * localT), + Math.round(zebra1[1] + (color2RGB[1] - zebra1[1]) * localT), + Math.round(zebra1[2] + (color2RGB[2] - zebra1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } else { + const offset2 = color22[5] || 0; + const zebra2 = zebra(offset2); + const alpha1 = color1[3] || 255; + const alpha2 = color22[4] || 255; + return [ + Math.round(color1[0] + (zebra2[0] - color1[0]) * localT), + Math.round(color1[1] + (zebra2[1] - color1[1]) * localT), + Math.round(color1[2] + (zebra2[2] - color1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } + } + if (color1[0] === "rainbow" || color22[0] === "rainbow") { + if (color1[0] === "rainbow" && color22[0] === "rainbow") { + const offset1 = color1[5] || 0; + const offset2 = color22[5] || 0; + const rainbow1 = rainbow(offset1); + const rainbow2 = rainbow(offset2); + const alpha1 = color1[4] || 255; + const alpha2 = color22[4] || 255; + const alpha = Math.round(alpha1 + (alpha2 - alpha1) * localT); + return [ + Math.round(rainbow1[0] + (rainbow2[0] - rainbow1[0]) * localT), + Math.round(rainbow1[1] + (rainbow2[1] - rainbow1[1]) * localT), + Math.round(rainbow1[2] + (rainbow2[2] - rainbow1[2]) * localT), + alpha + ]; + } else if (color1[0] === "rainbow") { + const offset1 = color1[5] || 0; + const rainbow1 = rainbow(offset1); + const alpha1 = color1[4] || 255; + let color2RGB; + let alpha2; + if (color22[0] === "zebra") { + const offset2 = color22[5] || 0; + color2RGB = zebra(offset2); + alpha2 = color22[4] || 255; + } else { + color2RGB = [color22[0], color22[1], color22[2]]; + alpha2 = color22[3] || 255; + } + return [ + Math.round(rainbow1[0] + (color2RGB[0] - rainbow1[0]) * localT), + Math.round(rainbow1[1] + (color2RGB[1] - rainbow1[1]) * localT), + Math.round(rainbow1[2] + (color2RGB[2] - rainbow1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } else { + const offset2 = color22[5] || 0; + const rainbow2 = rainbow(offset2); + const alpha1 = color1[3] || 255; + const alpha2 = color22[4] || 255; + return [ + Math.round(color1[0] + (rainbow2[0] - color1[0]) * localT), + Math.round(color1[1] + (rainbow2[1] - color1[1]) * localT), + Math.round(color1[2] + (rainbow2[2] - color1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } + } + if (color1[0] === "zebra" || color22[0] === "zebra") { + console.log("\u{1F993} Taking zebra branch"); + if (color1[0] === "zebra" && color22[0] === "zebra") { + const offset1 = color1[5] || 0; + const offset2 = color22[5] || 0; + const zebra1 = zebra(offset1); + const zebra2 = zebra(offset2); + const alpha1 = color1[4] || 255; + const alpha2 = color22[4] || 255; + const alpha = Math.round(alpha1 + (alpha2 - alpha1) * localT); + return [ + Math.round(zebra1[0] + (zebra2[0] - zebra1[0]) * localT), + Math.round(zebra1[1] + (zebra2[1] - zebra1[1]) * localT), + Math.round(zebra1[2] + (zebra2[2] - zebra1[2]) * localT), + alpha + ]; + } else if (color1[0] === "zebra") { + const offset1 = color1[5] || 0; + const zebra1 = zebra(offset1); + console.log("\u{1F993} DEBUG zebra first:", { offset1, zebra1, color1 }); + const alpha1 = color1[4] || 255; + let color2RGB; + let alpha2; + if (color22[0] === "rainbow") { + const offset2 = color22[5] || 0; + color2RGB = rainbow(offset2); + alpha2 = color22[4] || 255; + } else { + color2RGB = [color22[0], color22[1], color22[2]]; + alpha2 = color22[3] || 255; + } + return [ + Math.round(zebra1[0] + (color2RGB[0] - zebra1[0]) * localT), + Math.round(zebra1[1] + (color2RGB[1] - zebra1[1]) * localT), + Math.round(zebra1[2] + (color2RGB[2] - zebra1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } else { + const offset2 = color22[5] || 0; + const zebra2 = zebra(offset2); + const alpha1 = color1[3] || 255; + const alpha2 = color22[4] || 255; + return [ + Math.round(color1[0] + (zebra2[0] - color1[0]) * localT), + Math.round(color1[1] + (zebra2[1] - color1[1]) * localT), + Math.round(color1[2] + (zebra2[2] - color1[2]) * localT), + Math.round(alpha1 + (alpha2 - alpha1) * localT) + ]; + } + } + const r2 = Math.round(color1[0] + (color22[0] - color1[0]) * localT); + const g = Math.round(color1[1] + (color22[1] - color1[1]) * localT); + const b2 = Math.round(color1[2] + (color22[2] - color1[2]) * localT); + const a2 = Math.round(color1[3] + (color22[3] - color1[3]) * localT); + if (!fadeInfo.neat) { + const noiseAmount = 8; + const noiseR = (Math.random() - 0.5) * noiseAmount; + const noiseG = (Math.random() - 0.5) * noiseAmount; + const noiseB = (Math.random() - 0.5) * noiseAmount; + return [ + Math.max(0, Math.min(255, r2 + noiseR)), + Math.max(0, Math.min(255, g + noiseG)), + Math.max(0, Math.min(255, b2 + noiseB)), + a2 + ]; + } + return [r2, g, b2, a2]; +} +var twoDCommands; +function twoD(ref) { + twoDCommands = ref; +} +function makeBuffer(width2, height2, fillProcess, painting2, api) { + if (!width2 || !height2) return; + const bufferStart = performance.now(); + const imageData = new ImageData(width2, height2); + const buffer = { + pixels: imageData.data, + width: imageData.width, + height: imageData.height + }; + buffer.api = api; + if (typeof fillProcess === "function") { + const savedBuffer = getBuffer(); + const rc = c.slice(); + const savedPan = { x: panTranslation.x, y: panTranslation.y }; + panTranslation.x = 0; + panTranslation.y = 0; + setBuffer(buffer); + api.screen.pixels = buffer.pixels; + try { + fillProcess(api); + if (painting2 && typeof painting2.paint === "function") { + painting2.paint(true); + } + } catch (error) { + console.warn("\u26A0\uFE0F makeBuffer fillProcess error:", error); + } + panTranslation.x = savedPan.x; + panTranslation.y = savedPan.y; + setBuffer(savedBuffer); + color(...rc); + } + const bufferTime = performance.now() - bufferStart; + return buffer; +} +function cloneBuffer(buffer) { + return { + width: buffer.width, + height: buffer.height, + pixels: new Uint8ClampedArray(buffer.pixels) + }; +} +function getBuffer() { + return { width, height, pixels }; +} +function setBuffer(buffer) { + if (buffer.pixels && buffer.pixels.buffer && buffer.pixels.buffer.detached) { + console.warn("\u{1F6A8} Detected detached pixels buffer in setBuffer, recreating... (This should be rare now)"); + buffer.pixels = new Uint8ClampedArray(buffer.width * buffer.height * 4); + buffer.pixels.fill(0); + } + ({ width, height, pixels } = buffer); +} +function changePixels(changer) { + changer(pixels, width, height); +} +function pixel(x, y, painting2 = { width, height, pixels }) { + const buffer = painting2.pixels; + if (x >= painting2.width || y >= painting2.height || x < 0 || y < 0) { + return [0, 0, 0, 0]; + } + const i2 = (floor7(x) + floor7(y) * painting2.width) * 4; + return [buffer[i2], buffer[i2 + 1], buffer[i2 + 2], buffer[i2 + 3]]; +} +function colorsMatch(color1, color22) { + if (!color1) return false; + return color1[0] === color22[0] && color1[1] === color22[1] && color1[2] === color22[2] && color1[3] === color22[3]; +} +function flood(x, y, fillColor = c) { + if (x < 0 || y < 0 || x >= width || y >= height) { + return { color: [0, 0, 0, 0], area: 0 }; + } + const targetColor = pixel(x, y); + if (targetColor[3] === 0) { + return { + color: targetColor, + area: 0 + }; + } + const previousColorState = cloneColorForLog(c); + const resolvedFillColor = findColor2(fillColor); + if (inkFloodLoggingEnabled()) { + console.log( + `${inkFloodLogPrefix()}\u{1F30A} FLOOD DEBUG`, + { + input: cloneValueForLog(fillColor), + resolved: cloneColorForLog(resolvedFillColor), + previous: previousColorState, + origin: { x, y }, + target: cloneColorForLog(targetColor) + } + ); + } + if (gpuFloodEnabled && gpuFloodAvailable && gpuAllowed("flood") && gpuSpinModule && pixels && width && height) { + const floodStart = performance.now(); + const result = gpuSpinModule.gpuFlood( + pixels, + width, + height, + x, + y, + targetColor, + resolvedFillColor + ); + if (result.success) { + gpuOk("flood"); + const floodTime = performance.now() - floodStart; + graphPerf2.track("flood-gpu", floodTime); + if (inkFloodLoggingEnabled()) { + console.log( + `${inkFloodLogPrefix()}\u{1F30A} GPU FLOOD RESULT (JFA)`, + { + resolved: cloneColorForLog(resolvedFillColor), + previous: previousColorState, + area: result.area, + timeMs: floodTime.toFixed(2) + } + ); + } + return { + color: targetColor, + area: result.area + }; + } + gpuFailed("flood"); + } + const cpuStart = performance.now(); + let count = 0; + const visited = new Uint8Array(width * height); + const stack = [[x, y]]; + color(...resolvedFillColor); + const oldColor = c.slice(); + while (stack.length) { + const [cx, cy] = stack.pop(); + if (cx < 0 || cy < 0 || cx >= width || cy >= height) continue; + const index = cy * width + cx; + if (visited[index]) continue; + visited[index] = 1; + const currentColor = pixel(cx, cy); + if (colorsMatch(currentColor, targetColor)) { + count++; + plot(cx, cy); + if (cx + 1 < width) stack.push([cx + 1, cy]); + if (cx > 0) stack.push([cx - 1, cy]); + if (cy + 1 < height) stack.push([cx, cy + 1]); + if (cy > 0) stack.push([cx, cy - 1]); + } + } + color(...oldColor); + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("flood-cpu", cpuTime); + if (inkFloodLoggingEnabled()) { + console.log( + `${inkFloodLogPrefix()}\u{1F30A} CPU FLOOD RESULT`, + { + resolved: cloneColorForLog(resolvedFillColor), + previous: previousColorState, + area: count, + timeMs: cpuTime.toFixed(2) + } + ); + } + return { + color: targetColor, + area: count + }; +} +function resetRainbowCache2() { + currentRainbowColor = null; + currentZebraColor = null; + resetRainbowCache(); +} +function getFadeColor(t2, x = 0, y = 0) { + if (!fadeMode || fadeColors.length < 2) { + return c.slice(); + } + const fadeInfo = { + colors: fadeColors, + direction: fadeDirection, + neat: fadeNeat + }; + return getLocalFadeColor(t2, x, y, fadeInfo); +} +function normalizeColorInput(value) { + if (value === void 0 || value === null) return value; + if (typeof value === "object") { + if (typeof value.name === "string") { + return normalizeColorInput(value.name); + } + if (typeof value.value === "string") { + return normalizeColorInput(value.value); + } + if (typeof value.toString === "function" && value.toString !== Object.prototype.toString) { + return normalizeColorInput(value.toString()); + } + } + if (typeof value !== "string") return value; + let normalized = value.trim(); + if (normalized.startsWith("'") && !normalized.endsWith("'")) { + normalized = normalized.slice(1); + } + const quotePairs = ["'", '"', "`"]; + for (const quote of quotePairs) { + if (normalized.startsWith(quote) && normalized.endsWith(quote) && normalized.length > 1) { + normalized = normalized.slice(1, -1).trim(); + break; + } + } + if (normalized.startsWith("|") && normalized.endsWith("|") && normalized.length > 2) { + normalized = normalized.slice(1, -1); + } + const smartQuotes = ["\u201C", "\u201D", "\u2018", "\u2019"]; + if (smartQuotes.includes(normalized[0])) normalized = normalized.slice(1); + if (smartQuotes.includes(normalized[normalized.length - 1])) normalized = normalized.slice(0, -1); + if (normalized.startsWith("fade:")) { + const fadeContent = normalized.slice(5); + normalized = `fade:${fadeContent.toLowerCase()}`; + } else if (!(normalized.startsWith("#") || normalized.startsWith("0x"))) { + normalized = normalized.toLowerCase(); + } + return normalized; +} +function findColor2() { + let args = [...arguments]; + if (args.length === 1 && args[0] !== void 0) { + const isNumber = () => typeof args[0] === "number"; + const isArray = () => Array.isArray(args[0]); + const isString = () => typeof args[0] === "string"; + const isFadeObject = () => typeof args[0] === "object" && args[0] !== null && args[0].type === "fade"; + const isBool = typeof args[0] === "boolean"; + if (isFadeObject()) { + const fadeObj = args[0]; + const alphaValue = Array.isArray(fadeObj.alpha) ? fadeObj.alpha[0] : fadeObj.alpha; + const fadeColorArray = [fadeObj.fadeString, alphaValue || 255]; + return fadeColorArray; + } + if (isBool) { + resetRainbowCache2(); + return args[0] ? [255, 255, 255, 255] : [0, 0, 0, 255]; + } + if (!isNumber() && !isArray() && !isString() && !isFadeObject()) + return findColor2(any(args[0])); + if (isNumber()) { + resetRainbowCache2(); + if (args[0] > 255) { + args = hexToRgb(args[0]); + } else { + args = Array.from(args); + args.push(args[0], args[0]); + } + } else if (isArray()) { + return findColor2(...args[0]); + } else if (isString()) { + const normalizedString = normalizeColorInput(args[0]); + if (typeof normalizedString === "string") { + args[0] = normalizedString; + } + if (args[0].startsWith("fade:")) { + return [args[0], 255]; + } + resetRainbowCache2(); + const indexColor = parseColorIndex(args[0]); + if (indexColor) { + if (indexColor[0] === "rainbow") { + args = rainbow(); + } else { + args = indexColor; + } + } else if (args[0] === "erase") { + const originalAlpha = arguments[1]; + args = [-1, -1, -1]; + if (originalAlpha !== void 0) args.push(computeAlpha(originalAlpha)); + } else if (args[0] === "rainbow") { + args = rainbow(); + } else if (args[0] === "zebra") { + args = zebra(); + } else { + const cleanedHex = args[0].replace("#", "").replace("0x", "").toUpperCase(); + if (isHexString(cleanedHex) === true) { + args = hexToRgb(cleanedHex); + } else if (args[0].startsWith("fade:")) { + args = [0, 0, 0]; + args.push(255); + } else { + const cssColor = cssColors2[args[0]]; + if (cssColor) { + args = cssColor; + } else { + args = randIntArr(255, 3); + args.push(255); + if (debug) { + console.warn("\u26A0\uFE0F findColor: Unknown color string", normalizedString, "\u2192 falling back to random color"); + } + } + } + } + } + } else if (args.length === 2) { + if (args[0] === "rainbow") { + args = [...rainbow(), computeAlpha(args[1])]; + } else if (args[0] === "zebra") { + args = [...zebra(), computeAlpha(args[1])]; + } else if (typeof args[0] === "string") { + const normalizedString = normalizeColorInput(args[0]); + if (typeof normalizedString === "string") { + args[0] = normalizedString; + } + if (args[0].startsWith("fade:")) { + const fadeString = args[0]; + const alpha = computeAlpha(args[1] || 255); + return [fadeString, alpha]; + } + const indexColor = parseColorIndex(args[0]); + if (indexColor) { + if (indexColor[0] === "rainbow") { + args = [...rainbow(), computeAlpha(args[1])]; + } else { + args = [...indexColor, computeAlpha(args[1])]; + } + } else { + const cssColor = cssColors2[args[0]]; + if (cssColor) { + args = [...cssColor, computeAlpha(args[1])]; + } else { + args = [0, 0, 0, computeAlpha(args[1])]; + if (debug) { + console.warn("\u26A0\uFE0F findColor: Unknown color string with alpha", normalizedString, "\u2192 defaulting to black"); + } + } + } + } else if (Array.isArray(args[0])) { + const baseColor = [...args[0]]; + if (args.length > 1 && args[1] !== void 0) { + const alphaValue = computeAlpha(args[1]); + if (baseColor.length >= 4) { + baseColor[3] = alphaValue; + } else { + baseColor.push(alphaValue); + } + } + args = baseColor; + } else if (args[0] === void 0) { + const alphaValue = args[1]; + args = randIntArr(255, 3); + args.push(computeAlpha(alphaValue)); + } else { + args = [args[0], args[0], args[0], args[1]]; + } + } else if (args.length === 0 || args.length === 1 && args[0] === void 0) { + args = randIntArr(255, 3); + args.push(255); + } + if (args.length === 3) args = [...args, 255]; + if (args.some((val) => val === void 0 || isNaN(val) && !(typeof val === "string" && val.startsWith("fade:")) || typeof val === "string" && val.match(/^\d*\.?\d+s\.\.\.?$/))) { + const fadeString = args.find((val) => typeof val === "string" && val.startsWith("fade:")); + if (fadeString) { + const alpha = args.find((val) => typeof val === "number" && !isNaN(val)) || 255; + return [fadeString, alpha]; + } + args = args.filter((val) => !(typeof val === "string" && val.match(/^\d*\.?\d+s\.\.\.?$/))); + while (args.length < 3) { + args.push(255); + } + if (args.length === 3) args.push(255); + args = args.map((val, i2) => { + if (val === void 0 || isNaN(val) || typeof val === "string") { + return i2 === 3 ? 255 : randInt(255); + } + return val; + }); + args = args.slice(0, 4); + while (args.length < 4) { + args.push(args.length === 3 ? 255 : randInt(255)); + } + } + args.forEach((a2, i2) => { + if (isNaN(args[i2])) args[i2] = randInt(255); + }); + return args; +} +function computeAlpha(alpha) { + if (alpha > 0 && alpha < 1) alpha = round5(alpha * 255); + return alpha; +} +function setColor(r2, g, b2, a2 = 255) { + c[0] = floor7(r2); + c[1] = floor7(g); + c[2] = floor7(b2); + c[3] = floor7(a2); + return c.slice(); +} +function color(r2, g, b2, a2 = 255) { + if (arguments.length === 0) return c.slice(); + if (typeof r2 === "string" && r2.startsWith("fade:")) { + c[0] = r2; + c[1] = g || 255; + c[2] = 0; + c[3] = 255; + return c.slice(); + } + if (arguments.length === 1 && Array.isArray(r2) && typeof r2[0] === "string" && r2[0].startsWith("fade:")) { + c[0] = r2[0]; + c[1] = r2[1] || 255; + c[2] = 0; + c[3] = 255; + return c.slice(); + } + return setColor(r2, g, b2, a2); +} +function color2(r2, g, b2, a2 = 255) { + if (arguments.length === 0) return c2.slice(); + if (r2 === void 0 || r2 === null) { + c2 = null; + return; + } + if (!c2) c2 = []; + c2[0] = floor7(r2); + c2[1] = floor7(g); + c2[2] = floor7(b2); + c2[3] = floor7(a2); + return c2.slice(); +} +function evaluateFadeDirection(directionStr) { + const numericAngle = parseFloat(directionStr); + if (!isNaN(numericAngle)) { + return numericAngle; + } + const validDirections = [ + "horizontal", + "horizontal-reverse", + "vertical", + "vertical-reverse", + "diagonal", + "diagonal-reverse" + ]; + if (validDirections.includes(directionStr)) { + return directionStr; + } + const trimmed = directionStr.trim(); + if (/^-?\d+(\.\d+)?$/.test(trimmed)) { + const parsed = parseFloat(trimmed); + if (!isNaN(parsed)) { + return parsed; + } + } + if (currentKidLispContext) { + try { + if (directionStr.startsWith('"') || directionStr.startsWith("[")) { + const parsedExpression = JSON.parse(directionStr); + const result = currentKidLispContext.evaluate(parsedExpression, currentKidLispContext.api, currentKidLispContext.env); + const numResult = parseFloat(result); + if (!isNaN(numResult)) { + return numResult; + } + } else { + const result = currentKidLispContext.evaluate(directionStr, currentKidLispContext.api, currentKidLispContext.env); + const numResult = parseFloat(result); + if (!isNaN(numResult)) { + return numResult; + } + } + } catch (error) { + console.warn("Failed to evaluate fade direction expression:", directionStr, error); + } + } + console.warn("Invalid fade direction, falling back to horizontal:", directionStr); + return "horizontal"; +} +function calculateAngleFadePosition(x, y, minX, minY, maxX, maxY, angle3) { + const originalAngle = angle3; + angle3 = (angle3 % 360 + 360) % 360; + const areaWidth = maxX - minX; + const areaHeight = maxY - minY; + const centerX = minX + areaWidth / 2; + const centerY = minY + areaHeight / 2; + const relX = (x - centerX) / (areaWidth / 2); + const relY = (y - centerY) / (areaHeight / 2); + const radians2 = angle3 * Math.PI / 180; + const dirX = Math.cos(radians2); + const dirY = -Math.sin(radians2); + const dotProduct = relX * dirX + relY * dirY; + const maxDot = Math.sqrt(2); + const t2 = (dotProduct + maxDot) / (2 * maxDot); + if (x < minX + 3 && y < minY + 3) { + } + return Math.max(0, Math.min(1, t2)); +} +function clear() { + clearDepthBuffer(); + cleanupBlurBuffers(); + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x; + minY = activeMask.y; + maxX = activeMask.x + activeMask.width; + maxY = activeMask.y + activeMask.height; + } + const fadeInfo = parseFadeColor(c); + const isLocalFade = fadeInfo !== null; + if (isLocalFade) { + const colors = fadeInfo.colors; + const hasSpecialColors = colors.some((col) => col[0] === "rainbow" || col[0] === "zebra"); + if (!hasSpecialColors && colors.length >= 2) { + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const direction = fadeInfo.direction; + const numColors = colors.length; + const numSegments = numColors - 1; + const interpolateMultiColor = (t2) => { + t2 = Math.max(0, Math.min(1, t2)); + const segment = Math.min(Math.floor(t2 * numSegments), numSegments - 1); + const localT = t2 * numSegments - segment; + const c1 = colors[segment]; + const c22 = colors[segment + 1]; + return [ + c1[0] + (c22[0] - c1[0]) * localT | 0, + c1[1] + (c22[1] - c1[1]) * localT | 0, + c1[2] + (c22[2] - c1[2]) * localT | 0, + (c1[3] ?? 255) + ((c22[3] ?? 255) - (c1[3] ?? 255)) * localT | 0 + ]; + }; + if (direction === "vertical" || direction === "vertical-reverse") { + const reverse2 = direction === "vertical-reverse"; + const maxT = Math.max(1, workingHeight - 1); + for (let y = minY; y < maxY; y++) { + const t2 = reverse2 ? (maxY - 1 - y) / maxT : (y - minY) / maxT; + const [r2, g, b2, a2] = interpolateMultiColor(t2); + const rowStart = (y * width + minX) * 4; + pixels[rowStart] = r2; + pixels[rowStart + 1] = g; + pixels[rowStart + 2] = b2; + pixels[rowStart + 3] = a2; + const rowWidth = workingWidth * 4; + for (let copySize = 4; copySize < rowWidth; copySize *= 2) { + const copyEnd = Math.min(copySize * 2, rowWidth); + pixels.copyWithin(rowStart + copySize, rowStart, rowStart + copyEnd - copySize); + } + } + return; + } else if (direction === "horizontal" || direction === "horizontal-reverse") { + const reverse2 = direction === "horizontal-reverse"; + const maxT = Math.max(1, workingWidth - 1); + const rowColors = new Uint8ClampedArray(workingWidth * 4); + for (let x = 0; x < workingWidth; x++) { + const t2 = reverse2 ? (workingWidth - 1 - x) / maxT : x / maxT; + const [r2, g, b2, a2] = interpolateMultiColor(t2); + const idx = x * 4; + rowColors[idx] = r2; + rowColors[idx + 1] = g; + rowColors[idx + 2] = b2; + rowColors[idx + 3] = a2; + } + for (let y = minY; y < maxY; y++) { + const rowStart = (y * width + minX) * 4; + pixels.set(rowColors, rowStart); + } + return; + } else if (direction === "diagonal" || direction === "diagonal-reverse") { + const reverse2 = direction === "diagonal-reverse"; + const maxTx = Math.max(1, workingWidth - 1); + const maxTy = Math.max(1, workingHeight - 1); + for (let y = minY; y < maxY; y++) { + const dy = reverse2 ? (maxY - 1 - y) / maxTy : (y - minY) / maxTy; + const rowOffset = y * width; + for (let x = minX; x < maxX; x++) { + const dx = reverse2 ? (maxX - 1 - x) / maxTx : (x - minX) / maxTx; + const t2 = (dx + dy) * 0.5; + const [r2, g, b2, a2] = interpolateMultiColor(t2); + const i2 = (rowOffset + x) * 4; + pixels[i2] = r2; + pixels[i2 + 1] = g; + pixels[i2 + 2] = b2; + pixels[i2 + 3] = a2; + } + } + return; + } + } + } + if (isLocalFade) { + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + let t2 = 0; + const numericAngle = parseFloat(fadeInfo.direction); + if (!isNaN(numericAngle)) { + t2 = calculateAngleFadePosition(x, y, minX, minY, maxX - 1, maxY - 1, numericAngle); + } else { + if (fadeInfo.direction === "horizontal") { + t2 = (x - minX) / Math.max(1, maxX - 1 - minX); + } else if (fadeInfo.direction === "horizontal-reverse") { + t2 = (maxX - 1 - x) / Math.max(1, maxX - 1 - minX); + } else if (fadeInfo.direction === "vertical") { + t2 = (y - minY) / Math.max(1, maxY - 1 - minY); + } else if (fadeInfo.direction === "vertical-reverse") { + t2 = (maxY - 1 - y) / Math.max(1, maxY - 1 - minY); + } else if (fadeInfo.direction === "diagonal") { + const dx = (x - minX) / Math.max(1, maxX - 1 - minX); + const dy = (y - minY) / Math.max(1, maxY - 1 - minY); + t2 = (dx + dy) / 2; + } else if (fadeInfo.direction === "diagonal-reverse") { + const dx = (maxX - 1 - x) / Math.max(1, maxX - 1 - minX); + const dy = (maxY - 1 - y) / Math.max(1, maxY - 1 - minY); + t2 = (dx + dy) / 2; + } else { + t2 = (x - minX) / Math.max(1, maxX - 1 - minX); + } + } + const fadeColor = getLocalFadeColor(t2, x, y, fadeInfo); + const i2 = (x + y * width) * 4; + if (i2 >= 0 && i2 < pixels.length - 3) { + pixels[i2] = fadeColor[0]; + pixels[i2 + 1] = fadeColor[1]; + pixels[i2 + 2] = fadeColor[2]; + pixels[i2 + 3] = fadeColor[3]; + } + } + } + } else if (activeMask) { + const rowWidth = (maxX - minX) * 4; + for (let y = minY; y < maxY; y++) { + const rowStart = (y * width + minX) * 4; + pixels[rowStart] = c[0]; + pixels[rowStart + 1] = c[1]; + pixels[rowStart + 2] = c[2]; + pixels[rowStart + 3] = c[3]; + if (rowWidth > 4) { + for (let copySize = 4; copySize < rowWidth; copySize *= 2) { + const copyEnd = Math.min(copySize * 2, rowWidth); + pixels.copyWithin(rowStart + copySize, rowStart, rowStart + copyEnd - copySize); + } + } + } + } else { + if (c[0] === 0 && c[1] === 0 && c[2] === 0 && c[3] === 0) { + pixels.fill(0); + return; + } + pixels[0] = c[0]; + pixels[1] = c[1]; + pixels[2] = c[2]; + pixels[3] = c[3]; + for (let copySize = 4; copySize < pixels.length; copySize *= 2) { + const copyEnd = Math.min(copySize * 2, pixels.length); + pixels.copyWithin(copySize, 0, copyEnd - copySize); + } + } +} +function plot(x, y) { + x = floor7(x); + y = floor7(y); + if (!pixels) return; + if (x < 0 || x >= width || y < 0 || y >= height) return; + if (activeMask) { + if (y < activeMask.y || y >= activeMask.y + activeMask.height || x >= activeMask.x + activeMask.width || x < activeMask.x) + return; + } + for (const s2 of skips) if (x === s2.x && y === s2.y) return; + const i2 = (x + y * width) * 4; + if (i2 < 0 || i2 + 3 >= pixels.length) return; + const alpha = c[3]; + let plotColor = c; + if (typeof c[0] === "string" && c[0].startsWith("fade:")) { + const fadeInfo = parseLocalFade(c[0]); + if (fadeInfo) { + const resolvedColor = getLocalFadeColor(null, x, y, fadeInfo); + plotColor = [...resolvedColor, c[1] || 255]; + } else { + plotColor = [0, 0, 0, 0]; + } + } + if (forceReplaceMode) { + pixels[i2] = plotColor[0] ?? 0; + pixels[i2 + 1] = plotColor[1] ?? 0; + pixels[i2 + 2] = plotColor[2] ?? 0; + pixels[i2 + 3] = plotColor[3] ?? alpha; + return; + } + if (c[0] === -1 && c[1] === -1 && c[2] === -1) { + const na = 1 - c[3] / 255; + erase(pixels, i2, na); + if (eraseTarget) { + const pi = i2 >> 2; + eraseTarget[pi] = 255 - ((255 - eraseTarget[pi]) * na + 0.5) | 0; + } + } else if (alpha === 255) { + pixels.set(plotColor, i2); + } else if (alpha !== 0) { + blend2(pixels, plotColor, 0, i2); + } +} +function skip(...args) { + if (args[0] === null) skips.length = 0; + else + args.forEach((p) => { + skips.push({ + x: floor7(p.x || p[0]) + panTranslation.x, + y: floor7(p.y || p[1]) + panTranslation.y + }); + }); +} +function point(...args) { + let x, y; + if (args.length === 1) { + if (args[0].length >= 2) { + x = args[0][0]; + y = args[0][1]; + } else { + x = args[0].x; + y = args[0].y; + } + } else if (args.length >= 2) { + x = args[0]; + y = args[1]; + } else { + x = randInt(width); + y = randInt(height); + } + x += panTranslation.x; + y += panTranslation.y; + plot(x, y); + return [x, y]; +} +function shadePixels(points, shader, shaderArgs = []) { + points.forEach((p) => { + if (p.x < 0) return; + if (p.x >= width) return; + if (p.y < 0) return; + if (p.y >= height) return; + shader.position?.(p, ...shaderArgs); + if (p.x < 0) return; + if (p.x >= width) return; + if (p.y < 0) return; + if (p.y >= height) return; + p.x = floor7(p.x); + p.y = floor7(p.y); + const n2 = p.x + p.y * width; + if (writeBuffer[n2] !== 1) { + writeBuffer[n2] = 1; + const i2 = floor7(p.x + p.y * width) * 4; + const pixel2 = pixels.subarray(i2, i2 + 4); + shader.color({ x: p.x, y: p.y }, pixel2, c, p.color); + } + }); +} +function pan(x, y) { + if (typeof x === "object") { + x = x.x; + y = x.y; + } + if (y === void 0) y = x; + panTranslation.x += floor7(x); + panTranslation.y += floor7(y); +} +function unpan() { + panTranslation.x = 0; + panTranslation.y = 0; +} +var savedPanTranslation; +function savepan() { + savedPanTranslation = { ...panTranslation }; +} +function loadpan() { + if (savedPanTranslation) { + panTranslation.x = savedPanTranslation.x; + panTranslation.y = savedPanTranslation.y; + } +} +function mask(box2) { + activeMask = box2; +} +function unmask() { + activeMask = null; +} +function copy7(destX, destY, srcX, srcY, src, alpha = 1) { + destX = floor7(destX); + destY = floor7(destY); + srcX = floor7(srcX); + srcY = floor7(srcY); + if (destX < 0 || destX >= width || destY < 0 || destY >= height || srcX < 0 || srcX >= src.width || srcY < 0 || srcY >= src.height) { + return; + } + const di = (destX + destY * width) * 4; + const si = (srcX + srcY * src.width) * 4; + blend2(pixels, src.pixels, si, di, alpha); +} +function resize(bitmap, width2, height2) { + const ratioX = bitmap.width / width2; + const ratioY = bitmap.height / height2; + const pixels2 = new Uint8ClampedArray(width2 * height2 * 4); + for (let y = 0; y < height2; y += 1) { + for (let x = 0; x < width2; x += 1) { + const index = (y * width2 + x) * 4; + const srcX = floor7(x * ratioX); + const srcY = floor7(y * ratioY); + const srcIndex = (srcY * bitmap.width + srcX) * 4; + pixels2[index] = bitmap.pixels[srcIndex]; + pixels2[index + 1] = bitmap.pixels[srcIndex + 1]; + pixels2[index + 2] = bitmap.pixels[srcIndex + 2]; + pixels2[index + 3] = bitmap.pixels[srcIndex + 3]; + } + } + return { pixels: pixels2, width: width2, height: height2 }; +} +var cachedContrastLUT = null; +var cachedContrastLevel = null; +function contrast(level = 1) { + if (level === 1) return; + const contrastStart = performance.now(); + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + minX = Math.max(0, Math.floor(maskX)); + minY = Math.max(0, Math.floor(maskY)); + maxX = Math.min(width, Math.floor(maskX + activeMask.width)); + maxY = Math.min(height, Math.floor(maskY + activeMask.height)); + } + if (gpuContrastEnabled && gpuSpinAvailable && gpuAllowed("contrast") && gpuSpinModule && pixels && width && height) { + const mask2 = activeMask ? { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + } : null; + const result = gpuSpinModule.gpuContrast?.(pixels, width, height, level, mask2); + if (result) { + gpuOk("contrast"); + const contrastTime = performance.now() - contrastStart; + graphPerf2.track("contrast-gpu", contrastTime); + return; + } + gpuFailed("contrast"); + } + const cpuStart = performance.now(); + if (cachedContrastLevel !== level) { + cachedContrastLUT = new Uint8Array(256); + for (let i2 = 0; i2 < 256; i2++) { + const normalized = i2 / 255; + const adjusted = ((normalized - 0.5) * level + 0.5) * 255; + cachedContrastLUT[i2] = Math.max(0, Math.min(255, Math.round(adjusted))); + } + cachedContrastLevel = level; + } + const contrastLUT = cachedContrastLUT; + if (!activeMask) { + const len5 = pixels.length; + for (let i2 = 0; i2 < len5; i2 += 4) { + if (pixels[i2 + 3] === 0) continue; + pixels[i2] = contrastLUT[pixels[i2]]; + pixels[i2 + 1] = contrastLUT[pixels[i2 + 1]]; + pixels[i2 + 2] = contrastLUT[pixels[i2 + 2]]; + } + } else { + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const idx = (y * width + x) * 4; + if (pixels[idx + 3] === 0) continue; + pixels[idx] = contrastLUT[pixels[idx]]; + pixels[idx + 1] = contrastLUT[pixels[idx + 1]]; + pixels[idx + 2] = contrastLUT[pixels[idx + 2]]; + } + } + } + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("contrast-cpu", cpuTime); +} +function brightness(adjustment = 0) { + if (adjustment === 0) return; + const brightnessStart = performance.now(); + adjustment = Math.max(-255, Math.min(255, adjustment)); + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + minX = Math.max(0, Math.floor(maskX)); + minY = Math.max(0, Math.floor(maskY)); + maxX = Math.min(width, Math.floor(maskX + activeMask.width)); + maxY = Math.min(height, Math.floor(maskY + activeMask.height)); + } + if (gpuContrastEnabled && gpuSpinAvailable && gpuAllowed("brightness") && gpuSpinModule && pixels && width && height) { + const mask2 = activeMask ? { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + } : null; + const result = gpuSpinModule.gpuBrightness?.(pixels, width, height, adjustment, mask2); + if (result) { + gpuOk("brightness"); + const brightnessTime = performance.now() - brightnessStart; + graphPerf2.track("brightness-gpu", brightnessTime); + return; + } + gpuFailed("brightness"); + } + const cpuStart = performance.now(); + const brightnessLUT = new Uint8Array(256); + for (let i2 = 0; i2 < 256; i2++) { + brightnessLUT[i2] = Math.max(0, Math.min(255, i2 + adjustment)); + } + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const idx = (y * width + x) * 4; + if (pixels[idx + 3] === 0) continue; + pixels[idx] = brightnessLUT[pixels[idx]]; + pixels[idx + 1] = brightnessLUT[pixels[idx + 1]]; + pixels[idx + 2] = brightnessLUT[pixels[idx + 2]]; + } + } + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("brightness-cpu", cpuTime); +} +function invert4() { + const invertStart = performance.now(); + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + minX = Math.max(0, Math.floor(maskX)); + minY = Math.max(0, Math.floor(maskY)); + maxX = Math.min(width, Math.floor(maskX + activeMask.width)); + maxY = Math.min(height, Math.floor(maskY + activeMask.height)); + } + if (gpuContrastEnabled && gpuSpinAvailable && gpuAllowed("invert") && gpuSpinModule && pixels && width && height) { + const mask2 = activeMask ? { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + } : null; + const result = gpuSpinModule.gpuInvert?.(pixels, width, height, mask2); + if (result) { + gpuOk("invert"); + const invertTime = performance.now() - invertStart; + graphPerf2.track("invert-gpu", invertTime); + return; + } + gpuFailed("invert"); + } + const cpuStart = performance.now(); + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const idx = (y * width + x) * 4; + if (pixels[idx + 3] === 0) continue; + pixels[idx] = 255 - pixels[idx]; + pixels[idx + 1] = 255 - pixels[idx + 1]; + pixels[idx + 2] = 255 - pixels[idx + 2]; + } + } + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("invert-cpu", cpuTime); +} +function blitCropScale(src, srcWidth, srcHeight, cX, cY, scaleX, scaleY, destX, destY, dst, dstWidth, startX, startY, endX, endY) { + const srcXs = new Int32Array(endX - startX); + for (let dx = startX; dx < endX; dx += 1) { + const sx = cX + Math.floor(dx * scaleX); + srcXs[dx - startX] = sx >= 0 && sx < srcWidth ? sx : -1; + } + const src32 = new Uint32Array(src.buffer, src.byteOffset, src.length >> 2); + const dst32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length >> 2); + for (let dy = startY; dy < endY; dy += 1) { + const srcY = cY + Math.floor(dy * scaleY); + if (srcY < 0 || srcY >= srcHeight) continue; + const srcRow = srcY * srcWidth; + let destIdx32 = (destY + dy) * dstWidth + destX + startX; + for (let dx = startX; dx < endX; dx += 1, destIdx32 += 1) { + const sx = srcXs[dx - startX]; + if (sx < 0) continue; + const s2 = src32[srcRow + sx]; + const sA = s2 >>> 24; + if (sA === 255) { + dst32[destIdx32] = s2; + } else if (sA > 0) { + const srcIdx = (srcRow + sx) * 4; + const destIdx = destIdx32 * 4; + const alpha = sA + 1; + const invAlpha = 256 - alpha; + dst[destIdx] = alpha * src[srcIdx] + invAlpha * dst[destIdx] >> 8; + dst[destIdx + 1] = alpha * src[srcIdx + 1] + invAlpha * dst[destIdx + 1] >> 8; + dst[destIdx + 2] = alpha * src[srcIdx + 2] + invAlpha * dst[destIdx + 2] >> 8; + dst[destIdx + 3] = Math.min(255, dst[destIdx + 3] + sA); + } + } + } +} +function paste(from, destX = 0, destY = 0, scale7 = 1, blit = false) { + const pasteStart = performance.now(); + if (!from) { + graphPerf2.track("paste", 0); + return; + } + if (!pixels) { + console.warn("\u{1F6A8} paste: No destination pixels buffer available, skipping paste operation"); + graphPerf2.track("paste", 0); + return; + } + destX += panTranslation.x; + destY += panTranslation.y; + destX = Math.floor(destX); + destY = Math.floor(destY); + if (typeof scale7 === "number" && Math.abs(scale7) < 0.01) { + graphPerf2.track("paste", 0); + return; + } + if (scale7 !== 1) { + let angle3 = 0; + let anchor; + let tWidth, tHeight; + let crop; + if (typeof scale7 === "object") { + angle3 = scale7.angle; + tWidth = scale7.width; + tHeight = scale7.height; + anchor = scale7.anchor; + crop = scale7.crop; + if (scale7.scale === void 0 && (tWidth === void 0 || tHeight === void 0)) { + scale7 = 1; + } else { + scale7 = scale7.scale; + } + } + if (!angle3 && !crop && !tWidth && !tHeight && !anchor && typeof scale7 === "number" && scale7 > 0 && scale7 === ~~scale7 && scale7 <= 8) { + const srcWidth = from.width; + const srcHeight = from.height; + const srcPixels = from.pixels; + const scaleInt = ~~scale7; + const destWidth = srcWidth * scaleInt; + const destHeight = srcHeight * scaleInt; + if (destX >= 0 && destY >= 0 && destX + destWidth <= width && destY + destHeight <= height) { + const fastBlock = (srcPixels.byteOffset & 3) === 0 && (pixels.byteOffset & 3) === 0; + const src32 = fastBlock ? new Uint32Array(srcPixels.buffer, srcPixels.byteOffset, srcWidth * srcHeight) : null; + const dst32 = fastBlock ? new Uint32Array(pixels.buffer, pixels.byteOffset, width * height) : null; + for (let srcY = 0; srcY < srcHeight; srcY += 1) { + for (let srcX = 0; srcX < srcWidth; srcX += 1) { + const srcIndex = srcX + srcY * srcWidth << 2; + if (srcIndex >= srcPixels.length) continue; + const a2 = srcPixels[srcIndex + 3]; + if (a2 === 0) continue; + const baseDestX = destX + srcX * scaleInt; + const baseDestY = destY + srcY * scaleInt; + if (a2 === 255 && fastBlock) { + const s2 = src32[srcX + srcY * srcWidth]; + for (let dy = 0; dy < scaleInt; dy += 1) { + let di = (baseDestY + dy) * width + baseDestX; + for (let dx = 0; dx < scaleInt; dx += 1) dst32[di + dx] = s2; + } + } else { + color(srcPixels[srcIndex], srcPixels[srcIndex + 1], srcPixels[srcIndex + 2], a2); + if (scaleInt > 1) { + box(baseDestX, baseDestY, scaleInt, scaleInt, "fill"); + } else { + plot(baseDestX, baseDestY); + } + } + } + } + return; + } + } + let isImageBitmap = typeof ImageBitmap !== "undefined" && from instanceof ImageBitmap; + if (isImageBitmap && crop) { + const pixels2 = ensureBufferPixels(from); + if (pixels2) { + from = { width: from.width, height: from.height, pixels: pixels2 }; + isImageBitmap = false; + } else { + console.warn("\u{1F3A8} Failed to extract pixels from ImageBitmap for cropping"); + return; + } + } + if (!isImageBitmap && crop && from && from.width && from.height) { + const sourcePixels = from.pixels || from.painting && from.painting.pixels; + if (sourcePixels) { + const cX = Math.floor(crop.x); + const cY = Math.floor(crop.y); + const cW = Math.floor(crop.w); + const cH = Math.floor(crop.h); + const srcWidth = from.width; + const srcHeight = from.height; + const targetW = tWidth || (scale7 && typeof scale7 === "number" ? Math.floor(cW * scale7) : cW); + const targetH = tHeight || (scale7 && typeof scale7 === "number" ? Math.floor(cH * scale7) : cH); + if ((targetW !== cW || targetH !== cH) && !angle3 && (sourcePixels.byteOffset & 3) === 0 && (pixels.byteOffset & 3) === 0) { + const scaleX = cW / targetW; + const scaleY = cH / targetH; + let startY = Math.max(0, -destY); + let endY = Math.min(targetH, height - destY); + let startX = Math.max(0, -destX); + let endX = Math.min(targetW, width - destX); + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + startX = Math.max(startX, maskX - destX); + endX = Math.min(endX, maskX + activeMask.width - destX); + startY = Math.max(startY, maskY - destY); + endY = Math.min(endY, maskY + activeMask.height - destY); + } + if (endX > startX && endY > startY) { + blitCropScale( + sourcePixels, + srcWidth, + srcHeight, + cX, + cY, + scaleX, + scaleY, + destX, + destY, + pixels, + width, + startX, + startY, + endX, + endY + ); + } + return; + } + const croppedPixels = new Uint8ClampedArray(cW * cH * 4); + for (let y = 0; y < cH; y++) { + const srcY = cY + y; + if (srcY >= 0 && srcY < from.height) { + const srcRowStart = (srcY * srcWidth + cX) * 4; + const rowLength = cW * 4; + let startOffset = 0; + let copyLength = rowLength; + let srcStart = srcRowStart; + if (cX < 0) { + startOffset = -cX * 4; + copyLength -= startOffset; + srcStart = srcY * srcWidth * 4; + } + if (cX + cW > srcWidth) { + copyLength -= (cX + cW - srcWidth) * 4; + } + if (copyLength > 0 && srcStart < sourcePixels.length) { + const row = sourcePixels.subarray(srcStart, srcStart + copyLength); + if (y * rowLength + startOffset + row.length <= croppedPixels.length) { + croppedPixels.set(row, y * rowLength + startOffset); + } + } + } + } + const croppedBuffer = { + width: cW, + height: cH, + pixels: croppedPixels + }; + if ((targetW !== cW || targetH !== cH) && !angle3) { + const scaleX = cW / targetW; + const scaleY = cH / targetH; + const maskX = activeMask ? activeMask.x + panTranslation.x : 0; + const maskY = activeMask ? activeMask.y + panTranslation.y : 0; + const maskW = activeMask ? activeMask.width : width; + const maskH = activeMask ? activeMask.height : height; + const startY = Math.max(0, -destY); + const endY = Math.min(targetH, height - destY); + const startX = Math.max(0, -destX); + const endX = Math.min(targetW, width - destX); + for (let dy = startY; dy < endY; dy++) { + const srcY = Math.floor(dy * scaleY); + const destRowY = destY + dy; + const destRowStart = destRowY * width * 4; + for (let dx = startX; dx < endX; dx++) { + const destColX = destX + dx; + if (activeMask) { + if (destColX < maskX || destColX >= maskX + maskW || destRowY < maskY || destRowY >= maskY + maskH) { + continue; + } + } + const srcX = Math.floor(dx * scaleX); + const srcIdx = (srcY * cW + srcX) * 4; + const destIdx = destRowStart + destColX * 4; + const sA = croppedPixels[srcIdx + 3]; + if (sA > 0) { + const sR = croppedPixels[srcIdx]; + const sG = croppedPixels[srcIdx + 1]; + const sB = croppedPixels[srcIdx + 2]; + if (sA === 255) { + pixels[destIdx] = sR; + pixels[destIdx + 1] = sG; + pixels[destIdx + 2] = sB; + pixels[destIdx + 3] = 255; + } else { + const dA = pixels[destIdx + 3]; + const alpha = sA + 1; + const invAlpha = 256 - alpha; + pixels[destIdx] = alpha * sR + invAlpha * pixels[destIdx] >> 8; + pixels[destIdx + 1] = alpha * sG + invAlpha * pixels[destIdx + 1] >> 8; + pixels[destIdx + 2] = alpha * sB + invAlpha * pixels[destIdx + 2] >> 8; + pixels[destIdx + 3] = Math.min(255, dA + sA); + } + } + } + } + return; + } + if (tWidth || tHeight || typeof scale7 === "number" && scale7 !== 1 || scale7 && typeof scale7 === "object" || angle3) { + grid( + { + box: { x: destX, y: destY, w: cW, h: cH }, + transform: { scale: scale7, angle: angle3, width: tWidth, height: tHeight, anchor } + }, + croppedBuffer + ); + } else { + paste(croppedBuffer, destX, destY, 1, blit); + } + return; + } + } + if (from && from.width && from.height) { + grid( + { + box: { x: destX, y: destY, w: from.width, h: from.height }, + transform: { scale: scale7, angle: angle3, width: tWidth, height: tHeight, anchor } + }, + from + ); + } + return; + } + if (from.crop) { + const cropW = from.crop.w; + const cropH = from.crop.h; + const fromPixels = from.painting?.pixels || from.pixels; + if (fromPixels && destX >= 0 && destY >= 0 && destX + cropW <= width && destY + cropH <= height) { + const fromWidth = from.painting?.width || from.width; + const maskX = activeMask ? activeMask.x + panTranslation.x : 0; + const maskY = activeMask ? activeMask.y + panTranslation.y : 0; + const maskW = activeMask ? activeMask.width : width; + const maskH = activeMask ? activeMask.height : height; + for (let y = 0; y < cropH; y += 1) { + const srcY = from.crop.y + y; + const destRowY = destY + y; + for (let x = 0; x < cropW; x += 1) { + const dx = destX + x; + const dy = destRowY; + if (activeMask) { + if (dx < maskX || dx >= maskX + maskW || dy < maskY || dy >= maskY + maskH) { + continue; + } + } + const srcX = from.crop.x + x; + const srcIdx = (srcY * fromWidth + srcX) * 4; + const destIdx = (destRowY * width + dx) * 4; + if (srcIdx + 3 < fromPixels.length) { + const srcAlpha = fromPixels[srcIdx + 3]; + if (srcAlpha > 0) { + if (srcAlpha === 255) { + pixels[destIdx] = fromPixels[srcIdx]; + pixels[destIdx + 1] = fromPixels[srcIdx + 1]; + pixels[destIdx + 2] = fromPixels[srcIdx + 2]; + pixels[destIdx + 3] = fromPixels[srcIdx + 3]; + } else { + const alpha = srcAlpha + 1; + const invAlpha = 256 - alpha; + pixels[destIdx] = alpha * fromPixels[srcIdx] + invAlpha * pixels[destIdx] >> 8; + pixels[destIdx + 1] = alpha * fromPixels[srcIdx + 1] + invAlpha * pixels[destIdx + 1] >> 8; + pixels[destIdx + 2] = alpha * fromPixels[srcIdx + 2] + invAlpha * pixels[destIdx + 2] >> 8; + pixels[destIdx + 3] = Math.min(255, pixels[destIdx + 3] + srcAlpha); + } + } + } + } + } + } else { + const sourcePainting = from.painting || from; + if (sourcePainting && sourcePainting.pixels) { + for (let y = 0; y < cropH; y += 1) { + for (let x = 0; x < cropW; x += 1) { + copy7( + destX + x, + destY + y, + from.crop.x + x, + from.crop.y + y, + sourcePainting + ); + } + } + } + } + } else { + if (blit) { + pixels.set(from.pixels, 0); + } else { + const srcWidth = from.width; + const srcHeight = from.height; + const srcPixels = from.pixels; + if (destX >= 0 && destY >= 0 && destX + srcWidth <= width && destY + srcHeight <= height && srcPixels) { + const maskX = activeMask ? activeMask.x + panTranslation.x : 0; + const maskY = activeMask ? activeMask.y + panTranslation.y : 0; + const maskW = activeMask ? activeMask.width : width; + const maskH = activeMask ? activeMask.height : height; + if (!activeMask && (srcPixels.byteOffset & 3) === 0 && (pixels.byteOffset & 3) === 0) { + const src32 = new Uint32Array( + srcPixels.buffer, + srcPixels.byteOffset, + srcWidth * srcHeight + ); + const dst32 = new Uint32Array( + pixels.buffer, + pixels.byteOffset, + width * height + ); + for (let y = 0; y < srcHeight; y += 1) { + let si = y * srcWidth; + let di = (destY + y) * width + destX; + for (let x = 0; x < srcWidth; x += 1, si += 1, di += 1) { + const s2 = src32[si]; + const a2 = s2 >>> 24; + if (a2 === 255) { + dst32[di] = s2; + } else if (a2 !== 0) { + const si4 = si << 2; + const di4 = di << 2; + const alpha = a2 + 1; + const invAlpha = 256 - alpha; + pixels[di4] = alpha * srcPixels[si4] + invAlpha * pixels[di4] >> 8; + pixels[di4 + 1] = alpha * srcPixels[si4 + 1] + invAlpha * pixels[di4 + 1] >> 8; + pixels[di4 + 2] = alpha * srcPixels[si4 + 2] + invAlpha * pixels[di4 + 2] >> 8; + pixels[di4 + 3] = Math.min(255, pixels[di4 + 3] + a2); + } + } + } + } else { + for (let y = 0; y < srcHeight; y += 1) { + const srcRowStart = y * srcWidth * 4; + const destRowStart = ((destY + y) * width + destX) * 4; + for (let x = 0; x < srcWidth; x += 1) { + const dx = destX + x; + const dy = destY + y; + if (activeMask) { + if (dx < maskX || dx >= maskX + maskW || dy < maskY || dy >= maskY + maskH) { + continue; + } + } + const srcIdx = srcRowStart + x * 4; + const destIdx = destRowStart + x * 4; + if (srcIdx + 3 >= srcPixels.length || destIdx + 3 >= pixels.length) continue; + if (srcPixels[srcIdx + 3] > 0) { + const srcAlpha = srcPixels[srcIdx + 3]; + if (srcAlpha === 255) { + pixels[destIdx] = srcPixels[srcIdx]; + pixels[destIdx + 1] = srcPixels[srcIdx + 1]; + pixels[destIdx + 2] = srcPixels[srcIdx + 2]; + pixels[destIdx + 3] = srcPixels[srcIdx + 3]; + } else { + const alpha = srcAlpha + 1; + const invAlpha = 256 - alpha; + pixels[destIdx] = alpha * srcPixels[srcIdx] + invAlpha * pixels[destIdx] >> 8; + pixels[destIdx + 1] = alpha * srcPixels[srcIdx + 1] + invAlpha * pixels[destIdx + 1] >> 8; + pixels[destIdx + 2] = alpha * srcPixels[srcIdx + 2] + invAlpha * pixels[destIdx + 2] >> 8; + pixels[destIdx + 3] = Math.min(255, pixels[destIdx + 3] + srcAlpha); + } + } + } + } + } + } else { + for (let y = 0; y < srcHeight; y += 1) { + for (let x = 0; x < srcWidth; x += 1) { + const destPixelX = destX + x; + const destPixelY = destY + y; + if (activeMask) { + const maskX = destPixelX - activeMask.x - panTranslation.x; + const maskY = destPixelY - activeMask.y - panTranslation.y; + if (maskX < 0 || maskY < 0 || maskX >= activeMask.width || maskY >= activeMask.height) { + continue; + } + } + copy7(destPixelX, destPixelY, x, y, from); + } + } + } + } + } + const pasteEnd = performance.now(); + graphPerf2.track("paste", pasteEnd - pasteStart); +} +var stampSkipCounter = 0; +function stamp(from, x, y, scale7, angle3) { + const stampStart = performance.now(); + if (x == null) x = randIntRange(0, width - 1); + if (y == null) y = randIntRange(0, height - 1); + if (scale7 !== void 0 && Math.abs(scale7) < 0.01) { + graphPerf2.track("stamp", 0); + return; + } + if (graphPerf2 && graphPerf2.lastFPS && graphPerf2.lastFPS < 5) { + stampSkipCounter++; + if (stampSkipCounter % 3 !== 0) { + graphPerf2.track("stamp", 0); + return; + } + } else { + stampSkipCounter = 0; + } + if (scale7 !== void 0 || angle3 !== void 0) { + const effectiveScale = scale7 !== void 0 ? scale7 : 1; + const scaledWidth = from.width * effectiveScale; + const scaledHeight = from.height * effectiveScale; + const adjustedX = x - scaledWidth / 2; + const adjustedY = y - scaledHeight / 2; + const transform = {}; + if (scale7 !== void 0) transform.scale = scale7; + if (angle3 !== void 0) transform.angle = angle3; + paste(from, adjustedX, adjustedY, transform); + } else { + paste(from, x - from.width / 2, y - from.height / 2); + } + const stampEnd = performance.now(); + graphPerf2.track("stamp", stampEnd - stampStart); +} +var blendingMode = "blend"; +function blendMode(mode = "blend") { + blendingMode = mode; +} +var eraseTarget = null; +var eraseTargetWidth = 0; +function setEraseTarget(target, targetWidth) { + eraseTarget = target; + eraseTargetWidth = targetWidth; +} +function blend2(dst, src, si, di, alphaIn = 1) { + if (forceReplaceMode) { + for (let i2 = 0; i2 < 3; i2++) { + dst[di + i2] = src[si + i2]; + } + dst[di + 3] = src[si + 3] * alphaIn; + return; + } + if (blendingMode === "erase") { + const normalAlpha = 1 - src[si + 3] / 255; + dst[di + 3] *= normalAlpha; + if (dst[di + 3] === 0) { + dst[di] = 32; + dst[di + 1] = 32; + dst[di + 2] = 32; + } + if (eraseTarget) { + const pi = di >> 2; + eraseTarget[pi] = 255 - ((255 - eraseTarget[pi]) * normalAlpha + 0.5) | 0; + } + return; + } + if (src[si + 3] === 0) return; + if (blendingMode === "blit") { + for (let i2 = 0; i2 < 4; i2++) { + if (i2 != 3) { + dst[di + i2] = src[si + i2]; + } else { + dst[di + i2] = src[si + i2] * alphaIn; + } + } + return; + } + if (dst[di + 3] < 255 && src[si + 3] > 0) { + const epsilon = 1e-10; + const alphaSrc = src[si + 3] * alphaIn / 255; + const alphaDst = dst[di + 3] / 255; + const combinedAlpha = alphaSrc + (1 - alphaSrc) * alphaDst; + if (combinedAlpha > epsilon) { + for (let offset = 0; offset < 3; offset++) { + dst[di + offset] = (src[si + offset] * alphaSrc + dst[di + offset] * (1 - alphaSrc) * alphaDst) / (combinedAlpha + epsilon); + } + dst[di + 3] = combinedAlpha * 255; + } + } else { + const alpha = src[si + 3] * alphaIn + 1; + const invAlpha = 256 - alpha; + dst[di] = alpha * src[si + 0] + invAlpha * dst[di + 0] >> 8; + dst[di + 1] = alpha * src[si + 1] + invAlpha * dst[di + 1] >> 8; + dst[di + 2] = alpha * src[si + 2] + invAlpha * dst[di + 2] >> 8; + dst[di + 3] = 255; + } +} +function erase(pixels2, i2, normalizedAlpha) { + pixels2[i2 + 3] *= normalizedAlpha; +} +function lineFast(x0, y0, x1, y1) { + if (!pixels) return; + if (typeof c[0] === "string" && !c[0].startsWith("fade:")) { + setColor(0, 0, 0, c[3] || 255); + } + x0 = floor7(x0) || 0; + y0 = floor7(y0) || 0; + x1 = floor7(x1) || 0; + y1 = floor7(y1) || 0; + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x; + minY = activeMask.y; + maxX = activeMask.x + activeMask.width; + maxY = activeMask.y + activeMask.height; + } + const r2 = c[0], g = c[1], b2 = c[2], a2 = c[3]; + const isErase = r2 === -1 && g === -1 && b2 === -1; + const normalAlpha = isErase ? 1 - a2 / 255 : 0; + const isOpaque = a2 === 255 && !isErase; + const hasAlpha = a2 !== 0 && a2 !== 255 && !isErase; + const dx = abs3(x1 - x0); + const dy = -abs3(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx + dy; + while (true) { + if (x0 >= minX && x0 < maxX && y0 >= minY && y0 < maxY) { + const i2 = (x0 + y0 * width) * 4; + if (forceReplaceMode || isOpaque) { + pixels[i2] = r2; + pixels[i2 + 1] = g; + pixels[i2 + 2] = b2; + pixels[i2 + 3] = isOpaque ? 255 : a2; + } else if (isErase) { + pixels[i2 + 3] *= normalAlpha; + if (eraseTarget) { + const pi = i2 >> 2; + eraseTarget[pi] = 255 - ((255 - eraseTarget[pi]) * normalAlpha + 0.5) | 0; + } + } else if (hasAlpha) { + const invA = 255 - a2; + pixels[i2] = r2 * a2 + pixels[i2] * invA >> 8; + pixels[i2 + 1] = g * a2 + pixels[i2 + 1] * invA >> 8; + pixels[i2 + 2] = b2 * a2 + pixels[i2 + 2] * invA >> 8; + pixels[i2 + 3] = a2 + (pixels[i2 + 3] * invA >> 8); + } + } + if (x0 === x1 && y0 === y1) break; + const e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x0 += sx; + } + if (e2 <= dx) { + err += dx; + y0 += sy; + } + } +} +function lineh(x0, x1, y) { + x0 = floor7(x0); + x1 = floor7(x1); + y = floor7(y); + if (!pixels) return; + if (typeof c[0] === "string" && !c[0].startsWith("fade:")) { + setColor(0, 0, 0, c[3] || 255); + } + if (y < 0 || y >= height || x0 >= width || x1 < 0) return; + if (activeMask) { + if (y < activeMask.y || y >= activeMask.y + activeMask.height || x1 < activeMask.x || x0 >= activeMask.x + activeMask.width) + return; + } + x0 = clamp(x0, 0, width - 1); + x1 = clamp(x1, 0, width - 1); + if (activeMask) { + x0 = clamp(x0, activeMask.x, activeMask.x + activeMask.width - 1); + x1 = clamp(x1, activeMask.x, activeMask.x + activeMask.width - 1); + } + const firstIndex = (x0 + y * width) * 4; + const secondIndex = (x1 + y * width) * 4; + let startIndex, endIndex; + if (firstIndex > secondIndex) { + startIndex = secondIndex; + endIndex = firstIndex; + } else { + startIndex = firstIndex; + endIndex = secondIndex; + } + if (forceReplaceMode) { + for (let i2 = startIndex; i2 <= endIndex; i2 += 4) { + pixels[i2] = c[0]; + pixels[i2 + 1] = c[1]; + pixels[i2 + 2] = c[2]; + pixels[i2 + 3] = c[3]; + } + return; + } + if (blendMode !== "erase" && c[0] === -1 && c[1] === -1 && c[2] === -1) { + const normalAlpha = 1 - c[3] / 255; + for (let i2 = startIndex; i2 <= endIndex; i2 += 4) { + erase(pixels, i2, normalAlpha); + if (eraseTarget) { + const pi = i2 >> 2; + eraseTarget[pi] = 255 - ((255 - eraseTarget[pi]) * normalAlpha + 0.5) | 0; + } + } + } else if (c[3] === 255) { + for (let i2 = startIndex; i2 <= endIndex; i2 += 4) { + pixels[i2] = c[0]; + pixels[i2 + 1] = c[1]; + pixels[i2 + 2] = c[2]; + pixels[i2 + 3] = 255; + } + } else if (c[3] !== 0) { + for (let i2 = startIndex; i2 <= endIndex; i2 += 4) { + blend2(pixels, c, 0, i2); + } + } +} +function lineThick(x0, y0, x1, y1, r2) { + x0 += panTranslation.x; + y0 += panTranslation.y; + x1 += panTranslation.x; + y1 += panTranslation.y; + const r22 = r2 * r2 + r2; + const ldx = x1 - x0, ldy = y1 - y0; + const len22 = ldx * ldx + ldy * ldy; + if (len22 === 0) { + for (let dy = -r2; dy <= r2; dy++) { + const h = floor7(sqrt3(max5(0, r22 - dy * dy))); + lineh(x0 - h, x0 + h, y0 + dy); + } + return; + } + const invLen2 = 1 / len22; + const minY = floor7(min5(y0, y1) - r2); + const maxY = floor7(max5(y0, y1) + r2); + for (let y = minY; y <= maxY; y++) { + let lo = Infinity, hi = -Infinity; + const da = y - y0, db = y - y1; + const da2 = da * da, db2 = db * db; + if (da2 <= r22) { + const h = sqrt3(r22 - da2); + lo = x0 - h; + hi = x0 + h; + } + if (db2 <= r22) { + const h = sqrt3(r22 - db2); + if (x1 - h < lo) lo = x1 - h; + if (x1 + h > hi) hi = x1 + h; + } + const C = (y - y0) * ldx + x0 * ldy; + const D2 = sqrt3(r22 * len22); + if (abs3(ldy) > 1e-4) { + let bxLo = (C - D2) / ldy, bxHi = (C + D2) / ldy; + if (bxLo > bxHi) { + const tmp = bxLo; + bxLo = bxHi; + bxHi = tmp; + } + const tLo = ((bxLo - x0) * ldx + (y - y0) * ldy) * invLen2; + const tHi = ((bxHi - x0) * ldx + (y - y0) * ldy) * invLen2; + const tMin = max5(0, min5(tLo, tHi)); + const tMax = min5(1, max5(tLo, tHi)); + if (tMin <= tMax) { + const xA = x0 + tMin * ldx, yA = y0 + tMin * ldy; + const xB = x0 + tMax * ldx, yB = y0 + tMax * ldy; + const ha2 = r22 - (y - yA) * (y - yA); + const hb2 = r22 - (y - yB) * (y - yB); + if (ha2 >= 0) { + const h = sqrt3(ha2); + if (xA - h < lo) lo = xA - h; + if (xA + h > hi) hi = xA + h; + } + if (hb2 >= 0) { + const h = sqrt3(hb2); + if (xB - h < lo) lo = xB - h; + if (xB + h > hi) hi = xB + h; + } + } + } + if (lo <= hi) { + lineh(floor7(lo), floor7(hi), y); + } + } +} +function line() { + let x0, y0, x1, y1, thickness; + if (arguments.length === 1) { + const arg0 = arguments[0]; + if (arg0 && typeof arg0 === "object") { + x0 = arg0.x0; + y0 = arg0.y0; + x1 = arg0.x1; + y1 = arg0.y1; + thickness = arg0.thickness; + } + } else if (arguments.length >= 4) { + x0 = arguments[0]; + y0 = arguments[1]; + x1 = arguments[2]; + y1 = arguments[3]; + thickness = arguments[4]; + } else if (arguments.length === 2) { + const arg0 = arguments[0]; + const arg1 = arguments[1]; + if (Array.isArray(arg0) && Array.isArray(arg1)) { + x0 = arg0[0]; + y0 = arg0[1]; + x1 = arg1[0]; + y1 = arg1[1]; + } else if (arg0 && typeof arg0 === "object" && arg1 && typeof arg1 === "object") { + x0 = arg0.x; + y0 = arg0.y; + x1 = arg1.x; + y1 = arg1.y; + } + } else { + } + if (x0 == null) x0 = randIntRange(0, width); + if (y0 == null) y0 = randIntRange(0, height); + if (x1 == null) x1 = randIntRange(0, width); + if (y1 == null) y1 = randIntRange(0, height); + if (isNaN(x0)) x0 = randIntRange(0, width); + if (isNaN(y0)) y0 = randIntRange(0, height); + if (isNaN(x1)) x1 = randIntRange(0, width); + if (isNaN(y1)) y1 = randIntRange(0, height); + if (thickness > 1) { + const radius = floor7((thickness - 1) / 2); + lineThick(floor7(x0), floor7(y0), floor7(x1), floor7(y1), radius); + const out2 = [floor7(x0), floor7(y0), floor7(x1), floor7(y1)]; + twoDCommands?.push(["line", ...out2]); + return out2; + } + x0 += panTranslation.x; + y0 += panTranslation.y; + x1 += panTranslation.x; + y1 += panTranslation.y; + const cachedInk = c.slice(0); + const localFadeInfo = parseFadeColor(c); + const usingLocalFade = !!localFadeInfo; + if (!fadeMode && !usingLocalFade && !c2 && skips.length === 0) { + if (y0 === y1) { + lineh(x0, x1, y0); + } else { + lineFast(x0, y0, x1, y1); + } + } else if (y0 === y1 && !c2 && !fadeMode) { + lineh(x0, x1, y0); + } else { + const lineLength = sqrt3((x1 - x0) ** 2 + (y1 - y0) ** 2); + bresenham(x0, y0, x1, y1).forEach((p, index, points) => { + if (fadeMode) { + const t2 = lineLength > 0 ? sqrt3((p.x - x0) ** 2 + (p.y - y0) ** 2) / lineLength : 0; + const fadeColor = getFadeColor(t2, p.x, p.y); + setColor(...fadeColor); + plot(p.x, p.y); + } else if (usingLocalFade) { + const totalSteps = Math.max(1, points.length - 1); + const t2 = totalSteps === 0 ? 0 : index / totalSteps; + const fadeColor = getLocalFadeColor(t2, p.x, p.y, localFadeInfo); + setColor(...fadeColor); + plot(p.x, p.y); + } else if (c2) { + const step = sqrt3(p.x * p.x + p.y * p.y) / 255; + color(...shiftRGB(c, c2, step)); + plot(p.x, p.y); + } else { + plot(p.x, p.y); + } + }); + if (usingLocalFade) { + if (typeof cachedInk[0] === "string" && cachedInk[0].startsWith("fade:")) { + color(cachedInk); + } else { + setColor(...cachedInk); + } + } else if (c2 || fadeMode) { + setColor(...cachedInk); + } + } + const out = [x0, y0, x1, y1]; + twoDCommands?.push(["line", ...out]); + return out; +} +function pixelPerfectPolyline(points, shader) { + if (points.length < 2) return; + const pixels2 = []; + let last = points[0]; + points.forEach((cur) => { + const xMin = min5(last.x, cur.x); + const xMax = max5(last.x, cur.x); + const yMin = min5(last.y, cur.y); + const yMax = max5(last.y, cur.y); + if (xMin >= width || xMax < 0 || yMin >= height || yMax < 0) { + last = cur; + return; + } + const rb = last.color === "rainbow"; + bresenham(last.x, last.y, cur.x, cur.y).forEach((p, i2) => { + if (i2 > 0 || pixels2.length < 2) { + pixels2.push({ ...p, color: rb ? rainbow() : last.color }); + } + }); + last = cur; + }); + const filtered = []; + let c4 = 0; + while (c4 < pixels2.length) { + if (c4 > 0 && c4 + 1 < pixels2.length && (pixels2[c4 - 1].x === pixels2[c4].x || pixels2[c4 - 1].y === pixels2[c4].y) && // check left and up + (pixels2[c4 + 1].x === pixels2[c4].x || pixels2[c4 + 1].y === pixels2[c4].y) && // check right and down + pixels2[c4 - 1].x !== pixels2[c4 + 1].x && // check left and right of prev and next + pixels2[c4 - 1].y !== pixels2[c4 + 1].y) { + c4 += 1; + } + filtered.push(pixels2[c4]); + c4 += 1; + } + if (shader) { + shadePixels(filtered, shader); + } else { + filtered.forEach((p) => point(p)); + } +} +function lineAngle(x1, y1, dist5, degrees2) { + const x2 = x1 + dist5 * cos3(radians(degrees2)); + const y2 = y1 + dist5 * sin3(radians(degrees2)); + return line(x1, y1, x2, y2); +} +function circle(x0, y0, radius, filled = false, thickness, precision) { + if (filled || thickness > 1) { + oval(x0, y0, radius, radius, filled, thickness, precision); + return; + } + x0 = floor7(x0); + y0 = floor7(y0); + radius = floor7(radius); + let f2 = 1 - radius, ddF_x = 0, ddF_y = -2 * radius, x = 0, y = radius; + point(x0, y0 + radius); + point(x0, y0 - radius); + point(x0 + radius, y0); + point(x0 - radius, y0); + while (x < y) { + if (f2 >= 0) { + y -= 1; + ddF_y += 2; + f2 += ddF_y; + } + x += 1; + ddF_x += 2; + f2 += ddF_x + 1; + point(x0 + x, y0 + y); + point(x0 - x, y0 + y); + point(x0 + x, y0 - y); + point(x0 - x, y0 - y); + point(x0 + y, y0 + x); + point(x0 - y, y0 + x); + point(x0 + y, y0 - x); + point(x0 - y, y0 - x); + } +} +function pie(x0, y0, radius, startAngle, endAngle, precision = 3) { + const points = [[x0, y0]]; + const angleSpan = endAngle - startAngle; + const steps = max5(floor7(abs3(angleSpan) * radius / precision), 8); + for (let i2 = 0; i2 <= steps; i2++) { + const angle3 = startAngle + angleSpan * i2 / steps; + const x = x0 + radius * cos3(angle3); + const y = y0 + radius * sin3(angle3); + points.push([x, y]); + } + points.push([x0, y0]); + shape({ points, filled: true }); +} +function oval(x0, y0, radiusX, radiusY, filled = false, thickness = 1, precision) { + const points = generateEllipsePoints(x0, y0, radiusX, radiusY, precision); + shape({ points, filled, thickness }); +} +function generateEllipsePoints(x0, y0, radiusX, radiusY, precision = 20) { + const points = []; + for (let i2 = 0; i2 < 360; i2 += precision) { + const angle3 = radians(i2); + const x = x0 + radiusX * cos3(angle3); + const y = y0 + radiusY * sin3(angle3); + points.push([x, y]); + } + return points; +} +function poly(coords) { + let last = coords[0]; + for (let i2 = 1; i2 < coords.length; i2++) { + const cur = coords[i2]; + line(last, cur); + last = cur; + } +} +function pline(coords, thickness, shader) { + if (coords.length < 2) return; + let points = [], lines = [], tris = []; + let last = coords[coords.length - 1]; + let lpar, ldir; + for (let i2 = coords.length - 2; i2 >= 0; i2 -= 1) { + const cur = coords[i2]; + const lp = [last.x || last[0], last.y || last[1]], cp = [cur.x || cur[0], cur.y || cur[1]]; + const dir = normalize4([], subtract5([], cp, lp)); + if (!ldir) ldir = dir; + const rot = rotate3([], dir, [0, 0], PI2 / 2); + const offset1 = scale6([], rot, thickness / 2); + const offset2 = scale6([], rot, -thickness / 2); + let c1, c22; + if (!lpar) { + c1 = add6([], lp, offset1); + c22 = add6([], lp, offset2); + lpar = [c1, c22]; + } else { + [c1, c22] = lpar; + } + [c1, c22, lp, cp].forEach((v2) => floor3(v2, v2)); + const dot5 = dot4(dir, ldir); + let trig; + if (dot5 > 0) { + trig = [ + [c1, c22, lp], + [c22, lp, cp] + ]; + lines.push(...bresenham(...c1, ...lp)); + lines.push(...bresenham(...c22, ...cp)); + } else { + trig = [ + [c22, lp, c1], + [c1, cp, lp] + ]; + lines.push(...bresenham(...c22, ...lp)); + lines.push(...bresenham(...c1, ...cp)); + } + const clippedTris = trig.filter( + (triangle) => triangle.some((v2) => { + const tv = v2.slice(); + tv[0] += panTranslation.x; + tv[1] += panTranslation.y; + return tv[0] >= 0 && tv[0] < width && tv[1] >= 0 && tv[1] < height; + }) + ); + clippedTris.forEach((tri2) => fillTri(tri2, tris)); + ldir = dir; + lines.push(...bresenham(...lp, ...cp)); + if (i2 === coords.length - 2) + points.push({ x: c1[0], y: c1[1] }, { x: c22[0], y: c22[1] }); + points.push({ x: lp[0], y: lp[1] }, { x: cp[0], y: cp[1] }); + if (cur.color === "rainbow") color(...rainbow()); + else if (cur.color) color(...cur.color); + if (shader) { + const progress = 1 - i2 / (coords.length - 2); + shadePixels(tris, shader, [progress]); + } else { + tris.forEach((p) => point(p)); + } + tris.length = 0; + last = cur; + lpar = [c1, c22]; + } + return lpar; +} +function fillTri(v3, pix) { + const scan = []; + const [min10, mid, max9] = v3.sort((va, vb) => va[1] - vb[1]); + const v1 = [max9[0] - min10[0], max9[1] - min10[1]], v2 = [mid[0] - min10[0], mid[1] - min10[1]], cp = v1[0] * v2[1] - v2[0] * v1[1]; + const handedness = cp > 0 ? 1 : cp < 0 ? 0 : -1; + [ + [min10, max9, 0 + handedness], + // Min and Max for each edge with handedness. + [min10, mid, 1 - handedness], + [mid, max9, 1 - handedness] + ].forEach(function scanEdge(v4) { + const yStart = v4[0][Y], yEnd = v4[1][Y], xStart = v4[0][X], xEnd = v4[1][X]; + const yDist = yEnd - yStart; + const xDist = xEnd - xStart; + if (yDist <= 0) return; + const xStep = xDist / yDist; + let x = xStart; + for (let y = yStart; y < yEnd; y += 1) { + scan[y * 2 + v4[2]] = floor7(x); + x += xStep; + } + }); + for (let y = min10[Y]; y < max9[Y]; y += 1) { + for (let x = scan[y * 2]; x < scan[y * 2 + 1]; x += 1) { + pix.push({ x, y }); + } + } +} +function bresenham(x0, y0, x1, y1) { + const points = []; + x0 = floor7(x0) || 0; + y0 = floor7(y0) || 0; + x1 = floor7(x1) || 0; + y1 = floor7(y1) || 0; + const dx = abs3(x1 - x0); + const dy = -abs3(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx + dy; + while (true) { + points.push({ x: x0, y: y0 }); + if (x0 === x1 && y0 === y1) break; + const e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x0 += sx; + } + if (e2 <= dx) { + err += dx; + y0 += sy; + } + } + return points; +} +var BOX_CENTER = "center"; +function box() { + const boxStart = performance.now(); + let x, y, w, h, mode = "fill"; + for (let i2 = 0; i2 < arguments.length; i2++) { + if (Number.isNaN(arguments[i2])) { + arguments[i2] = void 0; + } + } + if (arguments.length === 1 || arguments.length === 2) { + if (Array.isArray(arguments[0])) { + x = arguments[0][0]; + y = arguments[0][1]; + w = arguments[0][2]; + h = arguments[0][3]; + } else if (arguments[0]) { + if (!isNaN(arguments[0].width)) arguments[0].w = arguments[0].width; + if (!isNaN(arguments[0].height)) arguments[0].h = arguments[0].height; + x = arguments[0].x || 0; + y = arguments[0].y || 0; + w = arguments[0].w || 0; + if (isNaN(arguments[0].h)) { + h = w; + } else { + h = arguments[0].h; + } + if (x === void 0 || y === void 0 || w === void 0) { + return console.error( + "Could not make a box {x,y,w,h} from:", + arguments[0] + ); + } + } + if (arguments[1]) mode = arguments[1]; + } else if (arguments.length === 3) { + x = arguments[0]; + y = arguments[1]; + w = arguments[2]; + h = arguments[2]; + } else if (arguments.length === 4) { + if (typeof arguments[3] === "number") { + x = arguments[0]; + y = arguments[1]; + w = arguments[2]; + h = arguments[3]; + } else { + x = arguments[0]; + y = arguments[1]; + w = arguments[2]; + h = arguments[2]; + mode = arguments[3]; + } + } else if (arguments.length === 5) { + x = arguments[0]; + y = arguments[1]; + w = arguments[2]; + h = arguments[3]; + mode = arguments[4]; + } else { + } + if (w === 0 || h === 0 || isNaN(w) || isNaN(h)) return; + if (w === Infinity) w = width; + if (h === Infinity) h = height; + if (nonvalue(x)) x = randInt(width); + if (nonvalue(y)) y = randInt(height); + if (nonvalue(w)) w = randInt(width); + if (nonvalue(h)) h = randInt(height); + if (mode === void 0 || mode === "") mode = "fill"; + if (mode.endsWith(BOX_CENTER)) { + x -= w / 2; + y -= h / 2; + mode = mode.slice(0, -BOX_CENTER.length - 1); + } + x = floor7(x); + y = floor7(y); + w = signedCeil(w); + h = signedCeil(h); + ({ x, y, w, h } = Box.from([x, y, w, h]).abs); + const thickness = parseInt(mode.split(":")[1]) || 1; + mode = mode.split(":")[0]; + if (mode === "outline" || mode === "out") { + if (thickness === 1) { + line(x - 1, y - 1, x + w, y - 1); + line(x - 1, y + h, x + w, y + h); + line(x - 1, y, x - 1, y + h - 1); + line(x + w, y, x + w, y + h - 1); + } else { + const leftX = x - thickness; + const topY = y - thickness; + const rightX = x + w + thickness; + const bottomY = y + h + thickness; + const boxHeight = h + thickness * 2; + const boxWidth = w + thickness * 2; + box(leftX, topY, boxWidth, thickness); + box(leftX, bottomY - thickness, boxWidth, thickness); + box(leftX, topY + thickness, thickness, boxHeight - thickness * 2); + box( + rightX - thickness, + topY + thickness, + thickness, + boxHeight - thickness * 2 + ); + } + } else if (mode === "inline" || mode === "in") { + if (thickness === 1) { + line(x, y, x + w - 1, y); + line(x, y + h - 1, x + w - 1, y + h - 1); + line(x, y + 1, x, y + h - 2); + line(x + w - 1, y + 1, x + w - 1, y + h - 2); + } else { + if (thickness * 2 <= w && thickness * 2 <= h) { + box(x, y, w, thickness); + box(x, y + h - thickness, w, thickness); + box(x, y + thickness, thickness, h - thickness * 2); + box(x + w - thickness, y + thickness, thickness, h - thickness * 2); + } else { + box(x, y, w, h); + } + } + } else if (mode === "fill" || mode === "") { + w -= 1; + if (typeof c[0] === "string" && !c[0].startsWith("fade:")) { + setColor(0, 0, 0, c[3] || 255); + } + const cachedInk = c.slice(0); + const fadeInfo = parseFadeColor(c); + const isLocalFade = fadeInfo !== null; + if (mathSign(height) === 1) { + for (let row = 0; row < h; row += 1) { + if (isLocalFade) { + const numericAngle = parseFloat(fadeInfo.direction); + if (!isNaN(numericAngle)) { + for (let col = 0; col <= w; col++) { + const pixelX = x + col; + const pixelY = y + row; + const t2 = calculateAngleFadePosition(pixelX, pixelY, x, y, x + w, y + h - 1, numericAngle); + const fadeColor = getLocalFadeColor(t2, pixelX, pixelY, fadeInfo); + setColor(...fadeColor); + plot(pixelX, pixelY); + } + } else { + let t2; + if (fadeInfo.direction === "vertical") { + t2 = h <= 1 ? 0 : row / (h - 1); + } else if (fadeInfo.direction === "vertical-reverse") { + t2 = h <= 1 ? 0 : 1 - row / (h - 1); + } else if (fadeInfo.direction === "horizontal") { + t2 = 0; + } else if (fadeInfo.direction === "horizontal-reverse") { + t2 = 0; + } else { + t2 = h <= 1 ? 0 : row / (h - 1); + } + if (fadeInfo.direction.startsWith("horizontal")) { + const lineY = y + row; + for (let px = x; px < x + w; px++) { + const horizontalT = w <= 1 ? 0 : fadeInfo.direction === "horizontal-reverse" ? 1 - (px - x) / (w - 1) : (px - x) / (w - 1); + const fadeColor = getLocalFadeColor(horizontalT, px, lineY, fadeInfo); + setColor(...fadeColor); + plot(px, lineY); + } + } else { + const fadeColor = getLocalFadeColor(t2, x, y + row, fadeInfo); + setColor(...fadeColor); + line(x, y + row, x + w, y + row); + } + } + } else { + line(x, y + row, x + w, y + row); + } + } + } else { + for (let row = 0; row > h; row -= 1) { + if (isLocalFade) { + const numericAngle = parseFloat(fadeInfo.direction); + if (!isNaN(numericAngle)) { + for (let col = 0; col <= w; col++) { + const pixelX = x + col; + const pixelY = y + row; + const t2 = calculateAngleFadePosition(pixelX, pixelY, x, y, x + w, y + h - 1, numericAngle); + const fadeColor = getLocalFadeColor(t2, pixelX, pixelY, fadeInfo); + setColor(...fadeColor); + plot(pixelX, pixelY); + } + } else { + let t2; + if (fadeInfo.direction === "vertical") { + t2 = Math.abs(row) / Math.abs(h - 1); + } else if (fadeInfo.direction === "vertical-reverse") { + t2 = 1 - Math.abs(row) / Math.abs(h - 1); + } else if (fadeInfo.direction === "horizontal") { + t2 = 0; + } else if (fadeInfo.direction === "horizontal-reverse") { + t2 = 0; + } else { + t2 = Math.abs(row) / Math.abs(h - 1); + } + if (fadeInfo.direction.startsWith("horizontal")) { + const savedFadeMode = fadeMode; + const savedFadeColors = fadeColors.slice(); + const savedFadeDirection = fadeDirection; + const savedFadeNeat = fadeNeat; + fadeMode = true; + fadeColors = fadeInfo.colors; + fadeDirection = fadeInfo.direction; + fadeNeat = fadeInfo.neat; + line(x, y + row, x + w, y + row); + fadeMode = savedFadeMode; + fadeColors = savedFadeColors; + fadeDirection = savedFadeDirection; + fadeNeat = savedFadeNeat; + } else { + const fadeColor = getLocalFadeColor(t2, x, y + row, fadeInfo); + setColor(...fadeColor); + line(x, y + row, x + w, y + row); + } + } + } else { + line(x, y + row, x + w, y + row); + } + } + } + } + graphPerf2.track("box", performance.now() - boxStart); +} +function shape() { + const shapeStart = performance.now(); + let argPoints; + let points; + let filled = true; + let thickness = 1; + if (arguments.length === 1 && !Array.isArray(arguments[0])) { + argPoints = arguments[0].points; + filled = arguments[0].filled; + thickness = arguments[0].thickness || thickness; + } else { + argPoints = arguments[0]; + } + if (!Array.isArray(argPoints[0])) { + points = []; + for (let p = 0; p < argPoints.length; p += 2) { + points.push([argPoints[p], argPoints[p + 1]]); + } + } else { + points = argPoints; + } + if (filled) { + if (points.length === 3) { + const triPixels = []; + fillTri(points, triPixels); + triPixels.forEach((p) => point(p)); + } else { + fillShape(points); + } + } else { + if (thickness === 1) { + points.forEach((p, i2) => { + const lastPoint = i2 < points.length - 1 ? points[i2 + 1] : points[0]; + line(...p, ...lastPoint); + }); + } else { + points.push(points[0]); + pline( + points.map((p) => p2.of(...p)), + thickness + ); + } + } + graphPerf2.track("shape", performance.now() - shapeStart); +} +function fillShape(points) { + let minY = Infinity; + let maxY = -Infinity; + for (let i2 = 0; i2 < points.length; i2++) { + minY = min5(minY, points[i2][1]); + maxY = max5(maxY, points[i2][1]); + } + for (let y = minY; y <= maxY; y++) { + let intersections = []; + for (let i2 = 0; i2 < points.length; i2++) { + let p1 = points[i2]; + let p22 = points[(i2 + 1) % points.length]; + if (p1[1] <= y && y < p22[1] || p22[1] <= y && y < p1[1]) { + let x = (y - p1[1]) * (p22[0] - p1[0]) / (p22[1] - p1[1]) + p1[0]; + intersections.push(x); + } + } + intersections.sort((a2, b2) => a2 - b2); + for (let i2 = 0; i2 < intersections.length; i2 += 2) { + let x1 = floor7(intersections[i2]); + let x2 = ceil5(intersections[i2 + 1]); + for (let x = x1; x < x2; x++) { + point(x, y); + } + } + } +} +function drawGradientTriangle(x1, y1, color1, z1, x2, y2, color22, z2, x3, y3, color3, z3) { + const maxCoord = max5(width, height) * 2; + if (abs3(x1) > maxCoord || abs3(y1) > maxCoord || abs3(x2) > maxCoord || abs3(y2) > maxCoord || abs3(x3) > maxCoord || abs3(y3) > maxCoord) { + return; + } + const minX = floor7(min5(x1, x2, x3)); + const maxX = ceil5(max5(x1, x2, x3)); + const minY = floor7(min5(y1, y2, y3)); + const maxY = ceil5(max5(y1, y2, y3)); + const screenMinX = max5(0, minX); + const screenMaxX = min5(width, maxX); + const screenMinY = max5(0, minY); + const screenMaxY = min5(height, maxY); + if (screenMinX >= screenMaxX || screenMinY >= screenMaxY) return; + const areaABC = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1); + if (abs3(areaABC) < 0.5) return; + const invArea = 1 / areaABC; + const dE1dx = y2 - y3, dE1dy = x3 - x2; + const dE2dx = y3 - y1, dE2dy = x1 - x3; + const dE3dx = y1 - y2, dE3dy = x2 - x1; + const px0 = screenMinX + 0.5; + const py0 = screenMinY + 0.5; + let e1Row = (x2 - px0) * (y3 - py0) - (x3 - px0) * (y2 - py0); + let e2Row = (x3 - px0) * (y1 - py0) - (x1 - px0) * (y3 - py0); + let e3Row = (x1 - px0) * (y2 - py0) - (x2 - px0) * (y1 - py0); + const c1r = color1[0], c1g = color1[1], c1b = color1[2], c1a = color1[3]; + const c2r = color22[0], c2g = color22[1], c2b = color22[2], c2a = color22[3]; + const c3r = color3[0], c3g = color3[1], c3b = color3[2], c3a = color3[3]; + const allOpaque = c1a >= 254 && c2a >= 254 && c3a >= 254; + const hasDepth = depthBuffer.length > 0; + const pix = pixels; + const bufW = width; + let pixCount = 0; + for (let y = screenMinY; y < screenMaxY; y++) { + let e1 = e1Row, e2 = e2Row, e3 = e3Row; + let rowPix = (screenMinX + y * bufW) * 4; + let rowDep = screenMinX + y * bufW; + let wasInside = false; + for (let x = screenMinX; x < screenMaxX; x++) { + const u2 = e1 * invArea; + const v2 = e2 * invArea; + const w = 1 - u2 - v2; + if (u2 >= 0 && v2 >= 0 && w >= 0) { + wasInside = true; + const depth = u2 * z1 + v2 * z2 + w * z3; + if (!hasDepth || depth <= depthBuffer[rowDep]) { + const r2 = u2 * c1r + v2 * c2r + w * c3r | 0; + const g = u2 * c1g + v2 * c2g + w * c3g | 0; + const b2 = u2 * c1b + v2 * c2b + w * c3b | 0; + if (allOpaque) { + pix[rowPix] = r2; + pix[rowPix + 1] = g; + pix[rowPix + 2] = b2; + pix[rowPix + 3] = 255; + } else { + const a2 = u2 * c1a + v2 * c2a + w * c3a | 0; + if (a2 >= 254) { + pix[rowPix] = r2; + pix[rowPix + 1] = g; + pix[rowPix + 2] = b2; + pix[rowPix + 3] = 255; + } else if (a2 > 0) { + const aa = a2 + 1; + const inv = 256 - a2; + pix[rowPix] = r2 * aa + pix[rowPix] * inv >> 8; + pix[rowPix + 1] = g * aa + pix[rowPix + 1] * inv >> 8; + pix[rowPix + 2] = b2 * aa + pix[rowPix + 2] * inv >> 8; + const dstA = pix[rowPix + 3]; + pix[rowPix + 3] = dstA > a2 ? dstA : a2; + } + } + if (hasDepth) depthBuffer[rowDep] = depth; + pixCount++; + } + } else if (wasInside) { + break; + } + e1 += dE1dx; + e2 += dE2dx; + e3 += dE3dx; + rowPix += 4; + rowDep += 1; + } + e1Row += dE1dy; + e2Row += dE2dy; + e3Row += dE3dy; + } + renderStats.pixelsDrawn += pixCount; +} +function subdivideTriangleIfNeeded(x1, y1, uv1, z1, w1, x2, y2, uv2, z2, w2, x3, y3, uv3, z3, w3, maxScreenSize = 300, depth = 0) { + if (depth > 4) { + return [[x1, y1, uv1, z1, w1, x2, y2, uv2, z2, w2, x3, y3, uv3, z3, w3]]; + } + const minX = min5(x1, x2, x3); + const maxX = max5(x1, x2, x3); + const minY = min5(y1, y2, y3); + const maxY = max5(y1, y2, y3); + const screenWidth = maxX - minX; + const screenHeight = maxY - minY; + const clampedMinX = max5(0, minX); + const clampedMaxX = min5(width, maxX); + const clampedMinY = max5(0, minY); + const clampedMaxY = min5(height, maxY); + const onScreenWidth = clampedMaxX - clampedMinX; + const onScreenHeight = clampedMaxY - clampedMinY; + if (onScreenWidth <= maxScreenSize && onScreenHeight <= maxScreenSize) { + return [[x1, y1, uv1, z1, w1, x2, y2, uv2, z2, w2, x3, y3, uv3, z3, w3]]; + } + if (onScreenWidth <= 0 || onScreenHeight <= 0) { + return [[x1, y1, uv1, z1, w1, x2, y2, uv2, z2, w2, x3, y3, uv3, z3, w3]]; + } + const mx12 = (x1 + x2) / 2, my12 = (y1 + y2) / 2; + const mx23 = (x2 + x3) / 2, my23 = (y2 + y3) / 2; + const mx31 = (x3 + x1) / 2, my31 = (y3 + y1) / 2; + const mz12 = (z1 + z2) / 2, mz23 = (z2 + z3) / 2, mz31 = (z3 + z1) / 2; + const invW1 = 1 / w1, invW2 = 1 / w2, invW3 = 1 / w3; + const u1w = uv1[0] * invW1, v1w = uv1[1] * invW1; + const u2w = uv2[0] * invW2, v2w = uv2[1] * invW2; + const u3w = uv3[0] * invW3, v3w = uv3[1] * invW3; + const mInvW12 = (invW1 + invW2) / 2, mInvW23 = (invW2 + invW3) / 2, mInvW31 = (invW3 + invW1) / 2; + const mw12 = 1 / mInvW12, mw23 = 1 / mInvW23, mw31 = 1 / mInvW31; + const mu12w = (u1w + u2w) / 2, mv12w = (v1w + v2w) / 2; + const mu23w = (u2w + u3w) / 2, mv23w = (v2w + v3w) / 2; + const mu31w = (u3w + u1w) / 2, mv31w = (v3w + v1w) / 2; + const muv12 = [mu12w / mInvW12, mv12w / mInvW12]; + const muv23 = [mu23w / mInvW23, mv23w / mInvW23]; + const muv31 = [mu31w / mInvW31, mv31w / mInvW31]; + const result = []; + result.push(...subdivideTriangleIfNeeded(x1, y1, uv1, z1, w1, mx12, my12, muv12, mz12, mw12, mx31, my31, muv31, mz31, mw31, maxScreenSize, depth + 1)); + result.push(...subdivideTriangleIfNeeded(mx12, my12, muv12, mz12, mw12, x2, y2, uv2, z2, w2, mx23, my23, muv23, mz23, mw23, maxScreenSize, depth + 1)); + result.push(...subdivideTriangleIfNeeded(mx31, my31, muv31, mz31, mw31, mx23, my23, muv23, mz23, mw23, x3, y3, uv3, z3, w3, maxScreenSize, depth + 1)); + result.push(...subdivideTriangleIfNeeded(mx12, my12, muv12, mz12, mw12, mx23, my23, muv23, mz23, mw23, mx31, my31, muv31, mz31, mw31, maxScreenSize, depth + 1)); + return result; +} +function drawTexturedTriangle(x1, y1, uv1, z1, w1, x2, y2, uv2, z2, w2, x3, y3, uv3, z3, w3, texture, alphaMultiplier = 1) { + if (w1 < MIN_PERSPECTIVE_W || w2 < MIN_PERSPECTIVE_W || w3 < MIN_PERSPECTIVE_W) { + return; + } + const maxCoord = max5(width, height) * 2; + if (abs3(x1) > maxCoord || abs3(y1) > maxCoord || abs3(x2) > maxCoord || abs3(y2) > maxCoord || abs3(x3) > maxCoord || abs3(y3) > maxCoord) { + return; + } + const minX = floor7(min5(x1, x2, x3)); + const maxX = ceil5(max5(x1, x2, x3)); + const minY = floor7(min5(y1, y2, y3)); + const maxY = ceil5(max5(y1, y2, y3)); + const screenMinX = max5(0, minX); + const screenMaxX = min5(width, maxX); + const screenMinY = max5(0, minY); + const screenMaxY = min5(height, maxY); + if (screenMinX >= screenMaxX || screenMinY >= screenMaxY) return; + if (showBoundingBoxes) { + boundingBoxes.push({ + minX, + maxX, + minY, + maxY, + screenMinX, + screenMaxX, + screenMinY, + screenMaxY, + rejected: false + }); + } + const areaABC = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1); + if (abs3(areaABC) < 0.5) return; + const texWidth = texture.width; + const texHeight = texture.height; + const invW1 = 1 / w1; + const invW2 = 1 / w2; + const invW3 = 1 / w3; + const u1OverW = uv1[0] * invW1; + const v1OverW = uv1[1] * invW1; + const u2OverW = uv2[0] * invW2; + const v2OverW = uv2[1] * invW2; + const u3OverW = uv3[0] * invW3; + const v3OverW = uv3[1] * invW3; + for (let y = screenMinY; y < screenMaxY; y++) { + for (let x = screenMinX; x < screenMaxX; x++) { + const areaPBC = (x2 - x) * (y3 - y) - (x3 - x) * (y2 - y); + const areaPCA = (x3 - x) * (y1 - y) - (x1 - x) * (y3 - y); + const u2 = areaPBC / areaABC; + const v2 = areaPCA / areaABC; + const w = 1 - u2 - v2; + if (u2 >= 0 && v2 >= 0 && w >= 0) { + const depth = u2 * z1 + v2 * z2 + w * z3; + const bufferIndex = x + y * width; + if (depthBuffer.length > 0) { + if (depth > depthBuffer[bufferIndex]) { + continue; + } + } + renderStats.pixelsDrawn++; + const interpInvW = u2 * invW1 + v2 * invW2 + w * invW3; + if (interpInvW < 1e-4) continue; + const interpUOverW = u2 * u1OverW + v2 * u2OverW + w * u3OverW; + const interpVOverW = u2 * v1OverW + v2 * v2OverW + w * v3OverW; + const texU = interpUOverW / interpInvW; + const texV = interpVOverW / interpInvW; + if (abs3(texU) > 1e3 || abs3(texV) > 1e3) continue; + let texX = (floor7(texU * texWidth) % texWidth + texWidth) % texWidth; + let texY = (floor7(texV * texHeight) % texHeight + texHeight) % texHeight; + const pixelIndex = (texY * texWidth + texX) * 4; + const r2 = texture.pixels[pixelIndex]; + const g = texture.pixels[pixelIndex + 1]; + const b2 = texture.pixels[pixelIndex + 2]; + const a2 = floor7(texture.pixels[pixelIndex + 3] * alphaMultiplier); + if (depthBuffer.length > 0) { + depthBuffer[bufferIndex] = depth; + } + color(r2, g, b2, a2); + point(x, y); + } + } + } +} +function tri() { + let x1, y1, x2, y2, x3, y3; + let mode = "fill"; + for (let i2 = 0; i2 < arguments.length; i2++) { + if (Number.isNaN(arguments[i2])) { + arguments[i2] = void 0; + } + } + if (arguments.length === 6) { + x1 = arguments[0]; + y1 = arguments[1]; + x2 = arguments[2]; + y2 = arguments[3]; + x3 = arguments[4]; + y3 = arguments[5]; + } else if (arguments.length === 7) { + x1 = arguments[0]; + y1 = arguments[1]; + x2 = arguments[2]; + y2 = arguments[3]; + x3 = arguments[4]; + y3 = arguments[5]; + mode = arguments[6]; + } else if (arguments.length === 1) { + if (Array.isArray(arguments[0])) { + const coords = arguments[0]; + if (coords.length >= 6) { + x1 = coords[0]; + y1 = coords[1]; + x2 = coords[2]; + y2 = coords[3]; + x3 = coords[4]; + y3 = coords[5]; + } + } else if (arguments[0] && arguments[0].points) { + const obj = arguments[0]; + const coords = obj.points; + if (Array.isArray(coords) && coords.length >= 6) { + x1 = coords[0]; + y1 = coords[1]; + x2 = coords[2]; + y2 = coords[3]; + x3 = coords[4]; + y3 = coords[5]; + } + if (obj.mode) mode = obj.mode; + } + } else if (arguments.length === 2) { + if (Array.isArray(arguments[0])) { + const coords = arguments[0]; + if (coords.length >= 6) { + x1 = coords[0]; + y1 = coords[1]; + x2 = coords[2]; + y2 = coords[3]; + x3 = coords[4]; + y3 = coords[5]; + } + mode = arguments[1]; + } + } else { + return console.error("Invalid triangle call. Expected tri(x1, y1, x2, y2, x3, y3) or tri(x1, y1, x2, y2, x3, y3, mode)"); + } + if ([x1, y1, x2, y2, x3, y3].some((coord) => coord === void 0 || isNaN(coord))) { + return console.error("Invalid triangle coordinates:", { x1, y1, x2, y2, x3, y3 }); + } + if (nonvalue(x1)) x1 = randInt(width); + if (nonvalue(y1)) y1 = randInt(height); + if (nonvalue(x2)) x2 = randInt(width); + if (nonvalue(y2)) y2 = randInt(height); + if (nonvalue(x3)) x3 = randInt(width); + if (nonvalue(y3)) y3 = randInt(height); + x1 = floor7(x1); + y1 = floor7(y1); + x2 = floor7(x2); + y2 = floor7(y2); + x3 = floor7(x3); + y3 = floor7(y3); + if (mode === "fill" || mode === "") { + const points = [[x1, y1], [x2, y2], [x3, y3]]; + fillShape(points); + } else { + const thickness = parseInt(mode.split(":")[1]) || 1; + const outlineMode = mode.split(":")[0]; + if (outlineMode === "outline" || outlineMode === "out") { + line(x1, y1, x2, y2); + line(x2, y2, x3, y3); + line(x3, y3, x1, y1); + } else if (outlineMode === "inline" || outlineMode === "in") { + line(x1, y1, x2, y2); + line(x2, y2, x3, y3); + line(x3, y3, x1, y1); + } else { + const points = [[x1, y1], [x2, y2], [x3, y3]]; + fillShape(points); + } + } +} +function grid({ + box: { x, y, w: cols, h: rows }, + transform: { scale: scale7, angle: angle3, width: twidth, height: theight, anchor }, + centers = [] +}, buffer) { + const gridStart = performance.now(); + const isSlowFrame = graphPerf2 && graphPerf2.lastFPS && graphPerf2.lastFPS < 15; + const oc = c.slice(); + let w, h; + if (scale7 !== void 0) { + if (number(scale7)) scale7 = { x: scale7, y: scale7 }; + w = cols * scale7.x; + h = rows * scale7.y; + } else if (twidth !== void 0 && theight !== void 0) { + w = twidth; + h = theight; + scale7 = { x: w / cols, y: h / rows }; + } + const colPix = w / cols, rowPix = h / rows; + if (scale7.x < 0) x -= w; + if (scale7.y < 0) y -= h; + angle3 = wrap(angle3, 360); + let xmod = 0, ymod = 0; + if (angle3) { + if (w % 2 !== 0 && h % 2 === 0) { + if (x % 1 !== 0 && angle3 === 90) xmod += 0.5; + if (angle3 === 270) { + xmod += x % 1 !== 0 ? 1 : 0.5; + ymod += 0.5; + } + if (angle3 === 180) { + ymod += 0.5; + if (x % 1 === 0) xmod += 0.5; + } + } + if (h % 2 !== 0 && w % 2 === 0) { + if (y % 1 !== 0 && angle3 === 90) ymod += 0.5; + if (angle3 === 270) { + xmod += 0.5; + ymod += y % 1 !== 0 ? 1 : 0.5; + } + if (angle3 === 180) { + xmod += 0.5; + if (y % 1 === 0) ymod += 0.5; + } + } else if (w % 2 === 0 && h % 2 === 0) { + xmod += 0.5; + ymod += 0.5; + } else if (w % 2 !== 0 && h % 2 !== 0) { + xmod += 0.5; + ymod += 0.5; + } + } + if (scale7.x < 0 && scale7.y > 0) { + if (angle3 >= 90 && angle3 < 270) ymod += 1 * mathSign(-scale7.y); + if (angle3 >= 180 && angle3 <= 270) xmod += 1 * mathSign(-scale7.x); + } else if (scale7.x > 0 && scale7.y > 0) { + if (angle3 >= 90 && angle3 < 270) xmod += 1 * mathSign(-scale7.x); + if (angle3 >= 180 && angle3 <= 270) ymod += 1 * mathSign(-scale7.y); + } else if (scale7.y < 0 && scale7.x > 0) { + if (angle3 >= 90 && angle3 < 270) ymod += 1 * mathSign(-scale7.y); + if (angle3 >= 180 && angle3 <= 270) xmod += 1 * mathSign(-scale7.x); + } else if (scale7.y < 0 && scale7.x < 0) { + if (angle3 >= 90 && angle3 < 270) xmod += 1 * mathSign(-scale7.x); + if (angle3 >= 180 && angle3 <= 270) ymod += 1 * mathSign(-scale7.y); + } + x += xmod; + y += ymod; + let centerX, centerY; + if (anchor) { + centerX = anchor.x + x; + centerY = anchor.y + y; + } else { + centerX = x + w / 2; + centerY = y + h / 2; + } + angle3 = radians(angle3); + if (buffer) { + const bufWidth = buffer.width; + const bufHeight = buffer.height; + let bufPixels = buffer.pixels; + if (!bufPixels) { + bufPixels = ensureBufferPixels(buffer); + } + if (!bufPixels) { + console.warn("\u{1F3A8} grid: buffer missing pixel data, skipping render", buffer); + return; + } + const scaleXAbs = ~~abs3(scale7.x); + const scaleYAbs = ~~abs3(scale7.y); + const isAngleZero = angle3 === 0; + let cosValue, sinValue; + if (!isAngleZero) { + cosValue = cos3(angle3); + sinValue = sin3(angle3); + } + const bufferWidth = scaleXAbs; + const bufferHeight = scaleYAbs; + const halfBoxWidth = bufferWidth >> 1; + const halfBoxHeight = bufferHeight >> 1; + const adjustedBufferWidth = bufferWidth + (halfBoxWidth << 1); + const adjustedBufferHeight = bufferHeight + (halfBoxHeight << 1); + const colPixInt = ~~(w / cols); + const rowPixInt = ~~(h / rows); + if (isAngleZero && scale7.x === scaleXAbs && scale7.y === scaleYAbs && scale7.x > 0 && scale7.y > 0) { + for (let j = 0; j < rows; j += 1) { + const srcY = j % bufHeight; + const destStartY = ~~(y + j * rowPixInt); + const destEndY = ~~(y + (j + 1) * rowPixInt); + const pixelHeight = destEndY - destStartY; + if (pixelHeight > 0 && destStartY >= 0 && destEndY <= height) { + for (let i2 = 0; i2 < cols; i2 += 1) { + const srcX = i2 % bufWidth; + const srcIndex = srcX + srcY * bufWidth << 2; + if (srcIndex < bufPixels.length) { + const r2 = bufPixels[srcIndex]; + const g = bufPixels[srcIndex + 1]; + const b2 = bufPixels[srcIndex + 2]; + const a2 = bufPixels[srcIndex + 3]; + const destStartX = ~~(x + i2 * colPixInt); + const destEndX = ~~(x + (i2 + 1) * colPixInt); + const pixelWidth = destEndX - destStartX; + if (pixelWidth > 0 && destStartX >= 0 && destEndX <= width) { + color(r2, g, b2, a2); + if (pixelWidth > 1 && pixelHeight > 1) { + box(destStartX, destStartY, pixelWidth, pixelHeight, "fill"); + } else if (pixelHeight === 1) { + lineh(destStartX, destEndX - 1, destStartY); + } else { + for (let dy = 0; dy < pixelHeight; dy += 1) { + lineh(destStartX, destEndX - 1, destStartY + dy); + } + } + } + } + } + } + } + } else { + const skipStep = isSlowFrame ? 2 : 1; + for (let j = 0; j < rows; j += skipStep) { + const plotY = y + rowPix * j; + const repeatY = j % bufHeight; + for (let i2 = 0; i2 < cols; i2 += skipStep) { + const plotX = x + colPix * i2; + let finalX, finalY; + if (isAngleZero) { + finalX = plotX; + finalY = plotY; + } else { + const dx = plotX - centerX; + const dy = plotY - centerY; + finalX = dx * cosValue - dy * sinValue + centerX; + finalY = dx * sinValue + dy * cosValue + centerY; + } + if (finalX < -adjustedBufferWidth || finalX > width + adjustedBufferWidth || finalY < -adjustedBufferHeight || finalY > height + adjustedBufferHeight) { + continue; + } + const repeatX = i2 % bufWidth; + const pixIndex = repeatX + bufWidth * repeatY << 2; + if (pixIndex < bufPixels.length) { + const colorData = [ + bufPixels[pixIndex], + bufPixels[pixIndex + 1], + bufPixels[pixIndex + 2], + bufPixels[pixIndex + 3] + ]; + color(...colorData); + const edgeX = Math.round(x + i2 * colPix); + const edgeX2 = Math.round(x + (i2 + 1) * colPix); + const edgeY = Math.round(y + j * rowPix); + const edgeY2 = Math.round(y + (j + 1) * rowPix); + const destStartX = Math.min(edgeX, edgeX2); + const destEndX = Math.max(edgeX, edgeX2); + const destStartY = Math.min(edgeY, edgeY2); + const destEndY = Math.max(edgeY, edgeY2); + const pixelWidth = destEndX - destStartX; + const pixelHeight = destEndY - destStartY; + if (pixelWidth > 0 && pixelHeight > 0) { + if (isAngleZero) { + if (pixelWidth > 1 && pixelHeight > 1) { + box(destStartX, destStartY, pixelWidth, pixelHeight, "fill"); + } else { + for (let dy = 0; dy < pixelHeight; dy += 1) { + lineh(destStartX, destEndX - 1, destStartY + dy); + } + } + } else { + const boxSize = Math.max(2, Math.ceil(Math.max(pixelWidth, pixelHeight) * 1.2)); + for (let dy = 0; dy < pixelHeight; dy += 1) { + for (let dx = 0; dx < pixelWidth; dx += 1) { + const px = destStartX + dx; + const py = destStartY + dy; + const relX = px - centerX; + const relY = py - centerY; + const rotX = relX * cosValue - relY * sinValue + centerX; + const rotY = relX * sinValue + relY * cosValue + centerY; + box(~~rotX, ~~rotY, boxSize, boxSize, "fill"); + } + } + } + } + } + } + } + } + } else { + const right = x + w - 1, bottom = y + h - 1; + color(64, 64, 64); + plot(x, y); + plot(right, y); + plot(x, bottom); + plot(right, bottom); + color(...oc); + for (let i2 = 0; i2 < cols; i2 += 1) { + const plotX = x + colPix * i2; + for (let j = 0; j < rows; j += 1) { + const plotY = y + rowPix * j; + const alphaMod = oc[3] / 255; + color(oc[0], oc[1], oc[2], even(i2 + j) ? 50 * alphaMod : 75 * alphaMod); + box(plotX, plotY, scale7.x, scale7.y); + centers.forEach((p) => { + color(oc[0], oc[1], oc[2], 100); + plot(plotX + p.x, plotY + p.y); + }); + } + } + color(...oc); + } + const gridEnd = performance.now(); + graphPerf2.track("grid", gridEnd - gridStart); +} +function draw() { + const args = arguments; + let drawing = args[0], x, y, scale7 = 1, angle3 = 0, thickness = 1; + if (typeof args[1] === "number") { + x = args[1]; + y = args[2]; + scale7 = args[3] || scale7; + angle3 = args[4] || angle3; + thickness = args[5] || thickness; + } else if (typeof args[1] === "object") { + drawing = args[0]; + if (Array.isArray(args[1])) { + x = args[1][0]; + y = args[1][1]; + } else { + x = args[1].x; + y = args[1].y; + } + scale7 = args[2] || scale7; + angle3 = args[3] || angle3; + thickness = args[4] || thickness; + } + if (drawing === void 0 || drawing === null) return; + let xOffset = 0; + let yOffset = 0; + if (drawing.offset) { + xOffset = drawing.offset[0] * scale7; + yOffset = drawing.baselineOffset ? drawing.baselineOffset[1] * scale7 : drawing.offset[1] * scale7; + } + if (drawing.pixels && drawing.resolution && !drawing.commands) { + const [charWidth, charHeight] = drawing.resolution; + const angleRad = radians(angle3); + const sinA = sin3(angleRad); + const cosA = cos3(angleRad); + const hasRotation = angle3 !== 0; + const centerX = charWidth / 2; + const centerY = charHeight / 2; + x += xOffset; + y += yOffset; + x = floor7(x); + y = floor7(y); + pan(x, y); + for (let row = 0; row < drawing.pixels.length; row++) { + const pixelRow = drawing.pixels[row]; + if (!Array.isArray(pixelRow)) continue; + for (let col = 0; col < pixelRow.length; col++) { + if (pixelRow[col] === 1) { + let pixelX = col - centerX; + let pixelY = row - centerY; + if (hasRotation) { + const rotatedX = pixelX * cosA - pixelY * sinA; + const rotatedY = pixelX * sinA + pixelY * cosA; + pixelX = rotatedX; + pixelY = rotatedY; + } + pixelX += centerX; + pixelY += centerY; + pixelX *= scale7; + pixelY *= scale7; + const finalX = Math.round(pixelX); + const finalY = Math.round(pixelY); + const isMatrixChunky = drawing?.fontName === "MatrixChunky8"; + const matrixDebugActive = matrixDebugEnabled(); + if (!matrixDebugActive && matrixChunkyDebugCount !== 0) + matrixChunkyDebugCount = 0; + if (isMatrixChunky && matrixDebugActive && row < 8 && col < 8) { + console.log("\u{1F9F1} MatrixChunky8 draw", { + char: drawing?.char || drawing?.code || "?", + row, + col, + finalX, + finalY, + scale: scale7, + currentColor: c4.slice(0, 4), + skip: skips.length, + mask: activeMask ? { + x: activeMask.x, + y: activeMask.y, + width: activeMask.width, + height: activeMask.height + } : null + }); + } + let beforePixel; + let logged = false; + if (isMatrixChunky && matrixDebugActive && matrixChunkyDebugCount < 20) { + beforePixel = pixel(finalX, finalY); + logged = true; + } + if (scale7 === 1) { + point(finalX, finalY); + } else { + for (let sy = 0; sy < scale7; sy++) { + for (let sx = 0; sx < scale7; sx++) { + point(finalX + sx, finalY + sy); + } + } + } + if (logged) { + const afterPixel = pixel(finalX, finalY); + matrixChunkyDebugCount += 1; + console.log("\u{1FA84} MatrixChunky8 point write", { + char: drawing?.char || drawing?.code || "?", + row, + col, + finalX, + finalY, + color: c4.slice(0, 4), + before: beforePixel, + after: afterPixel + }); + } + } + } + } + pan(-x, -y); + return; + } + angle3 = radians(angle3); + const s2 = sin3(angle3); + const c4 = cos3(angle3); + if (xOffset !== 0 || yOffset !== 0) { + const rotatedXOffset = xOffset * c4 - yOffset * s2; + const rotatedYOffset = xOffset * s2 + yOffset * c4; + x += Math.round(rotatedXOffset); + y += Math.round(rotatedYOffset); + } + x = floor7(x); + y = floor7(y); + pan(x, y); + const gesture = []; + function paintGesture() { + thickness === 1 ? poly(gesture) : pline(gesture, thickness); + gesture.length = 0; + } + drawing?.commands?.forEach(({ name, args: args2 }, i2) => { + args2 = args2.map((a2) => a2 * scale7); + if (name === "line") { + let x1 = args2[0]; + let y1 = args2[1]; + let x2 = args2[2]; + let y2 = args2[3]; + let nx1 = Math.round(x1 * c4 - y1 * s2); + let ny1 = Math.round(x1 * s2 + y1 * c4); + let nx2 = Math.round(x2 * c4 - y2 * s2); + let ny2 = Math.round(x2 * s2 + y2 * c4); + if (nx1 !== nx2 || ny1 !== ny2) { + if (thickness === 1) { + gesture.push([nx1, ny1], [nx2, ny2]); + } else { + gesture.push({ x: nx1, y: ny1 }, { x: nx2, y: ny2 }); + } + } + const nextCommand = drawing.commands[i2 + 1]; + if (nextCommand && nextCommand.name === "line") { + const nextArgs = nextCommand.args.map((a2) => a2 * scale7); + if (args2[2] !== nextArgs[0] || args2[3] !== nextArgs[1]) { + paintGesture(); + } else { + gesture.pop(); + } + } else { + paintGesture(); + } + } else if (name === "point") { + let px = args2[0]; + let py = args2[1]; + let npx = Math.round(px * c4 - py * s2); + let npy = Math.round(px * s2 + py * c4); + thickness === 1 ? point(npx, npy) : circle(npx, npy, thickness / 2, true); + } + }); + pan(-x, -y); +} +function printLine(text, font, startX, startY, blockWidth = 6, scale7 = 1, xOffset = 0, thickness = 1, rotation = 0, fontMetadata = null, fallbackFont = null) { + if (!text) return; + if (activeMask) { + const lineHeight = font?.A?.box?.height || 10; + const scaledHeight = lineHeight * scale7; + if (startY >= activeMask.y + activeMask.height || startY + scaledHeight <= activeMask.y || startX >= activeMask.x + activeMask.width || startX + text.length * blockWidth * scale7 <= activeMask.x) { + return; + } + } + const isProportional = fontMetadata?.proportional === true || fontMetadata?.bdfFont === "MatrixChunky8" || fontMetadata?.name === "MatrixChunky8"; + const getGlyphForChar = (char) => { + if (char.trim() === "") return null; + let primaryGlyph = null; + let primaryQuestion = null; + let fallbackGlyph = null; + let fallbackQuestion = null; + try { + primaryGlyph = font?.[char] || null; + primaryQuestion = font?.["?"] || null; + } catch (err) { + primaryGlyph = null; + primaryQuestion = null; + } + if (primaryGlyph && !primaryGlyph.isPlaceholder) return primaryGlyph; + if (fallbackFont) { + try { + fallbackGlyph = fallbackFont?.[char] || null; + fallbackQuestion = fallbackFont?.["?"] || null; + } catch (err) { + fallbackGlyph = null; + fallbackQuestion = null; + } + if (fallbackGlyph && !fallbackGlyph.isPlaceholder) return fallbackGlyph; + if (fallbackQuestion) return fallbackQuestion; + } + if (primaryGlyph) return primaryGlyph; + if (primaryQuestion) return primaryQuestion; + return null; + }; + if (isProportional) { + const advances = fontMetadata?.advances || {}; + const bdfOverrides = fontMetadata?.bdfOverrides || {}; + let currentX = startX + xOffset; + [...text.toString()].forEach((char, i2) => { + let charX = currentX; + let charY = startY; + if (bdfOverrides[char]) { + const override = bdfOverrides[char]; + if (override.x !== void 0) { + charX += override.x * scale7; + } + if (override.y !== void 0) { + charY += override.y * scale7; + } + } + const charAdvance = advances[char] || blockWidth; + draw( + getGlyphForChar(char), + charX, + charY, + scale7, + rotation, + thickness + ); + currentX += charAdvance * scale7; + }); + } else { + [...text.toString()].forEach((char, i2) => { + draw( + getGlyphForChar(char), + startX + blockWidth * scale7 * i2 + xOffset, + startY, + scale7, + rotation, + thickness + ); + }); + } +} +function noise16() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, activeMask.x); + minY = Math.max(0, activeMask.y); + maxX = Math.min(width, activeMask.x + activeMask.width); + maxY = Math.min(height, activeMask.y + activeMask.height); + } + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const i2 = (y * width + x) * 4; + pixels[i2] = byteInterval17(randInt(16)); + pixels[i2 + 1] = byteInterval17(randInt(16)); + pixels[i2 + 2] = byteInterval17(randInt(16)); + pixels[i2 + 3] = 255; + } + } +} +function noise16DIGITPAIN() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, activeMask.x); + minY = Math.max(0, activeMask.y); + maxX = Math.min(width, activeMask.x + activeMask.width); + maxY = Math.min(height, activeMask.y + activeMask.height); + } + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const i2 = (y * width + x) * 4; + pixels[i2] = byteInterval17(randInt(16)) * 0.6; + pixels[i2 + 1] = byteInterval17(randInt(16)) * 0.15; + pixels[i2 + 2] = byteInterval17(randInt(16)) * 0.55; + pixels[i2 + 3] = 255; + } + } +} +function noise16Aesthetic() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, activeMask.x); + minY = Math.max(0, activeMask.y); + maxX = Math.min(width, activeMask.x + activeMask.width); + maxY = Math.min(height, activeMask.y + activeMask.height); + } + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const i2 = (y * width + x) * 4; + pixels[i2] = byteInterval17(randInt(16)) * 0.4; + pixels[i2 + 1] = byteInterval17(randInt(16)) * 0.15; + pixels[i2 + 2] = byteInterval17(randInt(16)) * 0.8; + pixels[i2 + 3] = 255; + } + } +} +function noise16Sotce() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, activeMask.x); + minY = Math.max(0, activeMask.y); + maxX = Math.min(width, activeMask.x + activeMask.width); + maxY = Math.min(height, activeMask.y + activeMask.height); + } + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const i2 = (y * width + x) * 4; + if (flip()) pixels[i2] = byteInterval17(14 + randInt(2)); + if (flip()) pixels[i2 + 1] = byteInterval17(8 + randInt(2)) * 0.9; + if (flip()) pixels[i2 + 2] = byteInterval17(8 + randInt(2)) * 0.9; + pixels[i2 + 3] = 255; + } + } +} +function noiseTinted(tint, amount, saturation) { + tint = findColor2(tint); + for (let i2 = 0; i2 < pixels.length; i2 += 4) { + const grayscale = randInt(255); + pixels[i2] = lerp5( + lerp5(grayscale, randInt(255), saturation), + tint[0], + amount + ); + pixels[i2 + 1] = lerp5( + lerp5(grayscale, randInt(255), saturation), + tint[1], + amount + ); + pixels[i2 + 2] = lerp5( + lerp5(grayscale, randInt(255), saturation), + tint[2], + amount + ); + pixels[i2 + 3] = 255; + } +} +var spinAccumulator = 0; +var zoomAccumulator = 0; +var zoomTimeOffset = Math.random() * 1e3; +var scrollAccumulatorX = 0; +var scrollAccumulatorY = 0; +function resetScrollState() { + scrollAccumulatorX = 0; + scrollAccumulatorY = 0; +} +var shearAccumulatorX = 0; +var shearAccumulatorY = 0; +var suckAccumulator = 0; +function scroll(dx = 0, dy = 0) { + if (dx === 0 && dy === 0) return; + scrollAccumulatorX += dx; + scrollAccumulatorY += dy; + const integerDx = Math.trunc(scrollAccumulatorX); + const integerDy = Math.trunc(scrollAccumulatorY); + scrollAccumulatorX -= integerDx; + scrollAccumulatorY -= integerDy; + if (integerDx === 0 && integerDy === 0) { + return; + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskMinX = Math.floor(activeMask.x); + const maskMinY = Math.floor(activeMask.y); + const maskMaxX = Math.floor(activeMask.x + activeMask.width); + const maskMaxY = Math.floor(activeMask.y + activeMask.height); + minX = Math.max(0, Math.min(width, maskMinX)); + maxX = Math.max(0, Math.min(width, maskMaxX)); + minY = Math.max(0, Math.min(height, maskMinY)); + maxY = Math.max(0, Math.min(height, maskMaxY)); + } + const boundsWidth = maxX - minX; + const boundsHeight = maxY - minY; + if (boundsWidth <= 0 || boundsHeight <= 0) { + return; + } + let finalDx = (integerDx % boundsWidth + boundsWidth) % boundsWidth; + let finalDy = (integerDy % boundsHeight + boundsHeight) % boundsHeight; + if (finalDx === 0 && finalDy === 0) { + return; + } + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("scroll") && gpuSpinModule?.gpuScroll && pixels && width && height) { + const mask2 = activeMask ? { + x: minX, + y: minY, + width: boundsWidth, + height: boundsHeight + } : null; + const success = gpuSpinModule.gpuScroll(pixels, width, height, finalDx, finalDy, mask2); + if (success) { + gpuOk("scroll"); + return; + } + gpuFailed("scroll"); + } + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in scroll, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const tempPixels = new Uint8ClampedArray(pixels); + for (let y = 0; y < boundsHeight; y++) { + const srcY = minY + (y + boundsHeight - finalDy) % boundsHeight; + const destY = minY + y; + if (finalDx === 0) { + const srcRowStart = (srcY * width + minX) * 4; + const destRowStart = (destY * width + minX) * 4; + const rowBytes = boundsWidth * 4; + pixels.set(tempPixels.subarray(srcRowStart, srcRowStart + rowBytes), destRowStart); + } else { + const srcStartX = minX + finalDx; + const chunk1Width = boundsWidth - finalDx; + if (chunk1Width > 0) { + const srcStart = (srcY * width + srcStartX) * 4; + const destStart = (destY * width + minX) * 4; + const chunk1Bytes = chunk1Width * 4; + pixels.set(tempPixels.subarray(srcStart, srcStart + chunk1Bytes), destStart); + } + const chunk2Width = finalDx; + if (chunk2Width > 0) { + const srcStart = (srcY * width + minX) * 4; + const destStart = (destY * width + minX + chunk1Width) * 4; + const chunk2Bytes = chunk2Width * 4; + pixels.set(tempPixels.subarray(srcStart, srcStart + chunk2Bytes), destStart); + } + } + } +} +function flip2() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskMinX = Math.floor(activeMask.x); + const maskMinY = Math.floor(activeMask.y); + const maskMaxX = Math.floor(activeMask.x + activeMask.width); + const maskMaxY = Math.floor(activeMask.y + activeMask.height); + minX = Math.max(0, Math.min(width, maskMinX)); + maxX = Math.max(0, Math.min(width, maskMaxX)); + minY = Math.max(0, Math.min(height, maskMinY)); + maxY = Math.max(0, Math.min(height, maskMaxY)); + } + const boundsWidth = maxX - minX; + const boundsHeight = maxY - minY; + if (boundsWidth <= 0 || boundsHeight <= 1) return; + if (gpuSpinEnabled && gpuSpinAvailable && gpuSpinModule?.gpuComposite && pixels && width && height) { + const mask2 = { + x: minX, + y: minY, + width: boundsWidth, + height: boundsHeight + }; + const success = gpuSpinModule.gpuComposite(pixels, width, height, { + zoom: 1, + zoomAnchorX: 0.5, + zoomAnchorY: 0.5, + scrollX: 0, + scrollY: 0, + flipY: true, + contrast: 1, + brightness: 0, + mask: mask2 + }); + if (success) return; + } + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in flip, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const tempPixels = new Uint8ClampedArray(pixels); + for (let y = 0; y < boundsHeight; y++) { + const srcY = maxY - 1 - y; + const destY = minY + y; + const srcRowStart = (srcY * width + minX) * 4; + const destRowStart = (destY * width + minX) * 4; + const rowBytes = boundsWidth * 4; + pixels.set(tempPixels.subarray(srcRowStart, srcRowStart + rowBytes), destRowStart); + } +} +var spinBuffer; +var zoomBuffer; +var spinSkipCounter = 0; +var gpuEnabled = true; +var gpuSpinModule = null; +var gpuSpinEnabled = gpuEnabled; +var gpuSpinAvailable = null; +var gpuFloodAvailable = null; +var gpuFloodEnabled = gpuEnabled; +var gpuLayerCompositeAvailable = null; +var gpuLayerCompositeEnabled = gpuEnabled; +var gpuInitPromise = null; +var GPU_FAIL_THRESHOLD = 3; +var GPU_CACHE_KEY = "ac-gpu-disabled-v4"; +var gpuFailCounts = { + spin: 0, + composite: 0, + blur: 0, + sharpen: 0, + shear: 0, + suck: 0, + flood: 0, + zoom: 0, + scroll: 0, + contrast: 0, + brightness: 0, + invert: 0, + compositeLayers: 0 +}; +var gpuDisabled = {}; +var gpuFailoverResetCallback = null; +try { + const cached = typeof localStorage !== "undefined" && localStorage.getItem(GPU_CACHE_KEY); + if (cached) { + const parsed = JSON.parse(cached); + for (const key of Object.keys(parsed)) { + gpuDisabled[key] = true; + gpuFailCounts[key] = GPU_FAIL_THRESHOLD; + } + console.log(`\u{1F3AE} GPU: Loaded cached disabled effects: ${Object.keys(parsed).join(", ")}`); + } +} catch { +} +function gpuFailed(effectName) { + gpuFrameLog.failed.add(effectName); + gpuFailCounts[effectName] = (gpuFailCounts[effectName] || 0) + 1; + if (gpuFailCounts[effectName] >= GPU_FAIL_THRESHOLD && !gpuDisabled[effectName]) { + gpuDisabled[effectName] = true; + console.warn(`\u{1F3AE} GPU ${effectName}: Auto-disabled after ${GPU_FAIL_THRESHOLD} failures \u2014 using CPU fallback`); + gpuTelemetry.report("gpu-disabled", effectName); + try { + if (typeof localStorage !== "undefined") { + localStorage.setItem(GPU_CACHE_KEY, JSON.stringify(gpuDisabled)); + } + } catch { + } + if (typeof self !== "undefined") self.__gpuFailoverOccurred = true; + if (gpuFailoverResetCallback) gpuFailoverResetCallback(); + } + return false; +} +function gpuOk(effectName) { + if (gpuFailCounts[effectName] > 0) gpuFailCounts[effectName] = 0; + gpuFrameLog.ok.add(effectName); +} +function gpuAllowed(effectName) { + return !gpuDisabled[effectName]; +} +function getGpuStatus() { + return { failCounts: { ...gpuFailCounts }, disabled: { ...gpuDisabled } }; +} +var gpuFrameLog = { + frameCount: 0, + ok: /* @__PURE__ */ new Set(), + // effects that succeeded this period + failed: /* @__PURE__ */ new Set(), + // effects that failed this period + disabled: /* @__PURE__ */ new Set(), + // effects skipped (auto-disabled) + rendererCached: null + // GPU renderer string (cached on first query) +}; +function gpuLogTick() { + gpuFrameLog.frameCount++; + if (gpuFrameLog.frameCount % 8 !== 0) return; + if (!gpuFrameLog.rendererCached) { + try { + if (typeof OffscreenCanvas !== "undefined") { + const c4 = new OffscreenCanvas(1, 1); + const g = c4.getContext("webgl2"); + if (g) { + const dbg = g.getExtension("WEBGL_debug_renderer_info"); + gpuFrameLog.rendererCached = dbg ? g.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "unknown"; + g.getExtension("WEBGL_lose_context")?.loseContext(); + } else { + gpuFrameLog.rendererCached = "no-webgl2"; + } + } else { + gpuFrameLog.rendererCached = "no-offscreen"; + } + } catch { + gpuFrameLog.rendererCached = "error"; + } + } + const ok = [...gpuFrameLog.ok]; + const failed = [...gpuFrameLog.failed]; + const dis = Object.keys(gpuDisabled).filter((k) => gpuDisabled[k]); + if (gpuFrameLog.frameCount === 8) { + console.log(`\u{1F3AE} GPU renderer=${gpuFrameLog.rendererCached} ok=[${ok}] fail=[${failed}] disabled=[${dis}]`); + } + if (gpuFrameLog.frameCount === 8 || failed.length > 0) { + gpuTelemetry.report("gpu-status", null, { + frame: gpuFrameLog.frameCount, + renderer: gpuFrameLog.rendererCached, + ok, + failed, + disabled: dis + }); + } + gpuFrameLog.ok.clear(); + gpuFrameLog.failed.clear(); +} +var gpuTelemetry = /* @__PURE__ */ (() => { + const queue = []; + let flushTimer = null; + let deviceInfo = null; + function getDevice() { + if (deviceInfo) return deviceInfo; + const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""; + deviceInfo = { + userAgent: ua, + mobile: /Mobi|Android|iPhone|iPad/i.test(ua), + screen: typeof self !== "undefined" && self.screen ? { w: self.screen.width, h: self.screen.height } : null, + gpu: null + // filled on first report + }; + return deviceInfo; + } + function report(type, effect, detail) { + const device = getDevice(); + if (!device.gpu && typeof OffscreenCanvas !== "undefined") { + try { + const c4 = new OffscreenCanvas(1, 1); + const g = c4.getContext("webgl2"); + if (g) { + const dbg = g.getExtension("WEBGL_debug_renderer_info"); + if (dbg) { + device.gpu = { + vendor: g.getParameter(dbg.UNMASKED_VENDOR_WEBGL), + renderer: g.getParameter(dbg.UNMASKED_RENDERER_WEBGL) + }; + } + g.getExtension("WEBGL_lose_context")?.loseContext(); + } + } catch { + } + } + queue.push({ + type, + effect, + detail: detail || null, + device, + gpuStatus: getGpuStatus() + }); + if (!flushTimer) { + flushTimer = setTimeout(flush, 2e3); + } + } + async function flush() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + if (!queue.length) return; + const events = queue.splice(0, queue.length); + try { + await fetch("/api/kidlisp-log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ events }), + keepalive: true + }); + } catch { + } + } + return { report, flush }; +})(); +async function initGpuEffects() { + if (gpuInitPromise) return gpuInitPromise; + gpuInitPromise = (async () => { + try { + gpuSpinModule = await import("./gpu-effects.mjs"); + gpuSpinAvailable = gpuSpinModule.isGpuEffectsAvailable(); + gpuFloodAvailable = gpuSpinModule.isGpuFloodAvailable?.() ?? false; + gpuLayerCompositeAvailable = gpuSpinModule.isGpuLayerCompositeAvailable?.() ?? false; + } catch (e2) { + console.warn("\u{1F3AE} GPU Effects: Module load failed, using CPU fallback", e2); + gpuSpinAvailable = false; + gpuFloodAvailable = false; + gpuLayerCompositeAvailable = false; + } + return gpuSpinAvailable; + })(); + return gpuInitPromise; +} +var gpuContrastEnabled = gpuEnabled; +function setGpuConfig(config) { + gpuEnabled = !!config.gpu; + const effects = config.effects || {}; + gpuSpinEnabled = gpuEnabled && effects.spin !== false; + gpuFloodEnabled = gpuEnabled && effects.flood !== false; + gpuContrastEnabled = gpuEnabled && effects.contrast !== false; + gpuLayerCompositeEnabled = gpuEnabled && effects.composite !== false; + console.log(`\u{1F3AE} GPU Config: master=${gpuEnabled}, spin=${gpuSpinEnabled}, flood=${gpuFloodEnabled}, contrast=${gpuContrastEnabled}, composite=${gpuLayerCompositeEnabled}`); + if (gpuEnabled && !gpuSpinModule) initGpuEffects(); +} +function compositeLayers(layers) { + if (!layers || layers.length === 0) return true; + const compositeStart = performance.now(); + if (gpuLayerCompositeEnabled && gpuLayerCompositeAvailable && gpuAllowed("compositeLayers") && gpuSpinModule && pixels && width && height) { + const result = gpuSpinModule.gpuCompositeLayers(pixels, width, height, layers); + if (result.success) { + gpuOk("compositeLayers"); + const compositeTime = performance.now() - compositeStart; + graphPerf2.track("composite-layers-gpu", compositeTime); + return true; + } + gpuFailed("compositeLayers"); + } + const cpuStart = performance.now(); + for (const layer of layers) { + if (!layer.pixels) continue; + const alpha = layer.alpha !== void 0 ? layer.alpha : 255; + const destX = Math.floor(layer.x || 0); + const destY = Math.floor(layer.y || 0); + const srcW = layer.width; + const srcH = layer.height; + const src = layer.pixels; + const startX = Math.max(0, destX); + const startY = Math.max(0, destY); + const endX = Math.min(width, destX + srcW); + const endY = Math.min(height, destY + srcH); + if (startX >= endX || startY >= endY) continue; + const alphaFactor = alpha / 255; + for (let dy = startY; dy < endY; dy++) { + const srcY = dy - destY; + const srcRowStart = srcY * srcW * 4; + const dstRowStart = dy * width * 4; + for (let dx = startX; dx < endX; dx++) { + const srcX = dx - destX; + const srcIdx = srcRowStart + srcX * 4; + const dstIdx = dstRowStart + dx * 4; + const sA = src[srcIdx + 3] * alphaFactor; + if (sA < 1) continue; + const sR = src[srcIdx]; + const sG = src[srcIdx + 1]; + const sB = src[srcIdx + 2]; + if (sA >= 254) { + pixels[dstIdx] = sR; + pixels[dstIdx + 1] = sG; + pixels[dstIdx + 2] = sB; + pixels[dstIdx + 3] = 255; + } else { + const invAlpha = 255 - sA; + pixels[dstIdx] = sA * sR + invAlpha * pixels[dstIdx] >> 8; + pixels[dstIdx + 1] = sA * sG + invAlpha * pixels[dstIdx + 1] >> 8; + pixels[dstIdx + 2] = sA * sB + invAlpha * pixels[dstIdx + 2] >> 8; + pixels[dstIdx + 3] = Math.min(255, pixels[dstIdx + 3] + (sA >> 1)); + } + } + } + } + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("composite-layers-cpu", cpuTime); + return false; +} +function batchedEffects(options = {}) { + const { + zoom: zoom2 = 1, + zoomAnchorX = 0.5, + zoomAnchorY = 0.5, + scrollX = 0, + scrollY = 0, + contrast: contrastLevel = 1, + brightness: brightnessLevel = 0 + } = options; + const hasZoom = zoom2 !== 1; + const hasScroll = scrollX !== 0 || scrollY !== 0; + const hasContrast = contrastLevel !== 1; + const hasBrightness = brightnessLevel !== 0; + if (!hasZoom && !hasScroll && !hasContrast && !hasBrightness) { + return true; + } + const batchStart = performance.now(); + let mask2 = null; + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + mask2 = { + x: Math.max(0, Math.floor(maskX)), + y: Math.max(0, Math.floor(maskY)), + width: Math.min(width, Math.floor(maskX + activeMask.width)) - Math.max(0, Math.floor(maskX)), + height: Math.min(height, Math.floor(maskY + activeMask.height)) - Math.max(0, Math.floor(maskY)) + }; + } + if (gpuSpinAvailable && gpuAllowed("composite") && gpuSpinModule && pixels && width && height) { + const result = gpuSpinModule.gpuComposite?.(pixels, width, height, { + zoom: zoom2, + zoomAnchorX, + zoomAnchorY, + scrollX, + scrollY, + contrast: contrastLevel, + brightness: brightnessLevel, + mask: mask2 + }); + if (result) { + gpuOk("composite"); + const batchTime = performance.now() - batchStart; + graphPerf2.track("batched-effects-gpu", batchTime); + return true; + } + gpuFailed("composite"); + } + const cpuStart = performance.now(); + if (hasZoom) zoom_cpu(zoom2, zoomAnchorX, zoomAnchorY); + if (hasScroll) scroll_cpu(scrollX, scrollY); + if (hasContrast) contrast(contrastLevel); + if (hasBrightness) brightness(brightnessLevel); + const cpuTime = performance.now() - cpuStart; + graphPerf2.track("batched-effects-cpu", cpuTime); + return false; +} +function zoom_cpu(scale7, anchorX = 0.5, anchorY = 0.5) { + zoom(scale7, anchorX, anchorY); +} +function scroll_cpu(dx, dy) { + scroll(dx, dy); +} +function spin(steps = 0, anchorX = null, anchorY = null) { + const spinStart = performance.now(); + if (steps === 0) { + graphPerf2.track("spin", 0); + return; + } + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("spin") && gpuSpinModule && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x + panTranslation.x, + y: activeMask.y + panTranslation.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuSpin(pixels, width, height, steps, anchorX, anchorY, mask2); + if (success) { + gpuOk("spin"); + const spinEnd = performance.now(); + graphPerf2.track("spin", spinEnd - spinStart); + return; + } + gpuFailed("spin"); + } + return spinBlockBased(steps, anchorX, anchorY, spinStart); +} +spin.coordBuffers = null; +spin.destIndices = null; +function spinSimd(steps, anchorX, anchorY, spinStart) { + if (graphPerf2 && graphPerf2.lastFPS && graphPerf2.lastFPS < 6) { + spinSkipCounter++; + if (spinSkipCounter % 2 !== 0) { + graphPerf2.track("spin", 0); + return; + } + } else { + spinSkipCounter = 0; + } + if (steps === 0) { + graphPerf2.track("spin", 0); + return; + } + spinAccumulator += steps; + const integerSteps = floor7(spinAccumulator); + spinAccumulator -= integerSteps; + if (integerSteps === 0) { + graphPerf2.track("spin", 0); + return; + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x + panTranslation.x; + minY = activeMask.y + panTranslation.y; + maxX = activeMask.x + activeMask.width + panTranslation.x; + maxY = activeMask.y + activeMask.height + panTranslation.y; + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const centerX = anchorX !== null ? anchorX : minX + floor7(workingWidth / 2); + const centerY = anchorY !== null ? anchorY : minY + floor7(workingHeight / 2); + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in spinSimd, recreating"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const bufferSize = width * height * 4; + if (!spinBuffer || spinBuffer.length !== bufferSize) { + spinBuffer = new Uint8ClampedArray(bufferSize); + } + if (pixels.length !== bufferSize) { + pixels = new Uint8ClampedArray(bufferSize); + pixels.fill(0); + } + spinBuffer.set(pixels); + const coordCount = (maxY - minY) * (maxX - minX); + const maxCoords = Math.ceil(coordCount / 4) * 4; + if (!spinSimd.coordBuffers || spinSimd.coordBuffers.length < maxCoords * 2) { + spinSimd.coordBuffers = new Float32Array(maxCoords * 2); + spinSimd.srcIndices = new Uint32Array(maxCoords); + spinSimd.destIndices = new Uint32Array(maxCoords); + } + const coords = spinSimd.coordBuffers; + const srcIndices = spinSimd.srcIndices; + const destIndices = spinSimd.destIndices; + const twoPi = 2 * PI2; + const maxXMinus1 = maxX - 1; + const maxYMinus1 = maxY - 1; + let coordIndex = 0; + for (let destY = minY; destY < maxY; destY++) { + const dy = destY - centerY; + const dy2 = dy * dy; + const destRowOffset = destY * width; + for (let destX = minX; destX < maxX; destX++) { + const dx = destX - centerX; + const distanceSquared = dx * dx + dy2; + if (distanceSquared < 1) { + const idx = (destRowOffset + destX) * 4; + pixels[idx] = spinBuffer[idx]; + pixels[idx + 1] = spinBuffer[idx + 1]; + pixels[idx + 2] = spinBuffer[idx + 2]; + pixels[idx + 3] = spinBuffer[idx + 3]; + continue; + } + if (distanceSquared > 16e6) continue; + coords[coordIndex * 2] = dx; + coords[coordIndex * 2 + 1] = dy; + destIndices[coordIndex] = (destRowOffset + destX) * 4; + coordIndex++; + } + } + const groupsOf4 = Math.floor(coordIndex / 4); + const remainder = coordIndex % 4; + for (let group = 0; group < groupsOf4; group++) { + const baseIndex = group * 4; + const dx0 = coords[baseIndex * 2], dy0 = coords[baseIndex * 2 + 1]; + const dx1 = coords[(baseIndex + 1) * 2], dy1 = coords[(baseIndex + 1) * 2 + 1]; + const dx2 = coords[(baseIndex + 2) * 2], dy2 = coords[(baseIndex + 2) * 2 + 1]; + const dx3 = coords[(baseIndex + 3) * 2], dy3 = coords[(baseIndex + 3) * 2 + 1]; + const dist0 = Math.sqrt(dx0 * dx0 + dy0 * dy0); + const dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); + const dist22 = Math.sqrt(dx2 * dx2 + dy2 * dy2); + const dist32 = Math.sqrt(dx3 * dx3 + dy3 * dy3); + const angle0 = Math.atan2(dy0, dx0); + const angle1 = Math.atan2(dy1, dx1); + const angle22 = Math.atan2(dy2, dx2); + const angle3 = Math.atan2(dy3, dx3); + const totalChange0 = integerSteps / dist0; + const totalChange1 = integerSteps / dist1; + const totalChange2 = integerSteps / dist22; + const totalChange3 = integerSteps / dist32; + let srcAngle0 = angle0 - totalChange0; + let srcAngle1 = angle1 - totalChange1; + let srcAngle2 = angle22 - totalChange2; + let srcAngle3 = angle3 - totalChange3; + srcAngle0 = srcAngle0 - twoPi * Math.floor(srcAngle0 / twoPi); + srcAngle1 = srcAngle1 - twoPi * Math.floor(srcAngle1 / twoPi); + srcAngle2 = srcAngle2 - twoPi * Math.floor(srcAngle2 / twoPi); + srcAngle3 = srcAngle3 - twoPi * Math.floor(srcAngle3 / twoPi); + if (srcAngle0 < 0) srcAngle0 += twoPi; + if (srcAngle1 < 0) srcAngle1 += twoPi; + if (srcAngle2 < 0) srcAngle2 += twoPi; + if (srcAngle3 < 0) srcAngle3 += twoPi; + const srcX0 = centerX + dist0 * cos3(srcAngle0); + const srcY0 = centerY + dist0 * sin3(srcAngle0); + const srcX1 = centerX + dist1 * cos3(srcAngle1); + const srcY1 = centerY + dist1 * sin3(srcAngle1); + const srcX2 = centerX + dist22 * cos3(srcAngle2); + const srcY2 = centerY + dist22 * sin3(srcAngle2); + const srcX3 = centerX + dist32 * cos3(srcAngle3); + const srcY3 = centerY + dist32 * sin3(srcAngle3); + processSimdPixel(srcX0, srcY0, destIndices[baseIndex], minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight); + processSimdPixel(srcX1, srcY1, destIndices[baseIndex + 1], minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight); + processSimdPixel(srcX2, srcY2, destIndices[baseIndex + 2], minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight); + processSimdPixel(srcX3, srcY3, destIndices[baseIndex + 3], minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight); + } + for (let i2 = groupsOf4 * 4; i2 < coordIndex; i2++) { + const dx = coords[i2 * 2]; + const dy = coords[i2 * 2 + 1]; + const distance4 = Math.sqrt(dx * dx + dy * dy); + const angle3 = Math.atan2(dy, dx); + const totalAngleChange = integerSteps / distance4; + let srcAngle = angle3 - totalAngleChange; + srcAngle = srcAngle - twoPi * Math.floor(srcAngle / twoPi); + if (srcAngle < 0) srcAngle += twoPi; + const srcX = centerX + distance4 * Math.cos(srcAngle); + const srcY = centerY + distance4 * Math.sin(srcAngle); + processSimdPixel(srcX, srcY, destIndices[i2], minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight); + } + const spinEnd = performance.now(); + graphPerf2.track("spin", spinEnd - spinStart); +} +function processSimdPixel(srcX, srcY, destIdx, minX, minY, maxXMinus1, maxYMinus1, workingWidth, workingHeight) { + let wrappedSrcX = srcX; + let wrappedSrcY = srcY; + if (srcX < minX || srcX >= minX + workingWidth) { + const normalizedX = srcX - minX; + wrappedSrcX = minX + (normalizedX - workingWidth * Math.floor(normalizedX / workingWidth)); + if (wrappedSrcX < minX) wrappedSrcX += workingWidth; + } + if (srcY < minY || srcY >= minY + workingHeight) { + const normalizedY = srcY - minY; + wrappedSrcY = minY + (normalizedY - workingHeight * Math.floor(normalizedY / workingHeight)); + if (wrappedSrcY < minY) wrappedSrcY += workingHeight; + } + const nearestSrcX = Math.max(minX, Math.min(maxXMinus1, Math.round(wrappedSrcX))); + const nearestSrcY = Math.max(minY, Math.min(maxYMinus1, Math.round(wrappedSrcY))); + const srcIdx = (nearestSrcY * width + nearestSrcX) * 4; + pixels[destIdx] = spinBuffer[srcIdx]; + pixels[destIdx + 1] = spinBuffer[srcIdx + 1]; + pixels[destIdx + 2] = spinBuffer[srcIdx + 2]; + pixels[destIdx + 3] = spinBuffer[srcIdx + 3]; +} +spinSimd.coordBuffers = null; +spinSimd.srcIndices = null; +spinSimd.destIndices = null; +function spinBlockBased(steps, anchorX, anchorY, spinStart) { + if (graphPerf2 && graphPerf2.lastFPS && graphPerf2.lastFPS < 6) { + spinSkipCounter++; + if (spinSkipCounter % 2 !== 0) { + graphPerf2.track("spin", 0); + return; + } + } else { + spinSkipCounter = 0; + } + if (steps === 0) { + graphPerf2.track("spin", 0); + return; + } + spinAccumulator += steps; + const integerSteps = floor7(spinAccumulator); + spinAccumulator -= integerSteps; + if (integerSteps === 0) { + graphPerf2.track("spin", 0); + return; + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x + panTranslation.x; + minY = activeMask.y + panTranslation.y; + maxX = activeMask.x + activeMask.width + panTranslation.x; + maxY = activeMask.y + activeMask.height + panTranslation.y; + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const centerX = anchorX !== null ? anchorX : minX + floor7(workingWidth / 2); + const centerY = anchorY !== null ? anchorY : minY + floor7(workingHeight / 2); + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in spin, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const bufferSize = width * height * 4; + if (!spinBuffer || spinBuffer.length !== bufferSize) { + spinBuffer = new Uint8ClampedArray(bufferSize); + } + if (pixels.length !== bufferSize) { + pixels = new Uint8ClampedArray(bufferSize); + pixels.fill(0); + } + spinBuffer.set(pixels); + const twoPi = 2 * PI2; + const maxXMinus1 = maxX - 1; + const maxYMinus1 = maxY - 1; + for (let destY = minY; destY < maxY; destY++) { + const dy = destY - centerY; + const dy2 = dy * dy; + const destRowOffset = destY * width; + for (let destX = minX; destX < maxX; destX++) { + const dx = destX - centerX; + const distanceSquared = dx * dx + dy2; + if (distanceSquared < 1) { + const idx = (destRowOffset + destX) * 4; + pixels[idx] = spinBuffer[idx]; + pixels[idx + 1] = spinBuffer[idx + 1]; + pixels[idx + 2] = spinBuffer[idx + 2]; + pixels[idx + 3] = spinBuffer[idx + 3]; + continue; + } + const distance4 = sqrt3(distanceSquared); + const angle3 = Math.atan2(dy, dx); + const totalAngleChange = integerSteps / distance4; + let sourceAngle = angle3 - totalAngleChange; + sourceAngle = sourceAngle - twoPi * floor7(sourceAngle / twoPi); + if (sourceAngle < 0) sourceAngle += twoPi; + const srcXf = centerX + distance4 * cos3(sourceAngle); + const srcYf = centerY + distance4 * Math.sin(sourceAngle); + let wrappedSrcX = srcXf; + let wrappedSrcY = srcYf; + if (srcXf < minX || srcXf >= maxX) { + const normalizedX = srcXf - minX; + wrappedSrcX = minX + (normalizedX - workingWidth * floor7(normalizedX / workingWidth)); + if (wrappedSrcX < minX) wrappedSrcX += workingWidth; + } + if (srcYf < minY || srcYf >= maxY) { + const normalizedY = srcYf - minY; + wrappedSrcY = minY + (normalizedY - workingHeight * floor7(normalizedY / workingHeight)); + if (wrappedSrcY < minY) wrappedSrcY += workingHeight; + } + const nearestSrcX = Math.max(minX, Math.min(maxXMinus1, Math.round(wrappedSrcX))); + const nearestSrcY = Math.max(minY, Math.min(maxYMinus1, Math.round(wrappedSrcY))); + const srcIdx = (nearestSrcY * width + nearestSrcX) * 4; + const destIdx = (destRowOffset + destX) * 4; + pixels[destIdx] = spinBuffer[srcIdx]; + pixels[destIdx + 1] = spinBuffer[srcIdx + 1]; + pixels[destIdx + 2] = spinBuffer[srcIdx + 2]; + pixels[destIdx + 3] = spinBuffer[srcIdx + 3]; + } + } + const spinEnd = performance.now(); + graphPerf2.track("spin", spinEnd - spinStart); +} +function zoom(level = 1, anchorX = 0.5, anchorY = 0.5) { + if (level === 1) return; + const zoomStartTime = performance.now(); + let actualZoomLevel; + if (Math.abs(level - 1) >= 0.1) { + actualZoomLevel = level; + zoomAccumulator = 0; + } else { + const zoomDelta = level - 1; + zoomAccumulator += zoomDelta; + const threshold = 0.05; + if (Math.abs(zoomAccumulator) < threshold) return; + const zoomToApply = Math.sign(zoomAccumulator) * threshold; + actualZoomLevel = 1 + zoomToApply; + zoomAccumulator -= zoomToApply; + } + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("zoom") && gpuSpinModule?.gpuZoom && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x, + y: activeMask.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuZoom(pixels, width, height, actualZoomLevel, anchorX, anchorY, mask2); + if (success) { + gpuOk("zoom"); + const zoomEndTime = performance.now(); + graphPerf2.track("zoom", zoomEndTime - zoomStartTime); + return; + } + gpuFailed("zoom"); + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x; + minY = activeMask.y; + maxX = activeMask.x + activeMask.width; + maxY = activeMask.y + activeMask.height; + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const anchorPixelX = minX + workingWidth * anchorX; + const anchorPixelY = minY + workingHeight * anchorY; + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in zoom, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const bufferSize = width * height * 4; + if (!zoomBuffer || zoomBuffer.length !== bufferSize) { + zoomBuffer = new Uint8ClampedArray(bufferSize); + } + if (pixels.length !== bufferSize) { + console.error(`\u{1F6A8} ZOOM ERROR: Buffer size mismatch - pixels=${pixels.length}, expected=${bufferSize} (${width}x${height})`); + const copySize = Math.min(pixels.length, bufferSize); + zoomBuffer.fill(0); + if (copySize > 0) { + zoomBuffer.set(pixels.subarray(0, copySize)); + console.warn(`\u{1F6A8} ZOOM RECOVERY: Partial copy of ${copySize} bytes to prevent crash`); + } + } else { + zoomBuffer.set(pixels); + } + const scale7 = actualZoomLevel; + const invScale = 1 / scale7; + const invWorkingWidth = 1 / workingWidth; + const pixelCount = (maxY - minY) * (maxX - minX); + const maxPixels = Math.ceil(pixelCount / 4) * 4; + if (!zoom.coordBuffers || zoom.coordBuffers.length < maxPixels * 4) { + zoom.srcCoords = new Float32Array(maxPixels * 2); + zoom.destIndices = new Uint32Array(maxPixels); + } + const srcCoords = zoom.srcCoords; + const destIndices = zoom.destIndices; + let coordIndex = 0; + for (let destY = minY; destY < maxY; destY++) { + const destRowOffset = destY * width; + const srcYBase = (destY - anchorPixelY) * invScale + anchorPixelY; + let wrappedSrcYBase = ((srcYBase - minY) % workingHeight + workingHeight) % workingHeight + minY; + const nearestY = Math.round(wrappedSrcYBase); + const finalSrcY = minY + ((nearestY - minY) % workingHeight + workingHeight) % workingHeight; + if (finalSrcY >= minY && finalSrcY < maxY) { + const srcRowOffset = finalSrcY * width; + for (let destX = minX; destX < maxX; destX++) { + const srcX = (destX - anchorPixelX) * invScale + anchorPixelX; + srcCoords[coordIndex * 2] = srcX; + srcCoords[coordIndex * 2 + 1] = finalSrcY; + destIndices[coordIndex] = (destRowOffset + destX) * 4; + coordIndex++; + } + } + } + const groupsOf4 = Math.floor(coordIndex / 4); + for (let group = 0; group < groupsOf4; group++) { + const baseIndex = group * 4; + const srcX0 = srcCoords[baseIndex * 2]; + const srcX1 = srcCoords[(baseIndex + 1) * 2]; + const srcX2 = srcCoords[(baseIndex + 2) * 2]; + const srcX3 = srcCoords[(baseIndex + 3) * 2]; + const srcY = srcCoords[baseIndex * 2 + 1]; + const srcRowOffset = srcY * width; + const normalizedX0 = (srcX0 - minX) * invWorkingWidth; + const normalizedX1 = (srcX1 - minX) * invWorkingWidth; + const normalizedX2 = (srcX2 - minX) * invWorkingWidth; + const normalizedX3 = (srcX3 - minX) * invWorkingWidth; + const wrappedNormX0 = normalizedX0 - Math.floor(normalizedX0); + const wrappedNormX1 = normalizedX1 - Math.floor(normalizedX1); + const wrappedNormX2 = normalizedX2 - Math.floor(normalizedX2); + const wrappedNormX3 = normalizedX3 - Math.floor(normalizedX3); + const wrappedSrcX0 = minX + wrappedNormX0 * workingWidth; + const wrappedSrcX1 = minX + wrappedNormX1 * workingWidth; + const wrappedSrcX2 = minX + wrappedNormX2 * workingWidth; + const wrappedSrcX3 = minX + wrappedNormX3 * workingWidth; + const nearestX0 = Math.round(wrappedSrcX0); + const nearestX1 = Math.round(wrappedSrcX1); + const nearestX2 = Math.round(wrappedSrcX2); + const nearestX3 = Math.round(wrappedSrcX3); + const finalSrcX0 = minX + ((nearestX0 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX1 = minX + ((nearestX1 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX2 = minX + ((nearestX2 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX3 = minX + ((nearestX3 - minX) % workingWidth + workingWidth) % workingWidth; + if (finalSrcX0 >= minX && finalSrcX0 < maxX) { + processZoomPixel(srcRowOffset + finalSrcX0, destIndices[baseIndex]); + } + if (finalSrcX1 >= minX && finalSrcX1 < maxX) { + processZoomPixel(srcRowOffset + finalSrcX1, destIndices[baseIndex + 1]); + } + if (finalSrcX2 >= minX && finalSrcX2 < maxX) { + processZoomPixel(srcRowOffset + finalSrcX2, destIndices[baseIndex + 2]); + } + if (finalSrcX3 >= minX && finalSrcX3 < maxX) { + processZoomPixel(srcRowOffset + finalSrcX3, destIndices[baseIndex + 3]); + } + } + for (let i2 = groupsOf4 * 4; i2 < coordIndex; i2++) { + const srcX = srcCoords[i2 * 2]; + const srcY = srcCoords[i2 * 2 + 1]; + const srcRowOffset = srcY * width; + const normalizedX = (srcX - minX) * invWorkingWidth; + const wrappedNormX = normalizedX - Math.floor(normalizedX); + const wrappedSrcX = minX + wrappedNormX * workingWidth; + const nearestX = Math.round(wrappedSrcX); + const finalSrcX = minX + ((nearestX - minX) % workingWidth + workingWidth) % workingWidth; + if (finalSrcX >= minX && finalSrcX < maxX) { + processZoomPixel(srcRowOffset + finalSrcX, destIndices[i2]); + } + } +} +function processZoomPixel(srcPixelIndex, destIdx) { + const srcIdx = srcPixelIndex * 4; + pixels[destIdx] = zoomBuffer[srcIdx]; + pixels[destIdx + 1] = zoomBuffer[srcIdx + 1]; + pixels[destIdx + 2] = zoomBuffer[srcIdx + 2]; + pixels[destIdx + 3] = zoomBuffer[srcIdx + 3]; +} +zoom.srcCoords = null; +zoom.destIndices = null; +function zoomSimd(actualZoomLevel, anchorX, anchorY) { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x; + minY = activeMask.y; + maxX = activeMask.x + activeMask.width; + maxY = activeMask.y + activeMask.height; + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const anchorPixelX = minX + workingWidth * anchorX; + const anchorPixelY = minY + workingHeight * anchorY; + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in zoomSimd, recreating"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const bufferSize = width * height * 4; + if (!zoomBuffer || zoomBuffer.length !== bufferSize) { + zoomBuffer = new Uint8ClampedArray(bufferSize); + } + zoomBuffer.set(pixels); + const scale7 = actualZoomLevel; + const invScale = 1 / scale7; + const pixelCount = (maxY - minY) * (maxX - minX); + const maxPixels = Math.ceil(pixelCount / 4) * 4; + if (!zoomSimd.coordBuffers || zoomSimd.coordBuffers.length < maxPixels * 4) { + zoomSimd.srcCoords = new Float32Array(maxPixels * 2); + zoomSimd.destIndices = new Uint32Array(maxPixels); + zoomSimd.srcIndices = new Uint32Array(maxPixels); + } + const srcCoords = zoomSimd.srcCoords; + const destIndices = zoomSimd.destIndices; + const srcIndices = zoomSimd.srcIndices; + const invWorkingWidth = 1 / workingWidth; + const invWorkingHeight = 1 / workingHeight; + let coordIndex = 0; + for (let destY = minY; destY < maxY; destY++) { + const destRowOffset = destY * width; + const srcYBase = (destY - anchorPixelY) * invScale + anchorPixelY; + let wrappedSrcYBase = ((srcYBase - minY) % workingHeight + workingHeight) % workingHeight + minY; + const nearestY = Math.round(wrappedSrcYBase); + const finalSrcY = minY + ((nearestY - minY) % workingHeight + workingHeight) % workingHeight; + if (finalSrcY >= minY && finalSrcY < maxY) { + const srcRowOffset = finalSrcY * width; + for (let destX = minX; destX < maxX; destX++) { + const srcX = (destX - anchorPixelX) * invScale + anchorPixelX; + srcCoords[coordIndex * 2] = srcX; + srcCoords[coordIndex * 2 + 1] = finalSrcY; + destIndices[coordIndex] = (destRowOffset + destX) * 4; + coordIndex++; + } + } + } + const groupsOf4 = Math.floor(coordIndex / 4); + const remainder = coordIndex % 4; + for (let group = 0; group < groupsOf4; group++) { + const baseIndex = group * 4; + const srcX0 = srcCoords[baseIndex * 2]; + const srcX1 = srcCoords[(baseIndex + 1) * 2]; + const srcX2 = srcCoords[(baseIndex + 2) * 2]; + const srcX3 = srcCoords[(baseIndex + 3) * 2]; + const srcY = srcCoords[baseIndex * 2 + 1]; + const srcRowOffset = srcY * width; + const normalizedX0 = (srcX0 - minX) * invWorkingWidth; + const normalizedX1 = (srcX1 - minX) * invWorkingWidth; + const normalizedX2 = (srcX2 - minX) * invWorkingWidth; + const normalizedX3 = (srcX3 - minX) * invWorkingWidth; + const wrappedNormX0 = normalizedX0 - Math.floor(normalizedX0); + const wrappedNormX1 = normalizedX1 - Math.floor(normalizedX1); + const wrappedNormX2 = normalizedX2 - Math.floor(normalizedX2); + const wrappedNormX3 = normalizedX3 - Math.floor(normalizedX3); + const wrappedSrcX0 = minX + wrappedNormX0 * workingWidth; + const wrappedSrcX1 = minX + wrappedNormX1 * workingWidth; + const wrappedSrcX2 = minX + wrappedNormX2 * workingWidth; + const wrappedSrcX3 = minX + wrappedNormX3 * workingWidth; + const nearestX0 = Math.round(wrappedSrcX0); + const nearestX1 = Math.round(wrappedSrcX1); + const nearestX2 = Math.round(wrappedSrcX2); + const nearestX3 = Math.round(wrappedSrcX3); + const finalSrcX0 = minX + ((nearestX0 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX1 = minX + ((nearestX1 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX2 = minX + ((nearestX2 - minX) % workingWidth + workingWidth) % workingWidth; + const finalSrcX3 = minX + ((nearestX3 - minX) % workingWidth + workingWidth) % workingWidth; + if (finalSrcX0 >= minX && finalSrcX0 < maxX) { + processZoomSimdPixel(srcRowOffset + finalSrcX0, destIndices[baseIndex]); + } + if (finalSrcX1 >= minX && finalSrcX1 < maxX) { + processZoomSimdPixel(srcRowOffset + finalSrcX1, destIndices[baseIndex + 1]); + } + if (finalSrcX2 >= minX && finalSrcX2 < maxX) { + processZoomSimdPixel(srcRowOffset + finalSrcX2, destIndices[baseIndex + 2]); + } + if (finalSrcX3 >= minX && finalSrcX3 < maxX) { + processZoomSimdPixel(srcRowOffset + finalSrcX3, destIndices[baseIndex + 3]); + } + } + for (let i2 = groupsOf4 * 4; i2 < coordIndex; i2++) { + const srcX = srcCoords[i2 * 2]; + const srcY = srcCoords[i2 * 2 + 1]; + const srcRowOffset = srcY * width; + const normalizedX = (srcX - minX) * invWorkingWidth; + const wrappedNormX = normalizedX - Math.floor(normalizedX); + const wrappedSrcX = minX + wrappedNormX * workingWidth; + const nearestX = Math.round(wrappedSrcX); + const finalSrcX = minX + ((nearestX - minX) % workingWidth + workingWidth) % workingWidth; + if (finalSrcX >= minX && finalSrcX < maxX) { + processZoomSimdPixel(srcRowOffset + finalSrcX, destIndices[i2]); + } + } +} +function processZoomSimdPixel(srcPixelIndex, destIdx) { + const srcIdx = srcPixelIndex * 4; + pixels[destIdx] = zoomBuffer[srcIdx]; + pixels[destIdx + 1] = zoomBuffer[srcIdx + 1]; + pixels[destIdx + 2] = zoomBuffer[srcIdx + 2]; + pixels[destIdx + 3] = zoomBuffer[srcIdx + 3]; +} +zoomSimd.srcCoords = null; +zoomSimd.destIndices = null; +zoomSimd.srcIndices = null; +function suck(strength = 1, centerX, centerY) { + if (strength === 0) return; + suckAccumulator += strength; + const threshold = 0.5; + if (Math.abs(suckAccumulator) < threshold) return; + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = activeMask.x; + minY = activeMask.y; + maxX = activeMask.x + activeMask.width; + maxY = activeMask.y + activeMask.height; + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + const centerPixelX = centerX !== void 0 ? centerX : minX + workingWidth * 0.5; + const centerPixelY = centerY !== void 0 ? centerY : minY + workingHeight * 0.5; + const displacementAmount = Math.abs(suckAccumulator); + const direction = suckAccumulator > 0 ? 1 : -1; + if (displacementAmount < 0.01) { + suckAccumulator = 0; + return; + } + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("suck") && gpuSpinModule?.gpuSuck && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x + panTranslation.x, + y: activeMask.y + panTranslation.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuSuck( + pixels, + width, + height, + displacementAmount, + direction, + centerPixelX, + centerPixelY, + mask2 + ); + if (success) { + gpuOk("suck"); + suckAccumulator = 0; + return; + } + gpuFailed("suck"); + } + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in suck, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const tempPixels = new Uint8ClampedArray(pixels); + for (let destY = minY; destY < maxY; destY++) { + const destRowOffset = destY * width; + for (let destX = minX; destX < maxX; destX++) { + const dx = destX - centerPixelX; + const dy = destY - centerPixelY; + const distance4 = Math.sqrt(dx * dx + dy * dy); + if (distance4 < 1) { + const destIdx2 = (destRowOffset + destX) * 4; + pixels[destIdx2] = tempPixels[destIdx2]; + pixels[destIdx2 + 1] = tempPixels[destIdx2 + 1]; + pixels[destIdx2 + 2] = tempPixels[destIdx2 + 2]; + pixels[destIdx2 + 3] = tempPixels[destIdx2 + 3]; + continue; + } + const srcDistance = distance4 + direction * displacementAmount; + const scale7 = srcDistance / distance4; + let srcX = centerPixelX + dx * scale7; + let srcY = centerPixelY + dy * scale7; + srcX = Math.max(minX, Math.min(maxX - 1, srcX)); + srcY = Math.max(minY, Math.min(maxY - 1, srcY)); + const srcX_floor = Math.floor(srcX); + const srcY_floor = Math.floor(srcY); + const wx = srcX - srcX_floor; + const wy = srcY - srcY_floor; + const x1 = Math.max(minX, Math.min(maxX - 1, srcX_floor)); + const y1 = Math.max(minY, Math.min(maxY - 1, srcY_floor)); + const x2 = Math.max(minX, Math.min(maxX - 1, srcX_floor + 1)); + const y2 = Math.max(minY, Math.min(maxY - 1, srcY_floor + 1)); + const idx1 = (y1 * width + x1) * 4; + const idx2 = (y1 * width + x2) * 4; + const idx3 = (y2 * width + x1) * 4; + const idx4 = (y2 * width + x2) * 4; + const destIdx = (destRowOffset + destX) * 4; + for (let c4 = 0; c4 < 4; c4++) { + const topInterp = tempPixels[idx1 + c4] * (1 - wx) + tempPixels[idx2 + c4] * wx; + const bottomInterp = tempPixels[idx3 + c4] * (1 - wx) + tempPixels[idx4 + c4] * wx; + pixels[destIdx + c4] = Math.round(topInterp * (1 - wy) + bottomInterp * wy); + } + } + } + suckAccumulator = 0; +} +var blurAccumulator = 0; +var blurTempBuffer = null; +var blurTempBufferSize = 0; +function cleanupBlurBuffers() { + blurTempBuffer = null; + blurTempBufferSize = 0; + blurAccumulator = 0; +} +function blur(strength = 1, quality = "medium") { + if (strength <= 0.1) return; + const blurStartTime = performance.now(); + const scaledStrength = strength / 3; + blurAccumulator += scaledStrength; + const threshold = 0.5; + if (Math.abs(blurAccumulator) < threshold) return; + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("blur") && gpuSpinModule?.gpuBlur && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x + panTranslation.x, + y: activeMask.y + panTranslation.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuBlur(pixels, width, height, Math.abs(blurAccumulator), mask2); + if (success) { + gpuOk("blur"); + blurAccumulator = 0; + const blurEndTime2 = performance.now(); + graphPerf2.track("blur", blurEndTime2 - blurStartTime); + return; + } + gpuFailed("blur"); + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, Math.min(width, activeMask.x + panTranslation.x)); + maxX = Math.max( + 0, + Math.min(width, activeMask.x + activeMask.width + panTranslation.x) + ); + minY = Math.max(0, Math.min(height, activeMask.y + panTranslation.y)); + maxY = Math.max( + 0, + Math.min(height, activeMask.y + activeMask.height + panTranslation.y) + ); + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + if (workingWidth <= 0 || workingHeight <= 0) { + blurAccumulator = 0; + return; + } + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in blur, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const blurRadius = Math.max(1, Math.floor(Math.abs(blurAccumulator))); + const kernelSize = Math.min(blurRadius * 2 + 1, 15); + const weights = generateGaussianWeights(kernelSize, quality); + const radius = Math.floor(kernelSize / 2); + try { + const requiredSize = pixels.length; + if (!blurTempBuffer || blurTempBufferSize !== requiredSize) { + blurTempBuffer = new Uint8ClampedArray(requiredSize); + blurTempBufferSize = requiredSize; + } + blurTempBuffer.fill(0); + applyHorizontalBlur(pixels, blurTempBuffer, weights, radius, minX, minY, maxX, maxY); + applyVerticalBlur(blurTempBuffer, pixels, weights, radius, minX, minY, maxX, maxY); + } catch (error) { + console.warn("\u{1F6A8} Blur operation failed:", error); + blurTempBuffer = null; + blurTempBufferSize = 0; + } + const blurEndTime = performance.now(); + const blurDuration = blurEndTime - blurStartTime; + blurAccumulator = 0; +} +function generateGaussianWeights(kernelSize, quality) { + const weights = new Array(kernelSize); + const radius = Math.floor(kernelSize / 2); + const sigma = radius / 3; + if (quality === "fast") { + const weight = 1 / kernelSize; + for (let i2 = 0; i2 < kernelSize; i2++) { + weights[i2] = weight; + } + } else { + let sum = 0; + for (let i2 = 0; i2 < kernelSize; i2++) { + const x = i2 - radius; + const weight = Math.exp(-(x * x) / (2 * sigma * sigma)); + weights[i2] = weight; + sum += weight; + } + for (let i2 = 0; i2 < kernelSize; i2++) { + weights[i2] /= sum; + } + } + return weights; +} +function applyHorizontalBlur(sourcePixels, destPixels, weights, radius, minX, minY, maxX, maxY) { + if (!sourcePixels || !destPixels || !weights) return; + if (radius < 0 || radius >= weights.length / 2) return; + for (let y = minY; y < maxY; y++) { + const rowOffset = y * width; + for (let x = minX; x < maxX; x++) { + const destIdx = (rowOffset + x) * 4; + if (destIdx < 0 || destIdx + 3 >= destPixels.length) continue; + let r2 = 0, g = 0, b2 = 0, a2 = 0; + for (let k = -radius; k <= radius; k++) { + const srcX = Math.max(minX, Math.min(maxX - 1, x + k)); + const srcIdx = (rowOffset + srcX) * 4; + if (srcIdx < 0 || srcIdx + 3 >= sourcePixels.length) continue; + const weightIdx = k + radius; + if (weightIdx < 0 || weightIdx >= weights.length) continue; + const weight = weights[weightIdx]; + r2 += sourcePixels[srcIdx] * weight; + g += sourcePixels[srcIdx + 1] * weight; + b2 += sourcePixels[srcIdx + 2] * weight; + a2 += sourcePixels[srcIdx + 3] * weight; + } + destPixels[destIdx] = Math.round(Math.max(0, Math.min(255, r2))); + destPixels[destIdx + 1] = Math.round(Math.max(0, Math.min(255, g))); + destPixels[destIdx + 2] = Math.round(Math.max(0, Math.min(255, b2))); + destPixels[destIdx + 3] = Math.round(Math.max(0, Math.min(255, a2))); + } + } +} +function applyVerticalBlur(sourcePixels, destPixels, weights, radius, minX, minY, maxX, maxY) { + if (!sourcePixels || !destPixels || !weights) return; + if (radius < 0 || radius >= weights.length / 2) return; + for (let y = minY; y < maxY; y++) { + const rowOffset = y * width; + for (let x = minX; x < maxX; x++) { + const destIdx = (rowOffset + x) * 4; + if (destIdx < 0 || destIdx + 3 >= destPixels.length) continue; + let r2 = 0, g = 0, b2 = 0, a2 = 0; + for (let k = -radius; k <= radius; k++) { + const srcY = Math.max(minY, Math.min(maxY - 1, y + k)); + const srcIdx = (srcY * width + x) * 4; + if (srcIdx < 0 || srcIdx + 3 >= sourcePixels.length) continue; + const weightIdx = k + radius; + if (weightIdx < 0 || weightIdx >= weights.length) continue; + const weight = weights[weightIdx]; + r2 += sourcePixels[srcIdx] * weight; + g += sourcePixels[srcIdx + 1] * weight; + b2 += sourcePixels[srcIdx + 2] * weight; + a2 += sourcePixels[srcIdx + 3] * weight; + } + destPixels[destIdx] = Math.round(Math.max(0, Math.min(255, r2))); + destPixels[destIdx + 1] = Math.round(Math.max(0, Math.min(255, g))); + destPixels[destIdx + 2] = Math.round(Math.max(0, Math.min(255, b2))); + destPixels[destIdx + 3] = Math.round(Math.max(0, Math.min(255, a2))); + } + } +} +function sharpen(strength = 1) { + if (strength <= 0) return; + const sharpenStartTime = performance.now(); + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("sharpen") && gpuSpinModule?.gpuSharpen && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x + panTranslation.x, + y: activeMask.y + panTranslation.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuSharpen(pixels, width, height, strength, mask2); + if (success) { + gpuOk("sharpen"); + const sharpenEndTime2 = performance.now(); + graphPerf2.track("sharpen", sharpenEndTime2 - sharpenStartTime); + return; + } + gpuFailed("sharpen"); + } + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + minX = Math.max(0, Math.min(width, activeMask.x + panTranslation.x)); + maxX = Math.max( + 0, + Math.min(width, activeMask.x + activeMask.width + panTranslation.x) + ); + minY = Math.max(0, Math.min(height, activeMask.y + panTranslation.y)); + maxY = Math.max( + 0, + Math.min(height, activeMask.y + activeMask.height + panTranslation.y) + ); + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + if (workingWidth <= 0 || workingHeight <= 0) return; + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in sharpen, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + try { + const tempBuffer = new Uint8ClampedArray(pixels.length); + tempBuffer.set(pixels); + const centerWeight = 1 + 4 * strength; + const edgeWeight = -strength; + for (let y = minY + 1; y < maxY - 1; y++) { + for (let x = minX + 1; x < maxX - 1; x++) { + const centerIdx = (y * width + x) * 4; + if (tempBuffer[centerIdx + 3] === 0) continue; + for (let channel2 = 0; channel2 < 3; channel2++) { + let sum = 0; + sum += tempBuffer[centerIdx + channel2] * centerWeight; + sum += tempBuffer[((y - 1) * width + x) * 4 + channel2] * edgeWeight; + sum += tempBuffer[((y + 1) * width + x) * 4 + channel2] * edgeWeight; + sum += tempBuffer[(y * width + (x - 1)) * 4 + channel2] * edgeWeight; + sum += tempBuffer[(y * width + (x + 1)) * 4 + channel2] * edgeWeight; + pixels[centerIdx + channel2] = Math.max(0, Math.min(255, Math.round(sum))); + } + } + } + } catch (error) { + console.warn("\u{1F6A8} Sharpen operation failed:", error); + } + const sharpenEndTime = performance.now(); + graphPerf2.track("sharpen", sharpenEndTime - sharpenStartTime); +} +function sort() { + let minX = 0, minY = 0, maxX = width, maxY = height; + if (activeMask) { + const maskX = activeMask.x + panTranslation.x; + const maskY = activeMask.y + panTranslation.y; + minX = Math.max(0, Math.floor(maskX)); + minY = Math.max(0, Math.floor(maskY)); + maxX = Math.min(width, Math.floor(maskX + activeMask.width)); + maxY = Math.min(height, Math.floor(maskY + activeMask.height)); + } + const pixelsToSort = []; + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const index = (y * width + x) * 4; + const r2 = pixels[index]; + const g = pixels[index + 1]; + const b2 = pixels[index + 2]; + const a2 = pixels[index + 3]; + const luminance = 0.299 * r2 + 0.587 * g + 0.114 * b2; + pixelsToSort.push({ + r: r2, + g, + b: b2, + a: a2, + luminance + }); + } + } + pixelsToSort.sort((a2, b2) => a2.luminance - b2.luminance); + let sortedIndex = 0; + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const index = (y * width + x) * 4; + const sortedPixel = pixelsToSort[sortedIndex]; + pixels[index] = sortedPixel.r; + pixels[index + 1] = sortedPixel.g; + pixels[index + 2] = sortedPixel.b; + pixels[index + 3] = sortedPixel.a; + sortedIndex++; + } + } +} +function copyRegion(x, y, w, h) { + x = Math.floor(x); + y = Math.floor(y); + w = Math.floor(w); + h = Math.floor(h); + x += panTranslation.x; + y += panTranslation.y; + x = Math.max(0, Math.min(x, width)); + y = Math.max(0, Math.min(y, height)); + w = Math.max(0, Math.min(w, width - x)); + h = Math.max(0, Math.min(h, height - y)); + if (w <= 0 || h <= 0) { + return null; + } + const imageData = new ImageData(w, h); + const buffer = { + pixels: imageData.data, + width: imageData.width, + height: imageData.height + }; + for (let srcY = 0; srcY < h; srcY++) { + for (let srcX = 0; srcX < w; srcX++) { + const srcIndex = ((y + srcY) * width + (x + srcX)) * 4; + const destIndex = (srcY * w + srcX) * 4; + if (srcIndex >= 0 && srcIndex < pixels.length - 3) { + buffer.pixels[destIndex] = pixels[srcIndex]; + buffer.pixels[destIndex + 1] = pixels[srcIndex + 1]; + buffer.pixels[destIndex + 2] = pixels[srcIndex + 2]; + buffer.pixels[destIndex + 3] = pixels[srcIndex + 3]; + } + } + } + return buffer; +} +var stolen; +function steal(x, y, width2, height2) { + stolen = copyRegion(x, y, width2, height2); + return stolen; +} +function putback(x, y, scale7 = 1) { + if (!stolen) return; + const result = paste(stolen, x, y, scale7); + return result; +} +function shear(shearX = 0, shearY = 0) { + if (shearX === 0 && shearY === 0) return; + shearAccumulatorX += shearX; + shearAccumulatorY += shearY; + const finalShearX = shearAccumulatorX; + const finalShearY = shearAccumulatorY; + shearAccumulatorX = 0; + shearAccumulatorY = 0; + if (gpuSpinEnabled && gpuSpinAvailable && gpuAllowed("shear") && gpuSpinModule?.gpuShear && pixels && width && height) { + const mask2 = activeMask ? { + x: activeMask.x + panTranslation.x, + y: activeMask.y + panTranslation.y, + width: activeMask.width, + height: activeMask.height + } : null; + const success = gpuSpinModule.gpuShear(pixels, width, height, finalShearX, finalShearY, mask2); + if (success) { + gpuOk("shear"); + return; + } + gpuFailed("shear"); + } + let minX = 0, maxX = width, minY = 0, maxY = height; + if (activeMask) { + minX = Math.max(0, Math.min(width, activeMask.x + panTranslation.x)); + maxX = Math.max( + 0, + Math.min(width, activeMask.x + activeMask.width + panTranslation.x) + ); + minY = Math.max(0, Math.min(height, activeMask.y + panTranslation.y)); + maxY = Math.max( + 0, + Math.min(height, activeMask.y + activeMask.height + panTranslation.y) + ); + } + const workingWidth = maxX - minX; + const workingHeight = maxY - minY; + if (workingWidth <= 0 || workingHeight <= 0) return; + if (pixels.buffer && pixels.buffer.detached) { + console.warn("\u{1F6A8} Pixels buffer detached in shear, recreating from screen dimensions"); + pixels = new Uint8ClampedArray(width * height * 4); + pixels.fill(0); + } + const tempPixels = new Uint8ClampedArray(pixels); + if (finalShearX !== 0) { + for (let y = 0; y < workingHeight; y++) { + const distFromCenter = y - workingHeight / 2; + const rowShift = Math.round(finalShearX * distFromCenter); + for (let x = 0; x < workingWidth; x++) { + let srcX = x - rowShift; + srcX = (srcX % workingWidth + workingWidth) % workingWidth; + const srcOffset = ((minY + y) * width + (minX + srcX)) * 4; + const destOffset = ((minY + y) * width + (minX + x)) * 4; + pixels[destOffset] = tempPixels[srcOffset]; + pixels[destOffset + 1] = tempPixels[srcOffset + 1]; + pixels[destOffset + 2] = tempPixels[srcOffset + 2]; + pixels[destOffset + 3] = tempPixels[srcOffset + 3]; + } + } + tempPixels.set(pixels); + } + if (finalShearY !== 0) { + for (let x = 0; x < workingWidth; x++) { + const distFromCenter = x - workingWidth / 2; + const colShift = Math.round(finalShearY * distFromCenter); + for (let y = 0; y < workingHeight; y++) { + let srcY = y - colShift; + srcY = (srcY % workingHeight + workingHeight) % workingHeight; + const srcOffset = ((minY + srcY) * width + (minX + x)) * 4; + const destOffset = ((minY + y) * width + (minX + x)) * 4; + pixels[destOffset] = tempPixels[srcOffset]; + pixels[destOffset + 1] = tempPixels[srcOffset + 1]; + pixels[destOffset + 2] = tempPixels[srcOffset + 2]; + pixels[destOffset + 3] = tempPixels[srcOffset + 3]; + } + } + } +} +var Camera = class { + type = "perspective"; + matrix; + #x = 0; + #rotX = 0; + #y = 0; + #rotY = 0; + #z = 0; + #rotZ = 0; + fov; + near = 0.01; + far = 5e3; + position = [0, 0, 0, 1]; + rotation = [0, 0, 0]; + scale = [1, 1, 1]; + // centerCached; // Saved after each call to `center()`. + perspectiveMatrix; + #transformMatrix; + // Takes x, y, z position and an optional scale (xyz) array. + constructor(fov = 80, { x, y, z, scale: scale7 } = { x: 0, y: 0, z: 0, scale: 1 }) { + this.fov = fov; + this.x = x; + this.y = y; + this.z = z; + if (scale7) this.scale = scale7; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + } + set rotX(n2) { + this.#rotX = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.rotation[0] = n2; + } + get rotX() { + return this.#rotX; + } + set rotY(n2) { + this.#rotY = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.rotation[1] = n2; + } + get rotY() { + return this.#rotY; + } + set rotZ(n2) { + this.#rotZ = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.rotation[2] = n2; + } + get rotZ() { + return this.#rotZ; + } + // Returns the rotation of the camera in radians. + get rot() { + return [this.#rotX, this.#rotY, this.#rotZ]; + } + set x(n2 = 0) { + this.#x = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.position[0] = n2; + } + get x() { + return this.#x; + } + set y(n2 = 0) { + this.#y = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.position[1] = n2; + } + get y() { + return this.#y; + } + set z(n2 = 0) { + this.#z = n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + this.position[2] = n2; + } + get z() { + return this.#z; + } + forward(n2) { + this.#z -= n2; + this.#perspective(this.fov); + this.#transform(); + this.matrix = this.#transformMatrix; + } + #perspective(fov) { + const zNear = this.near; + const zFar = this.far; + this.perspectiveMatrix = perspective( + create5(), + radians(fov), + width / height, + zNear, + zFar + ); + const zRange = zNear - zFar; + const ten = (-zNear - zFar) / zRange; + const fourteen = 2 * zFar * zNear / zRange; + this.perspectiveMatrix[10] = ten; + this.perspectiveMatrix[14] = fourteen; + this.perspectiveMatrix[11] = 1; + } + get perspective() { + return this.perspectiveMatrix; + } + // Recalculate the camera matrix for a new display constraint. + // TODO: Eventually this should be redundant for custom cameras + // that don't hook into the `screen`. 24.02.21.15.26 + resize() { + this.forward(0); + } + // Get an XYZ position on a plane at a given depth, + // relative to screen coordinates. + ray(X2 = width / 2, Y2 = height / 2, depth = 1, flippedY = false) { + this.#perspective(this.fov); + const pos = [...this.position]; + if (flippedY) pos[1] *= -1; + const rotX = fromXRotation(create5(), radians(this.#rotX)); + const rotY = fromYRotation(create5(), radians(this.#rotY)); + const rotZ = fromZRotation(create5(), radians(this.#rotZ)); + const rotatedX = multiply5(create5(), rotX, create5()); + const rotatedY = multiply5(create5(), rotY, rotatedX); + const rotatedZ = multiply5(create5(), rotatedY, rotZ); + const scaled = scale5(create5(), rotatedZ, this.scale); + const world2 = scaled; + const invertedProjection = invert3( + create5(), + this.perspectiveMatrix + ); + const invWorldPersProj = mul5(create5(), world2, invertedProjection); + X2 = 1 - X2 / width; + Y2 = 1 - Y2 / height; + let x = 2 * X2 - 1; + let y = 2 * Y2 - 1; + const screenPos = fromValues3(x, -y, 1, 1); + const shiftedScreenPos = scale3(create3(), screenPos, depth); + const xyz = transformMat42( + create3(), + shiftedScreenPos, + invWorldPersProj + ); + const worldPos = sub3(create3(), pos, xyz); + return worldPos; + } + #transform() { + const panned = translate2(create5(), create5(), [ + this.#x, + this.#y, + this.#z + ]); + const rotY = fromYRotation(create5(), radians(-this.#rotY)); + const rotX = fromXRotation(create5(), radians(this.#rotX)); + const rotZ = fromZRotation(create5(), radians(this.#rotZ)); + const rotatedY = multiply5(create5(), rotY, panned); + const rotatedX = multiply5(create5(), rotX, rotatedY); + const rotatedZ = multiply5(create5(), rotZ, rotatedX); + const scaled = scale5(create5(), rotatedZ, this.scale); + this.#transformMatrix = multiply5( + create5(), + this.perspectiveMatrix, + scaled + ); + } +}; +var Dolly = class { + camera; + xVel = 0; + yVel = 0; + zVel = 0; + dec = 0.9; + constructor(camera) { + this.camera = camera; + } + sim() { + this.xVel *= this.dec; + this.yVel *= this.dec; + this.zVel *= this.dec; + if (abs3(this.xVel) > 0) this.camera.x += this.xVel; + if (abs3(this.yVel) > 0) this.camera.y += this.yVel; + if (abs3(this.zVel) > 0) this.camera.z += this.zVel; + } + push({ x, y, z }) { + const xz = rotate3( + create6(), + fromValues6(x, z), + fromValues6(0, 0), + radians(-this.camera.rotY) + // Take the camera Y axis for strafing. + ); + this.xVel += xz[0] || 0; + this.yVel += y || 0; + this.zVel += xz[1] || 0; + } +}; +var formId = 0; +var showWireframes = false; +var showBoundingBoxes = false; +var wireframeLines = []; +var boundingBoxes = []; +var renderStats = { + originalTriangles: 0, + clippedTriangles: 0, + subdividedTriangles: 0, + wireframeSegmentsTotal: 0, + wireframeSegmentsTextured: 0, + wireframeSegmentsGradient: 0, + wireframeSegmentsClipped: 0, + wireframeSegmentsOther: 0, + pixelsDrawn: 0, + trianglesRejected: 0 +}; +function setShowClippedWireframes(enabled) { + showWireframes = enabled; +} +function clearWireframeBuffer() { + wireframeLines = []; + boundingBoxes = []; + renderStats = { + originalTriangles: 0, + clippedTriangles: 0, + subdividedTriangles: 0, + wireframeSegmentsTotal: 0, + wireframeSegmentsTextured: 0, + wireframeSegmentsGradient: 0, + wireframeSegmentsClipped: 0, + wireframeSegmentsOther: 0, + pixelsDrawn: 0, + trianglesRejected: 0 + }; +} +function getRenderStats() { + return renderStats; +} +function clipLineToScreen(x1, y1, x2, y2) { + const INSIDE = 0; + const LEFT = 1; + const RIGHT = 2; + const BOTTOM = 4; + const TOP = 8; + function computeOutCode(x, y) { + let code2 = INSIDE; + if (x < 0) code2 |= LEFT; + else if (x >= width) code2 |= RIGHT; + if (y < 0) code2 |= TOP; + else if (y >= height) code2 |= BOTTOM; + return code2; + } + let outcode1 = computeOutCode(x1, y1); + let outcode2 = computeOutCode(x2, y2); + let accept = false; + while (true) { + if (!(outcode1 | outcode2)) { + accept = true; + break; + } else if (outcode1 & outcode2) { + break; + } else { + let x, y; + const outcodeOut = outcode1 ? outcode1 : outcode2; + if (outcodeOut & TOP) { + x = x1 + (x2 - x1) * (0 - y1) / (y2 - y1); + y = 0; + } else if (outcodeOut & BOTTOM) { + x = x1 + (x2 - x1) * (height - 1 - y1) / (y2 - y1); + y = height - 1; + } else if (outcodeOut & RIGHT) { + y = y1 + (y2 - y1) * (width - 1 - x1) / (x2 - x1); + x = width - 1; + } else if (outcodeOut & LEFT) { + y = y1 + (y2 - y1) * (0 - x1) / (x2 - x1); + x = 0; + } + if (outcodeOut === outcode1) { + x1 = x; + y1 = y; + outcode1 = computeOutCode(x1, y1); + } else { + x2 = x; + y2 = y; + outcode2 = computeOutCode(x2, y2); + } + } + } + if (accept) { + return { x1, y1, x2, y2 }; + } else { + return null; + } +} +function addWireframeLine(x1, y1, x2, y2, color3, category = "other") { + if (!isFinite(x1) || !isFinite(y1) || !isFinite(x2) || !isFinite(y2)) { + return; + } + const dx = x2 - x1; + const dy = y2 - y1; + if (dx === 0 && dy === 0) { + return; + } + const minX = -width * 0.1; + const maxX = width * 1.1; + const minY = -height * 0.1; + const maxY = height * 1.1; + const p1Valid = x1 >= minX && x1 <= maxX && y1 >= minY && y1 <= maxY; + const p2Valid = x2 >= minX && x2 <= maxX && y2 >= minY && y2 <= maxY; + if (!p1Valid || !p2Valid) { + return; + } + wireframeLines.push({ x1, y1, x2, y2, color: color3, category }); + renderStats.wireframeSegmentsTotal++; + switch (category) { + case "textured": + renderStats.wireframeSegmentsTextured++; + break; + case "gradient": + renderStats.wireframeSegmentsGradient++; + break; + case "clipped": + renderStats.wireframeSegmentsClipped++; + break; + default: + renderStats.wireframeSegmentsOther++; + break; + } +} +function drawBufferedWireframes() { + if (wireframeLines.length > 0) { + const savedColor = [c[0], c[1], c[2], c[3]]; + withForceReplaceMode(() => { + for (const wf of wireframeLines) { + const margin = max5(width, height) * 0.5; + const p1OutOfBounds = abs3(wf.x1) > width + margin || abs3(wf.y1) > height + margin || wf.x1 < -margin || wf.y1 < -margin; + const p2OutOfBounds = abs3(wf.x2) > width + margin || abs3(wf.y2) > height + margin || wf.x2 < -margin || wf.y2 < -margin; + if (p1OutOfBounds || p2OutOfBounds) { + continue; + } + const clipped = clipLineToScreen(wf.x1, wf.y1, wf.x2, wf.y2); + if (!clipped) continue; + setColor(wf.color[0], wf.color[1], wf.color[2], wf.color[3]); + line(clipped.x1, clipped.y1, clipped.x2, clipped.y2); + } + }); + setColor(...savedColor); + wireframeLines = []; + } +} +var signGlyphCache = {}; +function sign(text, options = {}) { + const { + scale: scale7 = 0.1, + color: color3 = [1, 1, 1, 1], + align = "left", + glyphs = {} + // Pass in typeface.glyphs from the piece + } = options; + const positions = []; + const colors = []; + const [r2, g, b2, a2 = 1] = Array.isArray(color3) ? color3.map((c4) => c4 > 1 ? c4 / 255 : c4) : [1, 1, 1, 1]; + const lineColor = [r2, g, b2, a2]; + let cursorX = 0; + let totalWidth = 0; + const charData = []; + for (const char of text) { + const charCode = char.charCodeAt(0); + let glyph = glyphs[char] || glyphs[charCode] || signGlyphCache[charCode]; + if (char === " ") { + charData.push({ char, glyph: null, advance: 3 * scale7 }); + totalWidth += 3 * scale7; + continue; + } + if (glyph) { + const advance = (glyph.advance || glyph.dwidth?.x || 4) * scale7; + charData.push({ char, glyph, advance }); + totalWidth += advance; + } else { + charData.push({ char, glyph: null, advance: 4 * scale7, missing: true }); + totalWidth += 4 * scale7; + } + } + if (align === "center") { + cursorX = -totalWidth / 2; + } else if (align === "right") { + cursorX = -totalWidth; + } + for (const { char, glyph, advance, missing } of charData) { + if (char === " ") { + cursorX += advance; + continue; + } + if (missing) { + const s2 = 3 * scale7; + const h = 6 * scale7; + positions.push( + [cursorX, 0, 0, 1], + [cursorX + s2, 0, 0, 1], + [cursorX + s2, 0, 0, 1], + [cursorX + s2, -h, 0, 1], + [cursorX + s2, -h, 0, 1], + [cursorX, -h, 0, 1], + [cursorX, -h, 0, 1], + [cursorX, 0, 0, 1] + ); + colors.push(lineColor, lineColor, lineColor, lineColor, lineColor, lineColor, lineColor, lineColor); + cursorX += advance; + continue; + } + const offsetX = (glyph.offset?.[0] || 0) * scale7; + const offsetY = (glyph.offset?.[1] || 0) * scale7; + if (glyph.commands) { + for (const cmd of glyph.commands) { + if (cmd.name === "line") { + const [x1, y1, x2, y2] = cmd.args; + positions.push( + [cursorX + offsetX + x1 * scale7, -(offsetY + y1 * scale7), 0, 1], + [cursorX + offsetX + x2 * scale7, -(offsetY + y2 * scale7), 0, 1] + ); + colors.push(lineColor, lineColor); + } else if (cmd.name === "point") { + const [x, y] = cmd.args; + const px = cursorX + offsetX + x * scale7; + const py = -(offsetY + y * scale7); + const ps = scale7 * 0.3; + positions.push( + [px - ps, py, 0, 1], + [px + ps, py, 0, 1], + [px, py - ps, 0, 1], + [px, py + ps, 0, 1] + ); + colors.push(lineColor, lineColor, lineColor, lineColor); + } + } + } else if (glyph.pixels) { + const [charWidth, charHeight] = glyph.resolution || [4, 8]; + for (let row = 0; row < glyph.pixels.length; row++) { + const pixelRow = glyph.pixels[row]; + if (!Array.isArray(pixelRow)) continue; + for (let col = 0; col < pixelRow.length; col++) { + if (pixelRow[col] === 1) { + const px = cursorX + offsetX + col * scale7; + const py = -(offsetY + row * scale7); + const ps = scale7 * 0.25; + positions.push( + [px, py - ps, 0, 1], + [px, py + ps, 0, 1] + ); + colors.push(lineColor, lineColor); + } + } + } + } + cursorX += advance; + } + if (positions.length === 0) { + positions.push([0, 0, 0, 1], [0, 0, 0, 1]); + colors.push([0, 0, 0, 0], [0, 0, 0, 0]); + } + return new Form( + { type: "line", positions, colors }, + { pos: [0, 0, 0], scale: 1 } + ); +} +function cacheSignGlyph(charCode, glyphData) { + signGlyphCache[charCode] = glyphData; +} +var Form = class { + primitive = "triangle"; + type = "triangle"; + limiter = 0; + // Only enabled on CPU rendered `line` at the moment. 22.11.06.18.19 + uid; + // = nanoid(4); // An id to keep across threads. Takes ~4 milliseconds. 😢 + tag; + // Gets sent to the GPU as a named / marked tag. + // Currently only available on `buffered` types in `3d.mjs` 23.02.07.09.46 + // Model + vertices = []; + indices = []; + // TODO: Texture and color should be optional, and perhaps based on type. + // TODO: Should this use a parameter called shader? + texture; + // = makeBuffer(32, 32); + color; + colorModifier; + noFade = false; + // Set to true to disable near-plane alpha fading + // GPU Specific Params & Buffers + gpuVerticesSent = 0; + gpuReset = false; + // Assumes this object is being recreated on the GPU. + gpuKeep = true; + gpuConvertColors = true; + gpuTransformed = false; + gpuRecolored = false; + MAX_POINTS = 1e5; + // Some buffered geometry gpu calls may use this hint. + uvs = []; + #gradientColors = [ + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 0, 1, 1] + ]; + /* I haven't needed support for this yet so it's left commented. 22.10.13.23.12 + #texCoords = [ + [0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0], + ]; + */ + // Transform + position = [0, 0, 0]; + rotation = [0, 0, 0]; + scale = [1, 1, 1]; + gradients = true; + // Blending + alpha = 1; + constructor({ + type, + vertices, + uvs = [], + positions, + colors, + texCoords, + gradients, + indices, + keep = true + }, fill, transform) { + this.gradients = gradients; + this.uid = formId; + formId += 1; + this.primitive = type; + this.type = type; + this.gpuKeep = keep; + if (type === "quad") this.primitive = "triangle"; + if (type === "triangle:buffered") this.primitive = "triangle"; + if (type === "line:buffered") this.primitive = "line"; + this.indices = indices || repeat(positions?.length, (i2) => i2); + if (fill?.pos || fill?.rot || fill?.scale) { + transform = fill; + fill = void 0; + } + if (fill?.tex) this.texture = fill.tex; + if (fill?.color) this.color = fill.color || c.slice(); + if (fill?.alpha) this.alpha = fill.alpha; + if (positions?.length > 0) + this.addPoints({ positions, colors, texCoords }, this.indices); + if (vertices?.length > 0) { + this.vertices = vertices; + this.uvs = uvs; + } + this.position = transform?.pos || [0, 0, 0]; + this.rotation = transform?.rot || [0, 0, 0]; + if (typeof transform.scale === "number") { + this.scale = [transform.scale, transform.scale, transform.scale]; + } else { + this.scale = transform?.scale || [1, 1, 1]; + } + } + // TODO: This needs to support color (and eventually N vertex attributes). + resetUID() { + this.uid = nanoid(4); + } + // Clears vertex and index attributes to prepare for replacement geometry. + clear() { + this.uvs = []; + this.vertices = []; + this.indices = []; + this.gpuReset = true; + this.gpuVerticesSent = 0; + } + // How close we are to being beyond the max points allotted by the GPU for + // buffer geometries. + maxProgress() { + return this.vertices.length / (this.MAX_POINTS + 1); + } + addPoints(attributes, indices) { + const incomingLength = attributes.positions.length; + const verticesLength = this.vertices.length; + const pointsAvailable = this.MAX_POINTS - verticesLength; + let end = incomingLength; + let maxedOut = false; + if (pointsAvailable < incomingLength) { + end = pointsAvailable; + maxedOut = true; + if (debug) + console.warn( + "Max. cutoff in GPU form!", + this, + incomingLength, + pointsAvailable + ); + } + for (let i2 = 0; i2 < end; i2++) { + const texCoord = attributes.texCoords?.[i2] || [ + attributes.positions[i2][X] / 2 + 0.5, + attributes.positions[i2][Y] / 2 + 0.5 + ]; + this.uvs.push(...texCoord); + if (attributes.colors?.[i2] && typeof this.colorModifier === "function") { + attributes.colors[i2] = this.colorModifier(attributes.colors[i2]); + } + this.vertices.push( + // For sending to the CPU. + new Vertex( + attributes.positions[i2], + attributes.colors?.[i2], + // this.#gradientColors[i % 3], + texCoord, + //this.#texCoords[i % 3] // Replace to enable bespoke texture coordinates. + attributes.normals?.[i2] + ) + ); + if (!indices) this.indices.push(verticesLength + i2); + } + if (indices) this.indices = indices; + return maxedOut; + } + // Get the world position of this form's local vertex. + transformVertex(vertex) { + const panned = fromTranslation2(create5(), [ + this.position[X], + this.position[Y], + this.position[Z] + ]); + const rotX = fromXRotation(create5(), radians(this.rotation[X])); + const rotY = fromYRotation(create5(), radians(this.rotation[Y])); + const rotZ = fromZRotation(create5(), radians(this.rotation[Z])); + const rotatedX = mul5(create5(), panned, rotX); + const rotatedY = mul5(create5(), rotatedX, rotY); + const rotatedZ = mul5(create5(), rotatedY, rotZ); + const matrix = rotatedZ; + scale5(matrix, matrix, this.scale); + return vertex.transformWorld(matrix); + } + graph({ matrix: cameraMatrix }) { + const scaled = scale5(create5(), create5(), this.scale); + const rotX = fromXRotation(create5(), radians(this.rotation[X])); + const rotY = fromYRotation(create5(), radians(this.rotation[Y])); + const rotZ = fromZRotation(create5(), radians(this.rotation[Z])); + const rotatedX = multiply5(create5(), rotX, scaled); + const rotatedY = multiply5(create5(), rotY, rotatedX); + const rotatedZ = multiply5(create5(), rotZ, rotatedY); + const panned = fromTranslation2(create5(), [ + this.position[X] * -1, + this.position[Y], + this.position[Z] * -1 + ]); + const transformed = multiply5(create5(), panned, rotatedZ); + if (this.type === "line" && this.vertices.length > 0) { + const fullMatrix = multiply5(create5(), cameraMatrix, transformed); + withForceReplaceMode(() => { + const savedColor = [c[0], c[1], c[2], c[3]]; + const baseColor = this.color; + for (let i2 = 0; i2 < this.vertices.length; i2 += 2) { + if (i2 + 1 < this.vertices.length) { + const a2 = this.vertices[i2]; + const b2 = this.vertices[i2 + 1]; + const transformedA = a2.transform(fullMatrix); + const transformedB = b2.transform(fullMatrix); + const clippedLine = clipLineToFrustum(transformedA, transformedB); + if (clippedLine.length < 2) continue; + const [lineA, lineB] = clippedLine; + const screenA = toScreenSpace(perspectiveDivide(lineA)); + const screenB = toScreenSpace(perspectiveDivide(lineB)); + if (!Number.isFinite(screenA.pos[0]) || !Number.isFinite(screenA.pos[1]) || !Number.isFinite(screenB.pos[0]) || !Number.isFinite(screenB.pos[1])) { + continue; + } + const clipped = clipLineToScreen( + screenA.pos[0], + screenA.pos[1], + screenB.pos[0], + screenB.pos[1] + ); + if (!clipped) continue; + if (baseColor && baseColor.length >= 3) { + setColor( + baseColor[0], + baseColor[1], + baseColor[2], + baseColor[3] ?? 255 + ); + } else if (a2.color && b2.color) { + const avg = [ + floor7((a2.color[0] + b2.color[0]) / 2 * 255), + floor7((a2.color[1] + b2.color[1]) / 2 * 255), + floor7((a2.color[2] + b2.color[2]) / 2 * 255), + floor7((a2.color[3] + b2.color[3]) / 2 * 255) + ]; + setColor(...avg); + } + line(clipped.x1, clipped.y1, clipped.x2, clipped.y2); + } + } + setColor(...savedColor); + }); + } + if (this.type === "triangle" && this.vertices.length >= 3) { + const fullMatrix = multiply5(create5(), cameraMatrix, transformed); + for (let i2 = 0; i2 < this.vertices.length; i2 += 3) { + if (i2 + 2 < this.vertices.length) { + const v0 = this.vertices[i2]; + const v1 = this.vertices[i2 + 1]; + const v2 = this.vertices[i2 + 2]; + const t0 = v0.transform(fullMatrix); + const t1 = v1.transform(fullMatrix); + const t2 = v2.transform(fullMatrix); + renderStats.originalTriangles++; + let clippedVertices = [t0, t1, t2]; + clippedVertices = clipInClipSpace(clippedVertices, ["near", "left", "right", "bottom", "top"]); + if (clippedVertices.length < 3) { + continue; + } + let anyWTooSmall = false; + for (let vi = 0; vi < clippedVertices.length; vi++) { + if (clippedVertices[vi].pos[W] < MIN_PERSPECTIVE_W) { + anyWTooSmall = true; + break; + } + } + if (anyWTooSmall) continue; + if (clippedVertices.length >= 3) { + renderStats.clippedTriangles++; + } + const nearPlane = NEAR_CLIP_Z; + const fadePlane = 0.5; + const minZ = min5(...clippedVertices.map((v3) => v3.pos[2])); + let alphaMultiplier = 1; + if (!this.noFade && minZ < fadePlane) { + alphaMultiplier = (minZ - nearPlane) / (fadePlane - nearPlane); + alphaMultiplier = max5(0, min5(1, alphaMultiplier)); + } + const screenVertices = clippedVertices.map((vertex) => { + const persp = perspectiveDivide(vertex); + return toScreenSpace(persp); + }); + const hasInvalidVertex = screenVertices.some( + (v3) => !Number.isFinite(v3.pos[0]) || !Number.isFinite(v3.pos[1]) + ); + if (hasInvalidVertex) continue; + const finalVertices = clipToScreen(screenVertices); + if (finalVertices.length < 3) continue; + for (let j = 1; j < finalVertices.length - 1; j++) { + const s0 = finalVertices[0]; + const s1 = finalVertices[j]; + const s2 = finalVertices[j + 1]; + const x0 = s0.pos[0], y0 = s0.pos[1]; + const x1 = s1.pos[0], y1 = s1.pos[1]; + const x2 = s2.pos[0], y2 = s2.pos[1]; + if (this.texture) { + const subdivided = subdivideTriangleIfNeeded( + x0, + y0, + s0.texCoords, + s0.pos[2], + s0.pos[3], + // x, y, uv, z, w + x1, + y1, + s1.texCoords, + s1.pos[2], + s1.pos[3], + x2, + y2, + s2.texCoords, + s2.pos[2], + s2.pos[3], + 300 + // Subdivide triangles larger than 300px to reduce overdraw + ); + if (subdivided.length > 1) { + renderStats.subdividedTriangles += subdivided.length; + } + for (const tri2 of subdivided) { + drawTexturedTriangle( + tri2[0], + tri2[1], + tri2[2], + tri2[3], + tri2[4], + // x0, y0, uv0, z0, w0 + tri2[5], + tri2[6], + tri2[7], + tri2[8], + tri2[9], + // x1, y1, uv1, z1, w1 + tri2[10], + tri2[11], + tri2[12], + tri2[13], + tri2[14], + // x2, y2, uv2, z2, w2 + this.texture, + alphaMultiplier + ); + if (showWireframes) { + const w0 = tri2[4]; + const w1 = tri2[9]; + const w2 = tri2[14]; + if (w0 > 0.01 && w1 > 0.01 && w2 > 0.01) { + const wireColor = subdivided.length > 1 ? [255, 255, 0, 255] : [255, 255, 255, 255]; + addWireframeLine(tri2[0], tri2[1], tri2[5], tri2[6], wireColor, "textured"); + addWireframeLine(tri2[5], tri2[6], tri2[10], tri2[11], wireColor, "textured"); + addWireframeLine(tri2[10], tri2[11], tri2[0], tri2[1], wireColor, "textured"); + } + } + } + } else { + const color0 = [ + floor7(s0.color[0] * 255), + floor7(s0.color[1] * 255), + floor7(s0.color[2] * 255), + floor7(s0.color[3] * 255 * alphaMultiplier) + ]; + const color1 = [ + floor7(s1.color[0] * 255), + floor7(s1.color[1] * 255), + floor7(s1.color[2] * 255), + floor7(s1.color[3] * 255 * alphaMultiplier) + ]; + const color22 = [ + floor7(s2.color[0] * 255), + floor7(s2.color[1] * 255), + floor7(s2.color[2] * 255), + floor7(s2.color[3] * 255 * alphaMultiplier) + ]; + drawGradientTriangle( + x0, + y0, + color0, + s0.pos[2], + // x, y, color, z + x1, + y1, + color1, + s1.pos[2], + x2, + y2, + color22, + s2.pos[2] + ); + if (showWireframes) { + const w0 = s0.pos[3]; + const w1 = s1.pos[3]; + const w2 = s2.pos[3]; + if (w0 > 0.01 && w1 > 0.01 && w2 > 0.01) { + const green = [0, 255, 0, 255]; + addWireframeLine(x0, y0, x1, y1, green, "gradient"); + addWireframeLine(x1, y1, x2, y2, green, "gradient"); + addWireframeLine(x2, y2, x0, y0, green, "gradient"); + } + } + } + } + if (showWireframes && finalVertices.length > 3) { + const red = [255, 0, 0, 255]; + for (let k = 0; k < finalVertices.length; k++) { + const vA = finalVertices[k]; + const vB = finalVertices[(k + 1) % finalVertices.length]; + if (vA.pos[3] > 0.01 && vB.pos[3] > 0.01) { + addWireframeLine(vA.pos[0], vA.pos[1], vB.pos[0], vB.pos[1], red, "clipped"); + } + } + } + } + } + } + const transformedVertices = []; + const matrix = multiply5(create5(), cameraMatrix, transformed); + this.vertices.forEach((vertex) => { + transformedVertices.push(vertex.transform(matrix)); + }); + return transformedVertices; + } +}; +var X = 0; +var Y = 1; +var Z = 2; +var W = 3; +var Vertex = class _Vertex { + static X = 0; + static Y = 1; + static Z = 2; + static W = 3; + pos; + color; + texCoords; + normal; + constructor(position = [0, 0, 0, 1], color3 = [1, 1, 1, 1], textureCoordinates = [0, 0], normal = null) { + this.pos = position; + this.color = color3; + this.texCoords = textureCoordinates; + if (normal !== null) { + this.normal = fromValues2(...normal); + } else { + this.normal = fromValues2(0, 0, 1); + } + } + // TODO: Optimize this function for large vertex counts. 22.10.13.00.14 + transform(matrix) { + const vert = new _Vertex( + transformMat42( + create3(), + [ + this.pos[X] * -1, + // FLIPPED + this.pos[Y], + this.pos[Z] * -1, + // FLIPPED + this.pos[W] + ], + matrix + ), + this.color, + this.texCoords + ); + return vert; + } + transformWorld(matrix) { + return new _Vertex( + transformMat42(create3(), this.pos, matrix), + this.color, + this.texCoords + ); + } +}; +var NEAR_CLIP_Z = 0.02; +var MIN_PERSPECTIVE_W = 1e-3; +function clipInClipSpace(vertices, clippingBoundary) { + let clipped = []; + function inside(vertex, edge) { + const p = vertex.pos; + const w = p[W]; + switch (edge) { + case "left": + return p[X] >= -w; + case "right": + return p[X] <= w; + case "bottom": + return p[Y] >= -w; + case "top": + return p[Y] <= w; + case "near": + return p[Z] >= NEAR_CLIP_Z; + case "far": + return p[Z] <= w; + } + } + function computeIntersection(v1, v2, edge) { + const p1 = v1.pos; + const p22 = v2.pos; + let t2; + switch (edge) { + case "left": + t2 = (-p1[W] - p1[X]) / (p22[X] - p1[X] + (p22[W] - p1[W])); + break; + case "right": + t2 = (p1[W] - p1[X]) / (p22[X] - p1[X] - (p22[W] - p1[W])); + break; + case "bottom": + t2 = (-p1[W] - p1[Y]) / (p22[Y] - p1[Y] + (p22[W] - p1[W])); + break; + case "top": + t2 = (p1[W] - p1[Y]) / (p22[Y] - p1[Y] - (p22[W] - p1[W])); + break; + case "near": + t2 = (NEAR_CLIP_Z - p1[Z]) / (p22[Z] - p1[Z]); + break; + case "far": + t2 = (p1[W] - p1[Z]) / (p22[Z] - p1[Z] - (p22[W] - p1[W])); + break; + } + if (!Number.isFinite(t2)) t2 = 0.5; + t2 = max5(0, min5(1, t2)); + const newPos = lerp2(create3(), p1, p22, t2); + const newColor = [ + v1.color[0] + (v2.color[0] - v1.color[0]) * t2, + v1.color[1] + (v2.color[1] - v1.color[1]) * t2, + v1.color[2] + (v2.color[2] - v1.color[2]) * t2, + v1.color[3] + (v2.color[3] - v1.color[3]) * t2 + ]; + const newTexCoords = [ + v1.texCoords[0] + (v2.texCoords[0] - v1.texCoords[0]) * t2, + v1.texCoords[1] + (v2.texCoords[1] - v1.texCoords[1]) * t2 + ]; + return new Vertex(newPos, newColor, newTexCoords); + } + for (const edge of clippingBoundary) { + const input3 = clipped.length > 0 ? clipped : vertices; + clipped = []; + if (input3.length === 0) break; + let prevVertex = input3[input3.length - 1]; + for (let i2 = 0; i2 < input3.length; i2++) { + const curVertex = input3[i2]; + if (inside(curVertex, edge)) { + if (!inside(prevVertex, edge)) { + const intersection = computeIntersection(prevVertex, curVertex, edge); + clipped.push(intersection); + } + clipped.push(curVertex); + } else if (inside(prevVertex, edge)) { + const intersection = computeIntersection(prevVertex, curVertex, edge); + clipped.push(intersection); + } + prevVertex = curVertex; + } + } + return clipped; +} +function clipLineToFrustum(v1, v2) { + const clippingBoundary = ["left", "right", "bottom", "top", "near", "far"]; + function inside(p, edge) { + switch (edge) { + case "left": + return p[X] >= -p[W]; + case "right": + return p[X] <= p[W]; + case "bottom": + return p[Y] >= -p[W]; + case "top": + return p[Y] <= p[W]; + case "near": + return p[Z] >= NEAR_CLIP_Z; + case "far": + return p[Z] <= p[W]; + } + } + function computeIntersection(p1, p22, edge) { + let t2; + switch (edge) { + case "left": + t2 = (-p1[W] - p1[X]) / (p22[X] - p1[X] + p22[W] - p1[W]); + break; + case "right": + t2 = (p1[W] - p1[X]) / (p22[X] - p1[X] - p22[W] + p1[W]); + break; + case "bottom": + t2 = (-p1[W] - p1[Y]) / (p22[Y] - p1[Y] + p22[W] - p1[W]); + break; + case "top": + t2 = (p1[W] - p1[Y]) / (p22[Y] - p1[Y] - p22[W] + p1[W]); + break; + case "near": + t2 = (NEAR_CLIP_Z - p1[Z]) / (p22[Z] - p1[Z]); + break; + case "far": + t2 = (p1[W] - p1[Z]) / (p22[Z] - p1[Z] - p22[W] + p1[W]); + break; + } + if (!Number.isFinite(t2)) t2 = 0.5; + t2 = max5(0, min5(1, t2)); + return t2; + } + let clippedVertices = [v1, v2]; + for (const edge of clippingBoundary) { + if (clippedVertices.length < 2) break; + const [p1, p22] = clippedVertices; + const p1Inside = inside(p1.pos, edge); + const p2Inside = inside(p22.pos, edge); + if (p1Inside && p2Inside) { + continue; + } else if (!p1Inside && !p2Inside) { + clippedVertices = []; + break; + } else { + const t2 = computeIntersection(p1.pos, p22.pos, edge); + const intersectionVertex = new Vertex( + lerp2(create3(), p1.pos, p22.pos, t2), + [ + p1.color[0] + (p22.color[0] - p1.color[0]) * t2, + p1.color[1] + (p22.color[1] - p1.color[1]) * t2, + p1.color[2] + (p22.color[2] - p1.color[2]) * t2, + p1.color[3] + (p22.color[3] - p1.color[3]) * t2 + ], + [ + p1.texCoords[0] + (p22.texCoords[0] - p1.texCoords[0]) * t2, + p1.texCoords[1] + (p22.texCoords[1] - p1.texCoords[1]) * t2 + ] + ); + if (p1Inside) { + clippedVertices = [p1, intersectionVertex]; + } else { + clippedVertices = [intersectionVertex, p22]; + } + } + } + return clippedVertices; +} +function perspectiveDivide(vertex) { + const w = vertex.pos[W]; + if (abs3(w) < MIN_PERSPECTIVE_W) { + const vert2 = new Vertex([NaN, NaN, NaN, w]); + vert2.color = vertex.color; + vert2.texCoords = vertex.texCoords; + return vert2; + } + const vert = new Vertex([ + vertex.pos[X] / w, + vertex.pos[Y] / w, + vertex.pos[Z] / w, + w + ]); + vert.color = vertex.color; + vert.texCoords = vertex.texCoords; + return vert; +} +function toScreenSpace(vertex) { + const x = vertex.pos[X]; + const y = -vertex.pos[Y]; + const z = vertex.pos[Z]; + const sX = (x + 1) / 2 * width; + const sY = (y + 1) / 2 * height; + const vert = new Vertex([sX, sY, z, vertex.pos[W]]); + vert.color = vertex.color; + vert.texCoords = vertex.texCoords; + return vert; +} +function clipToScreen(vertices) { + if (vertices.length < 3) return vertices; + const edges = [ + { axis: "x", sign: 1, bound: 0 }, + // left: x >= 0 + { axis: "x", sign: -1, bound: width }, + // right: x <= width + { axis: "y", sign: 1, bound: 0 }, + // top: y >= 0 + { axis: "y", sign: -1, bound: height } + // bottom: y <= height + ]; + let polygon = vertices; + for (const edge of edges) { + if (polygon.length < 3) break; + const clipped = []; + const prev = polygon[polygon.length - 1]; + for (let i2 = 0; i2 < polygon.length; i2++) { + const curr = polygon[i2]; + const prevVal = edge.axis === "x" ? prev.pos[0] : prev.pos[1]; + const currVal = edge.axis === "x" ? curr.pos[0] : curr.pos[1]; + const prevInside = edge.sign > 0 ? prevVal >= edge.bound : prevVal <= edge.bound; + const currInside = edge.sign > 0 ? currVal >= edge.bound : currVal <= edge.bound; + if (currInside) { + if (!prevInside) { + const t2 = (edge.bound - prevVal) / (currVal - prevVal); + if (Number.isFinite(t2) && t2 >= 0 && t2 <= 1) { + clipped.push(lerpVertex(prev, curr, t2)); + } + } + clipped.push(curr); + } else if (prevInside) { + const t2 = (edge.bound - prevVal) / (currVal - prevVal); + if (Number.isFinite(t2) && t2 >= 0 && t2 <= 1) { + clipped.push(lerpVertex(prev, curr, t2)); + } + } + } + polygon = clipped; + } + return polygon; +} +function lerpVertex(v1, v2, t2) { + const w1 = v1.pos[3]; + const w2 = v2.pos[3]; + const safeW = w1 > MIN_PERSPECTIVE_W && w2 > MIN_PERSPECTIVE_W && Number.isFinite(w1) && Number.isFinite(w2); + let newW; + if (safeW) { + const invW1 = 1 / w1; + const invW2 = 1 / w2; + const newInvW = invW1 + (invW2 - invW1) * t2; + newW = newInvW > 1e-4 ? 1 / newInvW : w1 + (w2 - w1) * t2; + } else { + newW = w1 + (w2 - w1) * t2; + } + const vert = new Vertex([ + v1.pos[0] + (v2.pos[0] - v1.pos[0]) * t2, + v1.pos[1] + (v2.pos[1] - v1.pos[1]) * t2, + v1.pos[2] + (v2.pos[2] - v1.pos[2]) * t2, + newW + ]); + vert.color = [ + v1.color[0] + (v2.color[0] - v1.color[0]) * t2, + v1.color[1] + (v2.color[1] - v1.color[1]) * t2, + v1.color[2] + (v2.color[2] - v1.color[2]) * t2, + v1.color[3] + (v2.color[3] - v1.color[3]) * t2 + ]; + if (safeW) { + const invW1 = 1 / w1, invW2 = 1 / w2; + const newInvW = invW1 + (invW2 - invW1) * t2; + const tc1OverW = [v1.texCoords[0] * invW1, v1.texCoords[1] * invW1]; + const tc2OverW = [v2.texCoords[0] * invW2, v2.texCoords[1] * invW2]; + const safeDiv = newInvW > 1e-4 ? newInvW : 1; + vert.texCoords = [ + (tc1OverW[0] + (tc2OverW[0] - tc1OverW[0]) * t2) / safeDiv, + (tc1OverW[1] + (tc2OverW[1] - tc1OverW[1]) * t2) / safeDiv + ]; + } else { + vert.texCoords = [ + v1.texCoords[0] + (v2.texCoords[0] - v1.texCoords[0]) * t2, + v1.texCoords[1] + (v2.texCoords[1] - v1.texCoords[1]) * t2 + ]; + } + return vert; +} + +// public/aesthetic.computer/disks/common/debug.mjs +var DEBUG = false; +function setDebug(bool) { + DEBUG = bool; +} + +// public/aesthetic.computer/lib/ask.mjs +var tokenProvider = null; +function setAskTokenProvider(provider) { + tokenProvider = provider; +} +var Conversation = class { + messages = []; + forgetful = false; + memory; + controller; + // from the `disk` api + store; + key; + constructor(store2, slug, forgetful = false, memory = Infinity) { + this.store = store2; + this.key = slug + ":conversation"; + this.forgetful = forgetful; + this.memory = memory; + } + // Retrieve messages from the store. + // This can probably be deprecated... 23.07.24.17.17 + async retrieve() { + if (!this.forgetful) { + this.messages = this.store[this.key] || await this.store.retrieve(this.key, "local:db") || []; + return this.messages.slice(); + } else { + return this.messages.slice(); + } + } + async forget() { + await this.store.delete(this.key, "local:db"); + delete this.store[this.key]; + this.messages = []; + } + ask(options, and, done, fail) { + let prompt, program = { before: "", after: "" }, hint; + if (typeof options === "string") { + prompt = options; + } else { + ({ prompt, program, hint } = options); + } + let messageLength = this.messages.length; + if (messageLength > this.memory) this.messages.length = messageLength = 0; + if (messageLength === 0) { + program.before = program.before?.trim(); + program.after = program.after?.trim(); + if (program.before) + this.messages.push({ by: "system", text: program.before }); + this.messages.push({ by: "user", text: prompt }); + if (program.after) + this.messages.push({ by: "system", text: program.after }); + } else { + this.messages.push({ by: "user", text: prompt }); + } + this.controller?.abort(); + this.controller = new AbortController(); + const signal = this.controller.signal; + const host = ``; + const responsePromise = (async () => { + const headers2 = { "Content-Type": "application/json" }; + try { + const token = await tokenProvider?.(); + if (token) headers2.Authorization = `Bearer ${token}`; + } catch (err) { + if (DEBUG) console.warn("\u{1F511} ask: no token \u2014", err); + } + return fetch(`${host}/api/ask`, { + method: "POST", + signal, + headers: headers2, + body: JSON.stringify({ messages: this.messages, hint }) + }); + })(); + if (this.forgetful) this.messages.length = 0; + let timeout; + const timeoutPromise = new Promise((resolve, reject) => { + timeout = setTimeout(() => { + this.controller?.abort(); + reject(new Error(`Reply timed out after 10 seconds!`)); + }, 10 * 1e3); + }); + let streamedReply = ""; + const convo = this; + function reportFailure(error) { + convo.messages = convo.messages.slice(0, messageLength); + fail?.(); + } + Promise.race([responsePromise, timeoutPromise]).then((response) => { + clearTimeout(timeout); + if (!response.ok) + throw new Error(`Failed to reply: ${response.status}`); + const readableStream = response.body; + const decoder = new TextDecoder(); + const reader = readableStream.getReader(); + function read() { + reader.read().then(async ({ done: complete, value }) => { + if (complete) { + convo.controller = null; + done?.(); + if (!convo.forgetful) + convo.messages.push({ by: "system", text: streamedReply }); + if (convo.store && convo.key) { + } + } else { + const got = decoder.decode(value, { stream: true }); + streamedReply += got; + and?.(got); + read(); + } + }).catch(reportFailure); + } + read(); + }).catch(reportFailure); + return () => { + this.controller?.abort(); + }; + } +}; + +// public/aesthetic.computer/lib/color-codes.mjs +function splitColorCodes(str7) { + if (!str7) return [str7 || ""]; + const parts = [""]; + let i2 = 0; + while (i2 < str7.length) { + if (str7[i2] !== "\\") { + parts[parts.length - 1] += str7[i2]; + i2 += 1; + } else if (str7[i2 + 1] === "\\") { + parts[parts.length - 1] += "\\"; + i2 += 2; + } else { + const close = str7.indexOf("\\", i2 + 1); + if (close === -1) { + parts[parts.length - 1] += "\\"; + i2 += 1; + } else { + parts.push(str7.slice(i2 + 1, close), ""); + i2 = close + 1; + } + } + } + return parts; +} +function stripColorCodes(str7) { + if (!str7) return str7; + const parts = splitColorCodes(str7); + let out = ""; + for (let i2 = 0; i2 < parts.length; i2 += 2) out += parts[i2]; + return out; +} +function hasColorCodes(str7) { + if (!str7) return false; + return splitColorCodes(str7).length > 1; +} +function escapeColorCodes(str7) { + if (!str7) return str7 || ""; + return str7.replace(/\\/g, "\\\\"); +} +function mapColorCodes(str7, fn) { + if (!str7) return str7; + const parts = splitColorCodes(str7); + let out = escapeColorCodes(parts[0]); + for (let i2 = 1; i2 < parts.length; i2 += 2) { + const replacement = fn(parts[i2]); + if (replacement !== null && replacement !== void 0) out += `\\${replacement}\\`; + out += escapeColorCodes(parts[i2 + 1]); + } + return out; +} + +// public/aesthetic.computer/lib/text.mjs +function capitalize(word) { + return word.charAt(0).toUpperCase() + word.slice(1); +} +function reverse(str7) { + return str7.split("").reverse().join(""); +} + +// public/aesthetic.computer/lib/ticker.mjs +var Ticker = class { + #offset = 0; + #speed = 1; + #separator = " - "; + #text = ""; + #textWidth = 0; + #separatorWidth = 0; + #cycleWidth = 0; + #api = null; + #lastUpdateTime = 0; + #targetFps = 30; + // Lock ticker animation to 30fps equivalent timing + constructor(text = "", options = {}) { + this.#text = text; + this.#speed = options.speed || 1; + this.#separator = options.separator || " - "; + } + // Set the text content for the ticker + setText(text) { + if (this.#text === text) return this; + this.#text = text; + this.#updateMeasurements(); + if (this.#cycleWidth > 0 && this.#offset >= this.#cycleWidth) { + this.#offset = this.#offset % this.#cycleWidth; + } + return this; + } + // Set the scrolling speed (pixels per frame) + setSpeed(speed) { + this.#speed = speed; + return this; + } + // Set the separator between text repetitions + setSeparator(separator) { + this.#separator = separator; + this.#updateMeasurements(); + return this; + } + // Set the target FPS for animation timing (default: 30) + setTargetFPS(fps) { + this.#targetFps = fps; + return this; + } + // Strip color codes from text for accurate measurement + // Color codes are in format \\color\\ or \\r,g,b\\ + #stripColorCodes(text) { + if (!text) return text; + return text.replace(/\\[^\\]*\\/g, ""); + } + // Update text measurements when text or separator changes + #updateMeasurements(font) { + if (!this.#api || !this.#text) return; + const strippedText = this.#stripColorCodes(this.#text); + const strippedSeparator = this.#stripColorCodes(this.#separator); + const textMeasurement = this.#api.text.box(strippedText, void 0, void 0, void 0, void 0, font); + const separatorMeasurement = this.#api.text.box(strippedSeparator, void 0, void 0, void 0, void 0, font); + this.#textWidth = textMeasurement.box.width; + this.#separatorWidth = separatorMeasurement.box.width; + this.#cycleWidth = this.#textWidth + this.#separatorWidth; + } + // Update the ticker animation (call this in your paint function) + update(api) { + this.#api = api; + if (this.#cycleWidth === 0 && this.#text) { + this.#updateMeasurements(); + } + const currentTime = api.clock.time(); + if (!currentTime) return; + const timeMs = currentTime.getTime(); + if (this.#lastUpdateTime === 0) { + this.#lastUpdateTime = timeMs; + return; + } + const deltaTime = timeMs - this.#lastUpdateTime; + const targetFrameTime = 1e3 / this.#targetFps; + if (deltaTime >= targetFrameTime) { + const framesPassed = Math.floor(deltaTime / targetFrameTime); + this.#offset += this.#speed * framesPassed; + this.#lastUpdateTime = timeMs; + if (this.#cycleWidth > 0) { + if (this.#offset >= this.#cycleWidth) { + this.#offset = this.#offset % this.#cycleWidth; + } else if (this.#offset < 0) { + this.#offset = this.#cycleWidth + this.#offset % this.#cycleWidth; + } + } + } + } + // Render the ticker at the specified position + paint(api, x, y, options = {}) { + if (!this.#text) return; + this.#api = api; + const font = options.font; + if (this.#cycleWidth === 0 || font) { + this.#updateMeasurements(font); + } + if (this.#cycleWidth <= 0) return; + const displayWidth = options.width || api.screen.width; + const numCycles = Math.ceil((displayWidth + this.#cycleWidth) / this.#cycleWidth) + 1; + for (let i2 = 0; i2 < numCycles; i2++) { + const baseX = x + i2 * this.#cycleWidth - this.#offset; + if (baseX > -this.#cycleWidth && baseX < x + displayWidth + this.#cycleWidth) { + if (options.color) api.ink(options.color, options.alpha || 255); + api.write(this.#text, { x: baseX, y }, void 0, void 0, false, font); + api.write(this.#separator, { x: baseX + this.#textWidth, y }, void 0, void 0, false, font); + } + } + } + // Get current offset (useful for debugging or synchronization) + getOffset() { + return this.#offset; + } + // Set the offset directly (useful for manual control) + setOffset(offset) { + this.#offset = offset; + const currentTime = this.#api?.clock?.time?.(); + if (currentTime) { + this.#lastUpdateTime = currentTime.getTime(); + } + return this; + } + // Reset the ticker to the beginning + reset() { + this.#offset = 0; + this.#lastUpdateTime = 0; + return this; + } + // Get the cycle width (useful for calculations) + getCycleWidth() { + return this.#cycleWidth; + } +}; + +// public/aesthetic.computer/lib/gizmo.mjs +var Hourglass = class { + ticks = 0; + max = 1; + progress = 0; + complete = false; + flips = 0n; + #completed; + #flipped; + autoFlip = false; + #every; + // Callback that fires every frame. + constructor(max9 = 1, { completed, flipped, every: every2, autoFlip = false } = {}, startingTicks = 0) { + this.max = max9; + this.ticks = startingTicks; + this.autoFlip = autoFlip; + this.#completed = completed; + this.#flipped = flipped; + this.#every = every2; + } + step() { + if (this.complete === true) return console.log("\u231B Already complete."); + this.ticks += 1; + this.#every?.(); + this.progress = this.ticks / this.max; + if (this.ticks >= this.max) { + this.complete = true; + this.progress = 1; + this.#completed?.(this.flips); + if (this.autoFlip) this.flip(); + } + } + get progress() { + return this.ticks / this.max; + } + flip() { + this.flips += 1n; + this.ticks = 0; + this.complete = false; + this.#flipped?.(this.flips, ...arguments); + } +}; +var EllipsisTicker = class { + #ellipsisDots = 0; + #lastUpdateTime = 0; + #updateInterval = 500; + // 500ms = 0.5 seconds, locked to 30fps equivalent + constructor(options = {}) { + this.#updateInterval = options.interval || 500; + } + // Get the current ellipses with a padded end. + text(repeat2, opts) { + let ellipsis = ""; + repeat2(this.#ellipsisDots, () => ellipsis += "."); + return opts?.pad === false ? ellipsis : ellipsis.padEnd(3, " "); + } + // Update the ticker using clock time instead of frame counting + update(clockTime) { + if (!clockTime) return; + const currentTime = clockTime.getTime(); + if (this.#lastUpdateTime === 0) { + this.#lastUpdateTime = currentTime; + return; + } + if (currentTime - this.#lastUpdateTime >= this.#updateInterval) { + this.#ellipsisDots = (this.#ellipsisDots + 1) % 4; + this.#lastUpdateTime = currentTime; + } + } + // Keep the old sim() method for backward compatibility, but it won't do anything + sim() { + } +}; + +// public/aesthetic.computer/lib/ui.mjs +var { round: round6 } = Math; +var TYPEFACE_UI; +function stripColorCodes2(text) { + if (!text || typeof text !== "string") return text; + return text.replace(/\\[^\\]*\\/g, ""); +} +function defaultRolloverScheme($, small = false) { + const mode = $?.dark ? $?.theme?.dark : $?.theme?.light; + const bg = mode?.buttonHover || mode?.buttonBg || ($?.dark ? [24, 32, 42] : [220, 245, 255]); + const outline = mode?.buttonHoverOutline || mode?.buttonOutline || [255, 220, 0]; + const text = mode?.buttonHoverText || mode?.buttonText || ($?.dark ? 255 : 0); + return small ? [bg, outline, text, bg] : [bg, outline, text, 255]; +} +var activeButtons = /* @__PURE__ */ new Set(); +var hoveredButtons = /* @__PURE__ */ new Set(); +function updateHoverCursor(event) { + for (const button of hoveredButtons) + if (!button.over) hoveredButtons.delete(button); + event?.cursor?.(hoveredButtons.size ? "active" : "precise"); +} +var recentRollouts = /* @__PURE__ */ new Map(); +var ROLLOUT_MEMORY_TIME = 2e3; +function trackRollout(buttonId) { + recentRollouts.set(buttonId, performance.now()); + for (const [id, timestamp2] of recentRollouts.entries()) { + if (performance.now() - timestamp2 > ROLLOUT_MEMORY_TIME) { + recentRollouts.delete(id); + } + } +} +function wasRecentlyRolledOut(buttonId) { + const rolloutTime = recentRollouts.get(buttonId); + return rolloutTime && performance.now() - rolloutTime < ROLLOUT_MEMORY_TIME; +} +function addActiveButton(btn, reason = "unknown", netLog = null) { + activeButtons.add(btn); + const logData = { + buttonId: btn.id || "unnamed", + reason, + totalActive: activeButtons.size, + down: btn.down, + downPointer: btn.downPointer + }; + if (netLog) { + netLog.info("\u{1F518} Button activated:", logData); + } + if (activeButtons.size > 5) { + if (netLog) netLog.warn("\u{1F6A8} Emergency cleanup: too many active buttons:", activeButtons.size); + emergencyButtonCleanup("too many active buttons detected: " + activeButtons.size, netLog); + } +} +function removeActiveButton(btn, reason = "unknown", netLog = null) { + const wasPresent = activeButtons.has(btn); + activeButtons.delete(btn); + if (wasPresent) { + const logData = { + buttonId: btn.id || "unnamed", + reason, + totalActive: activeButtons.size, + down: btn.down, + downPointer: btn.downPointer + }; + if (netLog) { + netLog.info("\u{1F518} Button deactivated:", logData); + } + } else { + const logData = { + buttonId: btn.id || "unnamed", + reason + }; + console.warn("\u26A0\uFE0F Tried to remove button not in activeButtons:", logData); + if (netLog) { + netLog.warn("\u26A0\uFE0F Button removal attempted but not found:", logData); + } + } +} +var debugButtonStateChecks = 0; +function debugActiveButtonsState() { + debugButtonStateChecks++; + if (debugButtonStateChecks % 60 === 0) { + const activeButtonsArray = Array.from(activeButtons); + const stuckButtons = activeButtonsArray.filter((btn) => !btn.down); + if (stuckButtons.length > 0) { + console.warn("\u{1F41B} INCONSISTENT BUTTON STATE DETECTED:", { + totalActive: activeButtonsArray.length, + stuckButtons: stuckButtons.map((btn) => ({ + id: btn.id || "unnamed", + down: btn.down, + over: btn.over, + downPointer: btn.downPointer + })), + allButtons: activeButtonsArray.map((btn) => ({ + id: btn.id || "unnamed", + down: btn.down, + over: btn.over, + downPointer: btn.downPointer + })) + }); + stuckButtons.forEach((btn) => { + console.log("\u{1F9F9} Cleaning up stuck button:", btn.id || "unnamed"); + btn.actions?.cancel?.(btn); + btn.over = false; + btn.downPointer = void 0; + activeButtons.delete(btn); + }); + } + } +} +function resetAllButtons(netLog = null) { + console.log("\u{1F504} MANUAL BUTTON RESET - clearing all active buttons"); + if (netLog) { + netLog.info("\u{1F504} Manual button reset initiated"); + } + const buttonsToReset = Array.from(activeButtons); + buttonsToReset.forEach((btn) => { + console.log("\u{1F504} Resetting button:", btn.id || "unnamed"); + btn.down = false; + btn.over = false; + btn.downPointer = void 0; + removeActiveButton(btn, "manual reset", netLog); + }); +} +function emergencyButtonCleanup(reason = "unknown", netLog = null) { + console.warn("\u{1F6A8} EMERGENCY BUTTON CLEANUP:", reason); + if (netLog) { + netLog.error("\u{1F6A8} Emergency button cleanup triggered:", { reason }); + } + const activeButtonsArray = Array.from(activeButtons); + const buttonStates = activeButtonsArray.map((btn) => ({ + id: btn.id || "unnamed", + down: btn.down, + over: btn.over, + downPointer: btn.downPointer + })); + console.log("\u{1F4CA} Active buttons before cleanup:", buttonStates); + if (netLog) { + netLog.info("\u{1F4CA} Emergency cleanup button states:", buttonStates); + } + activeButtonsArray.forEach((btn) => { + btn.actions?.cancel?.(btn); + btn.down = false; + btn.over = false; + btn.downPointer = void 0; + activeButtons.delete(btn); + }); + console.log("\u{1F9F9} Emergency cleanup complete - all buttons reset"); + if (netLog) { + netLog.info("\u{1F9F9} Emergency cleanup completed:", { buttonsReset: activeButtonsArray.length }); + } +} +if (typeof window !== "undefined") { + window.resetAllButtons = resetAllButtons; + window.emergencyButtonCleanup = emergencyButtonCleanup; +} +var Button = class { + btn; + box; + down = false; + disabled = false; + icon; + dom = false; + over = false; + // Keep track of rollover state. + multitouch = true; + // Toggle to false to make a single touch button2. + downPointer; + // Keep track of what original pointer downed the button. + actions; + // A held list of callbacks for virtually triggering events. + noEdgeDetection = false; + // Set to true to opt out of global edge detection cancellation + noRolloverActivation = false; + // Set to true to prevent activation via rollover from other buttons + stickyScrubbing = false; + // Set to true to prevent rollover activation when scrubbing from another button + offScreenScrubbing = false; + // Set to true to allow scrubbing to continue off-screen but still allow horizontal rollover + get up() { + return !this.down; + } + set up(value) { + this.down = !value; + } + // (x, y, width, height) or Box + constructor() { + if (arguments.length === 1) { + this.box = Box.from(arguments[0]); + } else this.box = new Box(...arguments); + this.btn = this; + } + publishToDom({ send: send2 }, label, message) { + send2({ + type: "button:hitbox:add", + content: { box: this.box, label, message } + }); + } + removeFromDom({ send: send2 }, label) { + send2({ type: "button:hitbox:remove", content: label }); + } + // For using in a piece's `act` function. Contains callbacks for + // events that take place inside the button. + // Usage: act(e, () => {}); // For 'push' callback only. + // act(e, {push: () => {}, down: () => {}, cancel: () => {}, draw() => {}}); + // You can optionally pass in an array of `pens` {x, y} for multi-touch support. + act(e2, callbacks = () => { + }, pens = []) { + const btn = this.btn; + if (btn.disabled) { + if (btn.over) { + btn.over = false; + hoveredButtons.delete(btn); + updateHoverCursor(e2); + } + return; + } + const netLog = e2.net?.log; + debugActiveButtonsState(); + if (typeof callbacks === "function") callbacks = { push: callbacks }; + btn.actions = callbacks; + if (e2.is("ui:cancel-interactions") && !btn.noEdgeDetection) { + const shouldCancel = btn.down && (!btn.offScreenScrubbing || btn.offScreenScrubbing && (e2.x < btn.box.x || e2.x >= btn.box.x + btn.box.w)); + if (shouldCancel) { + console.log("\u{1F6AB} Global edge detection - button cancelled:", { + buttonId: btn.id || "unnamed", + timestamp: performance.now(), + reason: "ui:cancel-interactions event", + wasDown: btn.down, + offScreenScrubbing: btn.offScreenScrubbing, + cursorX: e2.x, + buttonBounds: { x: btn.box.x, w: btn.box.w } + }); + btn.down = false; + btn.over = false; + hoveredButtons.delete(btn); + updateHoverCursor(e2); + btn.downPointer = void 0; + removeActiveButton(btn, "global edge detection", netLog); + callbacks.cancel?.(btn); + } + if (!shouldCancel && activeButtons.has(btn) && !btn.down) { + console.warn("\u{1F9F9} Edge detection cleanup - removing orphaned button:", { + buttonId: btn.id || "unnamed", + reason: "button in activeButtons but not down during edge detection" + }); + removeActiveButton(btn, "edge detection orphan cleanup", netLog); + btn.over = false; + btn.downPointer = void 0; + } + return; + } + const t2 = this.multitouch ? "any" : "1"; + if (e2.hudButtonActive && !btn.noEdgeDetection) { + return; + } + if (e2.is(`touch:${t2}`) && btn.box.contains(e2) && !btn.down) { + const wasRecentRollout = wasRecentlyRolledOut(btn.id || "unnamed"); + if (activeButtons.has(btn) && !btn.down) { + console.warn("\u26A0\uFE0F CORRUPTED STATE DETECTED - button in activeButtons but not down:", { + buttonId: btn.id || "unnamed", + reason: "cleaning up before new touch", + wasRecentlyRolledOut: wasRecentRollout + }); + removeActiveButton(btn, "corrupted state cleanup before touch", netLog); + btn.over = false; + btn.downPointer = void 0; + } + if (wasRecentRollout) { + console.log("\u{1F504} Retap after recent rollout - ensuring clean state:", { + buttonId: btn.id || "unnamed" + }); + btn.over = false; + btn.downPointer = void 0; + if (activeButtons.has(btn)) { + removeActiveButton(btn, "retap after rollout cleanup", netLog); + } + } + const downed = callbacks.down?.(btn); + btn.down = downed || downed === void 0 ? true : false; + if (btn.down && btn.downPointer === void 0) { + btn.downPointer = e2.pointer || 0; + } else if (btn.down && btn.downPointer !== void 0 && btn.downPointer !== e2.pointer) { + if (this.multitouch) { + console.log("\u{1F504} Transferring downPointer ownership:", { + buttonId: btn.id || "unnamed", + fromPointer: btn.downPointer, + toPointer: e2.pointer, + reason: "multitouch pointer transfer" + }); + btn.downPointer = e2.pointer || 0; + } else { + console.warn("\u26A0\uFE0F Button already has downPointer - NOT updating:", { + buttonId: btn.id || "unnamed", + eventPointer: e2.pointer, + existingDownPointer: btn.downPointer, + reason: "downPointer was not cleaned up properly" + }); + } + } + btn.over = btn.down; + if (btn.down) { + addActiveButton(btn, "touch down", netLog); + } + } + const isControllingLiftPointer = !this.multitouch || btn.downPointer === e2.pointer || btn.downPointer === void 0; + if (e2.is(`lift:${t2}`) && btn.down && isControllingLiftPointer) { + let up = function() { + const up2 = callbacks.up?.(btn); + if (up2 === false) { + btn.down = true; + btn.over = true; + console.log("\u{1F504} Button kept down by callback:", { + buttonId: btn.id || "unnamed", + reason: "up() returned false" + }); + } else { + btn.down = false; + btn.over = false; + btn.downPointer = void 0; + if (activeButtons.has(btn)) { + removeActiveButton(btn, "button up callback", netLog); + } + } + }; + const isControllingPointer = !this.multitouch || btn.downPointer === e2.pointer || btn.downPointer === void 0; + const isValidPush = isControllingPointer && btn.box.contains(e2); + if (isValidPush) { + btn.down = false; + btn.over = false; + if (activeButtons.has(btn)) { + removeActiveButton(btn, "valid push", netLog); + } + callbacks.push?.(btn); + up(); + } else if (isControllingPointer && !btn.box.contains(e2)) { + btn.down = false; + btn.over = false; + if (activeButtons.has(btn)) { + removeActiveButton(btn, "cancelled - lift outside", netLog); + } + callbacks.cancel?.(btn); + up(); + } else { + const isStuckButton = btn.down && !isControllingPointer; + if (isStuckButton) { + btn.down = false; + btn.over = false; + btn.downPointer = void 0; + removeActiveButton(btn, "force cleanup - stuck button", netLog); + callbacks.cancel?.(btn); + } else { + console.log("\u26A0\uFE0F Button down but lift ignored:", { + buttonId: btn.id || "unnamed", + timestamp: performance.now(), + pointer: e2.pointer, + downPointer: btn.downPointer, + isControllingPointer, + containsE: btn.box.contains(e2), + pensLength: pens?.length || 0, + reason: "lift event ignored - waiting for controlling pointer" + }); + } + } + } + if (e2.is && e2.is("move")) { + try { + const containsNow = btn.box.contains(e2); + if (containsNow && !btn.over && !btn.down) { + callbacks.hover?.(btn); + btn.over = true; + hoveredButtons.add(btn); + updateHoverCursor(e2); + } else if (!containsNow && btn.over && !btn.down) { + callbacks.leave?.(btn); + btn.over = false; + hoveredButtons.delete(btn); + updateHoverCursor(e2); + } + } catch (err) { + } + updateHoverCursor(e2); + } + const horizontallyWithin = btn.offScreenScrubbing && e2.x >= btn.box.x && e2.x < btn.box.x + btn.box.w; + if (e2.is(`draw:${t2}`) && !btn.over && (btn.box.contains(e2) || horizontallyWithin)) { + const anyButtonDown = Array.from(activeButtons).some((activeBtn) => activeBtn.down) || // Also check for buttons that might be down but not in activeButtons due to rollout + e2.drag && (e2.drag.x !== e2.x || e2.drag.y !== e2.y); + const isDraggingFromOtherButton = anyButtonDown && !btn.down; + const hasStickyButton = Array.from(activeButtons).some( + (activeBtn) => activeBtn.stickyScrubbing + ); + const shouldPreventRollover = btn.noRolloverActivation && isDraggingFromOtherButton || isDraggingFromOtherButton && hasStickyButton && btn.noRolloverActivation; + if (isDraggingFromOtherButton && !shouldPreventRollover && (btn.box.contains(e2) || horizontallyWithin)) { + for (const otherBtn of activeButtons) { + if (otherBtn !== btn && !otherBtn.stickyScrubbing) { + if (!this.multitouch || otherBtn.downPointer === e2.pointer || otherBtn.downPointer === void 0) { + otherBtn.down = false; + otherBtn.over = false; + otherBtn.actions?.up?.(otherBtn); + removeActiveButton(otherBtn, "rollover deactivation", netLog); + } + } + } + if (!hasStickyButton || btn.stickyScrubbing) { + btn.down = true; + btn.downPointer = e2.pointer || 0; + addActiveButton(btn, "rollover activation", netLog); + callbacks.down?.(btn); + } + } + if (!shouldPreventRollover || horizontallyWithin) { + if (callbacks.rollover) { + callbacks.rollover(btn); + } else { + callbacks.over?.(btn); + } + btn.over = true; + } + } + const containsDrag = btn.box.contains(e2.drag); + const inActiveButtons = activeButtons.has(btn); + const horizontallyWithinForOffScreen = btn.offScreenScrubbing && e2.drag && e2.drag.x >= btn.box.x && e2.drag.x < btn.box.x + btn.box.w; + const allowScrub = containsDrag || inActiveButtons && !btn.stickyScrubbing || btn.stickyScrubbing && btn.down || btn.offScreenScrubbing && btn.down && horizontallyWithinForOffScreen; + if (e2.is(`draw:${t2}`) && btn.down && allowScrub) { + const isControllingPointer = !this.multitouch || btn.downPointer === e2.pointer || btn.downPointer === void 0; + if (isControllingPointer || inActiveButtons && !btn.stickyScrubbing && btn.downPointer === e2.pointer || // Rollover scrubbing with pointer match + btn.stickyScrubbing && btn.down || btn.offScreenScrubbing && btn.down && horizontallyWithinForOffScreen) { + callbacks.scrub?.(btn); + } + } + if (e2.is(`draw:${t2}`) && btn.over && !btn.box.contains(e2) && btn.box.containsNone(pens)) { + const shouldRollout = !btn.stickyScrubbing && (!btn.offScreenScrubbing || btn.offScreenScrubbing && (e2.x < btn.box.x || e2.x >= btn.box.x + btn.box.w)); + const isControllingPointer = !this.multitouch || btn.downPointer === e2.pointer || btn.downPointer === void 0; + if (shouldRollout && isControllingPointer) { + trackRollout(btn.id || "unnamed"); + if (callbacks.rollout) { + callbacks.rollout(btn); + } else { + callbacks.out?.(btn); + } + btn.over = false; + if (activeButtons.has(btn)) { + removeActiveButton(btn, "rollout - always remove from active", netLog); + } + if (btn.down) { + btn.down = false; + btn.downPointer = void 0; + } + } + } + if (!btn.down && activeButtons.has(btn)) { + removeActiveButton(btn, "safety cleanup - button not down", netLog); + } + } + // Draws a callback if the button is not disabled. + paint(fn) { + if (!this.disabled) fn(this); + } + enableIf(flag) { + this.disabled = !flag; + } +}; +var Slider = class { + box; + button; + min = 0; + max = 1; + value = 0; + step = 0; + dragging = false; + constructor() { + let opts = {}; + if (arguments.length >= 4) { + if (typeof arguments[4] === "object") opts = arguments[4]; + this.box = new Box(arguments[0], arguments[1], arguments[2], arguments[3]); + } else { + this.box = Box.from(arguments[0]); + if (typeof arguments[1] === "object") opts = arguments[1]; + } + this.min = Number.isFinite(opts.min) ? opts.min : 0; + this.max = Number.isFinite(opts.max) ? opts.max : 1; + this.step = Number.isFinite(opts.step) ? opts.step : 0; + this.value = Number.isFinite(opts.value) ? opts.value : this.min; + this.button = new Button(this.box); + this.button.stickyScrubbing = true; + this.button.offScreenScrubbing = true; + this.button.noRolloverActivation = true; + } + get normalized() { + const span = this.max - this.min || 1; + return (this.value - this.min) / span; + } + setValue(value) { + let v2 = Math.max(this.min, Math.min(this.max, value)); + if (this.step > 0) { + const steps = Math.round((v2 - this.min) / this.step); + v2 = this.min + steps * this.step; + } + this.value = v2; + } + setNormalized(t2) { + const clamped = Math.max(0, Math.min(1, t2)); + this.setValue(this.min + clamped * (this.max - this.min)); + } + _updateFromEvent(e2) { + const t2 = (e2.x - this.box.x) / this.box.w; + this.setNormalized(t2); + } + act(e2, callbacks = {}) { + const onChange = callbacks.change; + this.button.act(e2, { + down: () => { + this.dragging = true; + this._updateFromEvent(e2); + callbacks.down?.(this); + onChange?.(this); + }, + scrub: () => { + this._updateFromEvent(e2); + onChange?.(this); + }, + up: () => { + this.dragging = false; + callbacks.up?.(this); + }, + cancel: () => { + this.dragging = false; + callbacks.cancel?.(this); + }, + over: () => callbacks.over?.(this), + out: () => callbacks.out?.(this) + }); + } + paint(draw2) { + draw2?.(this); + } +}; +var TextButton = class { + txt; + btn; + #gap = 4; + #cw = 6; + // Character width in pixels. Set from `typeface`. + #g2 = this.#gap * 2; + #h = 12 + this.#g2; + // 19; // + #offset = { x: this.#gap, y: this.#gap }; + #typeface = null; + constructor(text = "Button", pos = { x: 0, y: 0 }, typeface = TYPEFACE_UI, gap = null) { + if (gap !== null) { + this.#gap = gap; + this.#g2 = this.#gap * 2; + this.#offset = { x: this.#gap, y: this.#gap }; + } + this.#typeface = typeface; + this.#cw = typeface.blockWidth; + this.#h = typeface.blockHeight + this.#gap * 2; + this.txt = text; + this.btn = new Button(this.#computePosition(text, { ...pos })); + } + get act() { + return this.btn.act; + } + set disabled(d2) { + return this.btn.disabled = d2; + } + get disabled() { + return this.btn.disabled; + } + get down() { + return this.btn.down; + } + set down(d2) { + return this.btn.down = d2; + } + get noEdgeDetection() { + return this.btn.noEdgeDetection; + } + set noEdgeDetection(value) { + this.btn.noEdgeDetection = value; + } + get stickyScrubbing() { + return this.btn.stickyScrubbing; + } + set stickyScrubbing(value) { + this.btn.stickyScrubbing = value; + } + #measureTextWidth(text) { + const visibleText = stripColorCodes2(text); + if (this.#typeface?.getAdvance) { + let w = 0; + for (const ch of visibleText) w += this.#typeface.getAdvance(ch); + return w; + } + return visibleText.length * this.#cw; + } + get width() { + return this.#measureTextWidth(this.txt) + this.#gap * 2; + } + get height() { + return this.#h; + } + // Compute position for box. + // pos: {x, y} or { top, left } for positioning. + // pos: {bottom, right} for bottom right... + // { center: "xy", screen } for screen centering. + #computePosition(text, pos = { x: 0, y: 0 }) { + pos = { ...pos }; + let x, y; + const w = this.#measureTextWidth(text) + this.#g2; + const h = this.#h; + if (pos.screen) { + pos.screen.x = pos.screen.x || 0; + pos.screen.y = pos.screen.y || 0; + } + if (pos.center === "xy") { + return { + x: pos.screen.x + (pos.x || 0) + pos.screen.width / 2 - w / 2, + y: pos.screen.y + (pos.y || 0) + pos.screen.height / 2 - h / 2, + w, + h + }; + } + if (pos.center === "x") { + x = (pos.screen?.x || 0) + (pos.screen?.width || 0) / 2 - w / 2; + } else { + x = (pos.screen?.x || 0) + (pos.x || 0); + if (pos.right !== void 0) { + x += pos.screen.width - pos.right - w; + } else { + x += pos.left || 0; + } + } + y = (pos.screen?.y || 0) + (pos.y || 0); + if (pos.bottom !== void 0) { + y += pos.screen.height - pos.bottom - this.#h; + } else { + y += pos.top || 0; + } + return { x, y, w, h }; + } + // Update just the label text (keeps the same box when visible length matches). + replaceLabel(txt) { + this.txt = txt; + } + reposition(pos, txt) { + if (txt) this.txt = txt; + this.btn.box = Box.from(this.#computePosition(this.txt, pos)); + } + paint($, scheme = [0, 255, 255, 0], hoverScheme = [255, 0, 0, 255], disabledScheme = [64, 127, 127, 64], rolloverScheme = void 0) { + if (rolloverScheme === void 0) rolloverScheme = defaultRolloverScheme($); + let s2; + if (this.btn.disabled) { + s2 = disabledScheme; + } else if (this.btn.down && hoverScheme) { + s2 = hoverScheme; + } else if (this.btn.over && rolloverScheme) { + s2 = rolloverScheme; + } else { + s2 = scheme; + } + const fillColor = s2[0] !== void 0 ? s2[0] : [0, 0, 0]; + const outlineColor = s2[1] !== void 0 ? s2[1] : fillColor; + const textColor = s2[2] !== void 0 ? s2[2] : outlineColor; + const textAlpha = typeof s2[3] === "number" ? s2[3] : void 0; + $.ink(fillColor).box(this.btn.box, "fill"); + $.ink(outlineColor).box(this.btn.box, "outline"); + if (textAlpha !== void 0) { + $.ink(textColor).write(this.txt, p2.add(this.btn.box, this.#offset), void 0, textAlpha); + } else { + $.ink(textColor).write(this.txt, p2.add(this.btn.box, this.#offset)); + } + } +}; +var TextButtonSmall = class { + txt; + btn; + // MatrixChunky8: 4px char width, 7px char height + #cw = 4; + #ch = 7; + #padL = 2; + // Left padding + #padR = 2; + // Right padding + #padY = 2; + // Vertical padding + #offset; + constructor(text = "Button", pos = { x: 0, y: 0 }) { + this.txt = text; + this.#offset = { x: this.#padL, y: this.#padY }; + this.btn = new Button(this.#computePosition(text, { ...pos })); + } + get act() { + return this.btn.act; + } + set disabled(d2) { + return this.btn.disabled = d2; + } + get disabled() { + return this.btn.disabled; + } + get down() { + return this.btn.down; + } + set down(d2) { + return this.btn.down = d2; + } + get stickyScrubbing() { + return this.btn.stickyScrubbing; + } + set stickyScrubbing(value) { + this.btn.stickyScrubbing = value; + } + get width() { + const visibleText = stripColorCodes2(this.txt); + return visibleText.length * this.#cw + this.#padL + this.#padR; + } + get height() { + return this.#ch + this.#padY * 2; + } + #computePosition(text, pos = { x: 0, y: 0 }) { + const visibleText = stripColorCodes2(text); + const w = visibleText.length * this.#cw + this.#padL + this.#padR; + const h = this.#ch + this.#padY * 2; + let x = pos.x || 0; + let y = pos.y || 0; + if (pos.screen) { + const sx = pos.screen.x || 0; + const sy = pos.screen.y || 0; + const sw = pos.screen.width || 0; + const sh = pos.screen.height || 0; + if (pos.center === "xy") { + return { x: sx + sw / 2 - w / 2, y: sy + sh / 2 - h / 2, w, h }; + } + if (pos.center === "x") { + x = sx + sw / 2 - w / 2; + } + if (pos.center !== "x") { + if (pos.right !== void 0) { + x = sx + sw - pos.right - w; + } else { + x = sx + (pos.left || pos.x || 0); + } + } + if (pos.bottom !== void 0) { + y = sy + sh - pos.bottom - h; + } else { + y = sy + (pos.top || pos.y || 0); + } + } + return { x, y, w, h }; + } + reposition(pos, txt) { + if (txt) this.txt = txt; + this.btn.box = Box.from(this.#computePosition(this.txt, pos)); + } + paint($, scheme = [[0, 64, 0], [0, 140, 0], 255, [0, 64, 0]], hoverScheme = [[0, 100, 0], [0, 180, 0], 255, [0, 100, 0]], disabledScheme = [[32, 32, 32], [64, 64, 64], 80, [32, 32, 32]], rolloverScheme = void 0) { + if (rolloverScheme === void 0) rolloverScheme = defaultRolloverScheme($, true); + let s2; + if (this.btn.disabled) { + s2 = disabledScheme; + } else if (this.btn.down) { + s2 = hoverScheme; + } else if (this.btn.over && rolloverScheme) { + s2 = rolloverScheme; + } else { + s2 = scheme; + } + $.ink(s2[0]).box(this.btn.box, "fill").ink(s2[1]).box(this.btn.box, "outline").ink(s2[2]).write(this.txt, p2.add(this.btn.box, this.#offset), void 0, void 0, false, "MatrixChunky8"); + } +}; +function setTypeface(tf2) { + TYPEFACE_UI = tf2; +} + +// public/aesthetic.computer/lib/platform.mjs +var platform_exports = {}; +__export(platform_exports, { + Aesthetic: () => Aesthetic, + AestheticExtension: () => AestheticExtension, + AestheticIOSApp: () => AestheticIOSApp, + Android: () => Android, + Desktop: () => Desktop, + Instagram: () => Instagram, + MacOS: () => MacOS, + MetaBrowser: () => MetaBrowser, + Safari: () => Safari, + TikTok: () => TikTok, + iOS: () => iOS, + isAestheticIOSAppUserAgent: () => isAestheticIOSAppUserAgent +}); +var nav; +try { + nav = navigator; +} catch (e2) { + nav = {}; +} +var iOS = /(iPad|iPhone|iPod)/g.test(nav.userAgent); +var Safari = /apple/i.test(nav.vendor); +var Android = /(Android)/g.test(nav.userAgent); +var MetaBrowser = /(OculusBrowser)/g.test(nav.userAgent); +var Desktop = !iOS && !Android && !MetaBrowser; +var MacOS = /(Macintosh|Mac OS X)/g.test(nav.userAgent) && (nav.maxTouchPoints || 0) <= 1; +var Instagram = /(Instagram)/g.test(nav.userAgent); +var TikTok = /BytedanceWebview/i.test(nav.userAgent); +var Aesthetic = /Aesthetic/i.test(nav.userAgent); +var AestheticExtension = /AestheticExtension/i.test(nav.userAgent); +function isAestheticIOSAppUserAgent(userAgent) { + return /^Aesthetic$/i.test(String(userAgent || "").trim()); +} +var AestheticIOSApp = isAestheticIOSAppUserAgent(nav.userAgent); + +// public/aesthetic.computer/lib/shop.mjs +var signed = [ + // 📚 Books on https://shop.aesthetic.computer + "25.4.8.21.19", + "25.4.8.21.17", + "25.4.8.21.11", + "25.4.8.21.08", + "25.4.8.21.07", + "25.4.8.21.0", + "25.4.8.20.47", + "25.4.8.21.13", + "25.11.17.19.03", + // 🎵 Music + "25.11.3.14.17", + // 🖍️ Pictures + "25.4.13.17.47", + "25.4.13.19.06", + "25.4.13.18.18", + "25.4.13.19.24", + "25.4.13.18.54", + // 🚲 Bikes + "25.12.4.10.09", + "25.12.4.10.08", + // 🛠️ Tools + "25.12.4.11.21", + "25.12.4.11.22", + "25.12.4.11.23", + "25.12.4.11.24", + "25.12.4.11.25", + // 💻 Laptops + "26.4.17.12.51", + "blank" + // Alias for the AC Native Laptop — "shop blank" in the prompt. +]; + +// public/aesthetic.computer/lib/kidlisp.mjs +var kidlisp_exports = {}; +__export(kidlisp_exports, { + KIDLISP_COLORS: () => KIDLISP_COLORS, + KIDLISP_FUNCTIONS: () => KIDLISP_FUNCTIONS, + KIDLISP_VOCABULARY: () => KIDLISP_VOCABULARY, + KidLisp: () => KidLisp, + clearAllCaches: () => clearAllCaches, + clearExecutionTrace: () => clearExecutionTrace, + decodeKidlispFromUrl: () => decodeKidlispFromUrl, + disableKidlispTrace: () => disableKidlispTrace, + enableKidlispConsole: () => enableKidlispConsole, + enableKidlispTrace: () => enableKidlispTrace, + encodeKidlispForUrl: () => encodeKidlispForUrl, + ensureGlobalInstance: () => ensureGlobalInstance, + evaluate: () => evaluate, + fetchCachedCode: () => fetchCachedCode, + fetchKidlispMetadata: () => fetchKidlispMetadata, + fetchMultipleCachedCodes: () => fetchMultipleCachedCodes, + getCachedCode: () => getCachedCode, + getCachedCodeMultiLevel: () => getCachedCodeMultiLevel, + getExecutionTrace: () => getExecutionTrace, + getGlobalInstance: () => getGlobalInstance, + getSyntaxHighlightingColors: () => getSyntaxHighlightingColors, + globalCodeCache: () => globalCodeCache, + initPersistentCache: () => initPersistentCache, + isActualKidLisp: () => isActualKidLisp, + isChaoticSource: () => isChaoticSource, + isKidlispConsoleEnabled: () => isKidlispConsoleEnabled, + isKidlispSource: () => isKidlispSource, + isKidlispTraceEnabled: () => isKidlispTraceEnabled, + isPromptInKidlispMode: () => isPromptInKidlispMode, + isValidRGBString: () => isValidRGBString, + module: () => module, + parse: () => parse, + postExecutionTrace: () => postExecutionTrace, + postKidlispConsoleImage: () => postKidlispConsoleImage, + saveCodeToAllCaches: () => saveCodeToAllCaches, + setCachedCode: () => setCachedCode, + slideUpdate: () => slideUpdate, + tokenize: () => tokenize, + updateKidLispAudio: () => updateKidLispAudio +}); + +// public/aesthetic.computer/lib/sound/wave-timbre.mjs +var WAVE_TIMBRE = { + sine: { brightness: 2.7183, bite: 0.869, riseMs: 0.667, asyncMs: 0 }, + composite: { brightness: 2.7344, bite: 0.6847, riseMs: 2, asyncMs: 0.553 }, + triangle: { brightness: 2.8012, bite: 0.869, riseMs: 0.667, asyncMs: 0 }, + square: { brightness: 4.2905, bite: 1, riseMs: 0, asyncMs: 0 }, + whistle: { brightness: 4.58, bite: 0, riseMs: 45.833, asyncMs: 13.834 }, + sawtooth: { brightness: 5.2978, bite: 0.7105, riseMs: 2, asyncMs: 0.289 }, + harp: { brightness: 7.7683, bite: 0.8995, riseMs: 0.25, asyncMs: 0.264 } +}; + +// public/aesthetic.computer/lib/melody-parser.mjs +var MELODY_WAVE_TYPES = [ + "sine", + "triangle", + "sawtooth", + "square", + "harp", + "whistle", + "noise-white", + // Parser modes rather than oscillators — sample playback, speech, and the + // generator/bubble hooks. + "sample", + "stample", + "say", + "custom", + "bubble" +]; +var MELODY_WAVE_ALIASES = { + saw: "sawtooth", + noise: "noise-white", + pluck: "harp", + guitar: "harp", + string: "harp", + flute: "whistle", + ocarina: "whistle" +}; +function normalizeWaveType(name) { + const canonical = MELODY_WAVE_ALIASES[name] || name; + return MELODY_WAVE_TYPES.includes(canonical) ? canonical : null; +} +function parseMelody(melodyString, startingOctave = 4) { + const notes = []; + let i2 = 0; + let currentOctave = startingOctave; + let baseOctave = startingOctave; + let relativeOffset = 0; + let currentWaveType = "sine"; + let currentVolume = 0.8; + let currentToneShift = 0; + let globalDurationModifier = null; + let isStruck = false; + let currentStampleCode = null; + let currentSayText = null; + function applyStickyDurationModifier(baseDuration, hasLocalModifier) { + if (hasLocalModifier) { + return baseDuration; + } + if (!globalDurationModifier) { + return baseDuration; + } + if (globalDurationModifier.startsWith(".")) { + const dots = globalDurationModifier.length; + return 2 / Math.pow(2, dots); + } else if (globalDurationModifier.startsWith(",")) { + const commas = globalDurationModifier.length; + return 2 * Math.pow(2, commas); + } + return baseDuration; + } + while (i2 < melodyString.length) { + let char = melodyString[i2]; + if (char === '"') { + let quotedText = ""; + let quoteStartIndex = i2; + i2++; + while (i2 < melodyString.length && melodyString[i2] !== '"') { + quotedText += melodyString[i2]; + i2++; + } + if (i2 < melodyString.length && melodyString[i2] === '"') { + const speechNote = { + note: "speech", + text: quotedText, + octave: currentOctave, + duration: 2, + // Default duration for speech + waveType: currentWaveType, + volume: currentVolume, + struck: isStruck, + toneShift: currentToneShift, + stampleCode: currentStampleCode, + sayText: currentSayText, + isSpeech: true + }; + notes.push(speechNote); + i2++; + continue; + } else { + i2 = quoteStartIndex; + } + } + if (char === "{") { + const endBrace = melodyString.indexOf("}", i2); + if (endBrace !== -1) { + const content = melodyString.substring(i2 + 1, endBrace); + const contentLower = content.toLowerCase(); + const quoteChar = content[0]; + const isQuoted = (quoteChar === '"' || quoteChar === "'" || quoteChar === "`") && content.includes(quoteChar, 1); + if (isQuoted) { + const closingQuote = content.indexOf(quoteChar, 1); + const speechText = content.slice(1, closingQuote); + const afterQuote = content.slice(closingQuote + 1); + if (afterQuote.startsWith(":")) { + const volume = parseFloat(afterQuote.slice(1)); + if (!isNaN(volume) && volume >= 0 && volume <= 1) { + currentVolume = volume; + } + } + currentWaveType = "say"; + currentSayText = speechText; + console.log(`\u{1F5E3}\uFE0F Parsed {say}: "${speechText}" - following notes will play speech at note pitch`); + } else if (content.startsWith("#")) { + const colonIndex = content.indexOf(":"); + let paintingCode, volumeStr; + if (colonIndex !== -1) { + paintingCode = content.slice(1, colonIndex); + volumeStr = content.slice(colonIndex + 1); + const volume = parseFloat(volumeStr); + if (!isNaN(volume) && volume >= 0 && volume <= 1) { + currentVolume = volume; + } + } else { + paintingCode = content.slice(1); + } + currentWaveType = "stample"; + currentStampleCode = paintingCode; + currentSayText = null; + console.log(`\u{1F3B5} Parsed painting code stample: #${paintingCode}${volumeStr ? `:${volumeStr}` : ""}`); + } else if (contentLower.endsWith("hz&") || contentLower.endsWith("hz")) { + const isCumulative = contentLower.endsWith("hz&"); + const hzPart = isCumulative ? contentLower.slice(0, -3) : contentLower.slice(0, -2); + const hzValue = parseFloat(hzPart); + if (!isNaN(hzValue)) { + if (isCumulative) { + currentToneShift = { value: hzValue, cumulative: true, step: hzValue }; + console.log(`\u{1F3B5} Parsed cumulative Hz shift: ${hzValue}Hz&`); + } else { + currentToneShift = hzValue; + console.log(`\u{1F3B5} Parsed Hz shift: ${hzValue}Hz`); + } + } else { + console.log(`\u{1F3B5} PARSER ERROR: Failed to parse Hz value: {${content}}`); + } + } else if (contentLower.includes(":")) { + const [waveType, volumeStr] = contentLower.split(":"); + const normalizedWaveType = normalizeWaveType(waveType); + if (normalizedWaveType) { + currentWaveType = normalizedWaveType; + currentStampleCode = null; + if (normalizedWaveType !== "say") currentSayText = null; + } + const volume = parseFloat(volumeStr); + if (!isNaN(volume) && volume >= 0 && volume <= 1) { + currentVolume = volume; + } + } else if (/^\d*\.?\d+$/.test(contentLower)) { + const volume = parseFloat(contentLower); + if (!isNaN(volume) && volume >= 0 && volume <= 1) { + currentVolume = volume; + } + } else if (normalizeWaveType(contentLower)) { + currentWaveType = normalizeWaveType(contentLower); + if (currentWaveType !== "stample" && currentWaveType !== "sample") { + currentStampleCode = null; + } + if (currentWaveType !== "say") { + currentSayText = null; + } + } + i2 = endBrace + 1; + continue; + } + i2++; + continue; + } + if (char === "^") { + isStruck = !isStruck; + i2++; + continue; + } + if (/[0-9]/.test(char)) { + const octave = parseInt(char); + currentOctave = octave; + baseOctave = octave; + relativeOffset = 0; + i2++; + if (i2 < melodyString.length) { + const noteChar = melodyString[i2].toLowerCase(); + if (/[a-g]/.test(noteChar)) { + let note2 = noteChar; + i2++; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === "s" || nextChar === "#") { + note2 += "s"; + i2++; + } else if (nextChar === "b" && i2 + 1 < melodyString.length && !/[a-g]/i.test(melodyString[i2 + 1])) { + note2 += "b"; + i2++; + } + } + let sonicExtension = 0; + if (i2 < melodyString.length && melodyString[i2] === "'") { + while (i2 < melodyString.length && melodyString[i2] === "'") { + sonicExtension++; + i2++; + } + } + let duration = 2; + let sonicDuration = duration; + let hasLocalModifier = false; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === ".") { + hasLocalModifier = true; + let dots = 0; + while (i2 < melodyString.length && melodyString[i2] === ".") { + dots++; + i2++; + } + duration = 2 / Math.pow(2, dots); + globalDurationModifier = ".".repeat(dots); + } else if (nextChar === ",") { + hasLocalModifier = true; + let commas = 0; + while (i2 < melodyString.length && melodyString[i2] === ",") { + commas++; + i2++; + } + duration = 2 * Math.pow(2, commas); + globalDurationModifier = ",".repeat(commas); + } + } + if (!hasLocalModifier) { + duration = applyStickyDurationModifier(duration, false); + } + if (sonicExtension > 0) { + sonicDuration = duration * Math.pow(2, sonicExtension); + } else { + sonicDuration = duration; + } + const noteObj = { + note: note2, + octave: currentOctave, + duration, + waveType: currentWaveType, + volume: currentVolume, + struck: isStruck, + toneShift: currentToneShift, + stampleCode: currentStampleCode, + sayText: currentSayText + // Speech text for {say} waveform type + }; + if (sonicDuration !== duration) { + noteObj.sonicDuration = sonicDuration; + } + notes.push(noteObj); + } else { + } + } + } else if (char === "[" || char === "]") { + const swingType = char; + let swingCount = 0; + while (i2 < melodyString.length && melodyString[i2] === swingType) { + swingCount++; + i2++; + } + if (i2 < melodyString.length) { + const noteChar = melodyString[i2].toLowerCase(); + if (/[a-g]/.test(noteChar)) { + let note2 = noteChar; + i2++; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === "#") { + note2 += "s"; + i2++; + } else if (nextChar === "b" && i2 + 1 < melodyString.length && !/[a-g]/i.test(melodyString[i2 + 1])) { + note2 += "b"; + i2++; + } + } + let sonicExtension = 0; + if (i2 < melodyString.length && melodyString[i2] === "'") { + while (i2 < melodyString.length && melodyString[i2] === "'") { + sonicExtension++; + i2++; + } + } + let octave = currentOctave; + let duration = 2; + let sonicDuration = duration; + let swing = swingType === "[" ? "early" : "late"; + let swingAmount = swingCount; + let hasLocalModifier = false; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === ".") { + hasLocalModifier = true; + let dots = 0; + while (i2 < melodyString.length && melodyString[i2] === ".") { + dots++; + i2++; + } + duration = 2 / Math.pow(2, dots); + } else if (nextChar === ",") { + hasLocalModifier = true; + let commas = 0; + while (i2 < melodyString.length && melodyString[i2] === ",") { + commas++; + i2++; + } + duration = 2 * Math.pow(2, commas); + } + } + if (!hasLocalModifier) { + duration = applyStickyDurationModifier(duration, false); + } + if (sonicExtension > 0) { + sonicDuration = duration * Math.pow(2, sonicExtension); + } else { + sonicDuration = duration; + } + const noteObj = { + note: note2, + octave, + duration, + swing, + swingAmount, + waveType: currentWaveType, + volume: currentVolume, + struck: isStruck, + toneShift: currentToneShift, + stampleCode: currentStampleCode, + sayText: currentSayText + }; + if (sonicDuration !== duration) { + noteObj.sonicDuration = sonicDuration; + } + notes.push(noteObj); + } + } + } else if (/[a-g+-]/.test(char.toLowerCase())) { + let relativeModifier = ""; + if (char === "+" || char === "-") { + relativeModifier = char; + i2++; + while (i2 < melodyString.length && melodyString[i2] === char) { + relativeModifier += char; + i2++; + } + if (i2 >= melodyString.length || !/[a-g]/.test(melodyString[i2].toLowerCase())) { + if (char === "-") { + let commas = relativeModifier.length; + let duration2 = 2 * Math.pow(2, commas); + notes.push({ note: "rest", octave: currentOctave, duration: duration2, waveType: currentWaveType, volume: currentVolume, struck: isStruck, toneShift: currentToneShift, stampleCode: currentStampleCode, sayText: currentSayText }); + continue; + } else { + continue; + } + } + char = melodyString[i2]; + } + let note2 = char.toLowerCase(); + i2++; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === "#") { + note2 += "s"; + i2++; + } else if (nextChar === "b" && i2 + 1 < melodyString.length && !/[a-g]/i.test(melodyString[i2 + 1])) { + note2 += "b"; + i2++; + } + } + let sonicExtension = 0; + if (i2 < melodyString.length && melodyString[i2] === "'") { + while (i2 < melodyString.length && melodyString[i2] === "'") { + sonicExtension++; + i2++; + } + } + let octave; + if (relativeModifier) { + let modifierOffset = 0; + if (relativeModifier.startsWith("+")) { + modifierOffset = relativeModifier.length; + relativeOffset += modifierOffset; + } else if (relativeModifier.startsWith("-")) { + modifierOffset = -relativeModifier.length; + relativeOffset += modifierOffset; + } + octave = baseOctave + relativeOffset; + currentOctave = octave; + } else { + octave = currentOctave; + } + let duration = 2; + let sonicDuration = duration; + let hasLocalModifier = false; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === ".") { + hasLocalModifier = true; + let dots = 0; + while (i2 < melodyString.length && melodyString[i2] === ".") { + dots++; + i2++; + } + duration = 2 / Math.pow(2, dots); + globalDurationModifier = ".".repeat(dots); + } else if (nextChar === ",") { + hasLocalModifier = true; + let commas = 0; + while (i2 < melodyString.length && melodyString[i2] === ",") { + commas++; + i2++; + } + duration = 2 * Math.pow(2, commas); + globalDurationModifier = ",".repeat(commas); + } + } + if (!hasLocalModifier) { + duration = applyStickyDurationModifier(duration, false); + } + if (sonicExtension > 0) { + sonicDuration = duration * Math.pow(2, sonicExtension); + } else { + sonicDuration = duration; + } + const noteObj = { + note: note2, + octave, + duration, + waveType: currentWaveType, + volume: currentVolume, + struck: isStruck, + toneShift: currentToneShift, + stampleCode: currentStampleCode, + sayText: currentSayText + }; + if (sonicDuration !== duration) { + noteObj.sonicDuration = sonicDuration; + } + notes.push(noteObj); + } else if (char === "_") { + let duration = 2; + let hasLocalModifier = false; + i2++; + if (i2 < melodyString.length) { + const nextChar = melodyString[i2]; + if (nextChar === ".") { + hasLocalModifier = true; + let dots = 0; + while (i2 < melodyString.length && melodyString[i2] === ".") { + dots++; + i2++; + } + duration = 2 / Math.pow(2, dots); + globalDurationModifier = ".".repeat(dots); + } else if (nextChar === ",") { + hasLocalModifier = true; + let commas = 0; + while (i2 < melodyString.length && melodyString[i2] === ",") { + commas++; + i2++; + } + duration = 2 * Math.pow(2, commas); + globalDurationModifier = ",".repeat(commas); + } + } + if (!hasLocalModifier) { + duration = applyStickyDurationModifier(duration, false); + } + notes.push({ note: "rest", octave: currentOctave, duration, waveType: currentWaveType, volume: currentVolume, struck: isStruck, toneShift: currentToneShift, stampleCode: currentStampleCode, sayText: currentSayText }); + } else if (char === " ") { + i2++; + } else if (char === "-") { + let dashes = 0; + let tempI = i2; + while (tempI < melodyString.length && melodyString[tempI] === "-") { + dashes++; + tempI++; + } + let duration = 2 * Math.pow(2, dashes); + i2 = tempI; + notes.push({ note: "rest", octave: currentOctave, duration, waveType: currentWaveType, volume: currentVolume, struck: isStruck, toneShift: currentToneShift, stampleCode: currentStampleCode, sayText: currentSayText }); + } else if (char === "|") { + i2++; + } else if (char === "~") { + i2++; + } else { + i2++; + } + } + return notes; +} +function noteToTone(note2, octave = 4) { + const noteMap = { + "c": "C", + "cs": "C#", + "c#": "C#", + "db": "C#", + "d": "D", + "ds": "D#", + "d#": "D#", + "eb": "D#", + "e": "E", + "f": "F", + "fs": "F#", + "f#": "F#", + "gb": "F#", + "g": "G", + "gs": "G#", + "g#": "G#", + "ab": "G#", + "a": "A", + "as": "A#", + "a#": "A#", + "bb": "A#", + "b": "B" + }; + const normalizedNote = note2.toLowerCase(); + const baseNote = noteMap[normalizedNote] || "C"; + const finalOctave = octave !== null ? octave : 4; + return `${finalOctave}${baseNote}`; +} +function parseSimultaneousMelody(melodyString, startingOctave = 4) { + let globalWaveType = "sine"; + let processedMelodyString = melodyString.trim(); + const globalWaveTypePattern = /^{(sine|sawtooth|saw|square|triangle|noise-white|sample|stample|custom|bubble)}/; + const waveMatch = globalWaveTypePattern.exec(processedMelodyString); + if (waveMatch) { + globalWaveType = waveMatch[1].toLowerCase(); + processedMelodyString = processedMelodyString.substring(waveMatch[0].length).trim(); + } + const groups = []; + let currentGroup = ""; + let braceDepth = 0; + for (let i2 = 0; i2 < processedMelodyString.length; i2++) { + const char = processedMelodyString[i2]; + if (char === "{") { + braceDepth++; + currentGroup += char; + } else if (char === "}") { + braceDepth--; + currentGroup += char; + } else if (/\s/.test(char) && braceDepth === 0) { + if (currentGroup.length > 0) { + groups.push(currentGroup); + currentGroup = ""; + } + } else { + currentGroup += char; + } + } + if (currentGroup.length > 0) { + groups.push(currentGroup); + } + console.log("\u{1F3B5} MELODY PARSER: Splitting melody into groups:", { + original: processedMelodyString, + groups, + groupCount: groups.length + }); + if (groups.length <= 1) { + const singleGroup = groups[0] || ""; + const asteriskIndices = []; + for (let i2 = 0; i2 < singleGroup.length; i2++) { + if (singleGroup[i2] === "*") { + asteriskIndices.push(i2); + } + } + const hasMutation = asteriskIndices.length > 0; + let contentForParsing = singleGroup; + let mutationTriggerPositions = []; + if (hasMutation) { + contentForParsing = singleGroup.replace(/\*/g, ""); + mutationTriggerPositions = asteriskIndices.map((asteriskIndex) => { + let noteCount = 0; + for (let i2 = 0; i2 < asteriskIndex; i2++) { + const char = singleGroup[i2]; + if (char === "{") { + while (i2 < singleGroup.length && singleGroup[i2] !== "}") { + i2++; + } + if (i2 < singleGroup.length && singleGroup[i2] === "}") { + i2++; + } + i2--; + } else if (/[a-g_-]/.test(char) || char.match(/[0-9]/) && i2 + 1 < singleGroup.length && /[a-g]/.test(singleGroup[i2 + 1])) { + noteCount++; + } + } + return noteCount; + }); + } + const hasLocalWaveType = /{(sine|sawtooth|saw|square|triangle|noise-white|sample|stample|custom|bubble)}/.test(contentForParsing); + const contentWithGlobalWave = hasLocalWaveType ? contentForParsing : globalWaveType !== "sine" ? `{${globalWaveType}} ${contentForParsing}` : contentForParsing; + const parsedTrack = parseMelody(contentWithGlobalWave, startingOctave); + if (hasMutation) { + parsedTrack.hasMutation = true; + parsedTrack.originalContent = contentForParsing; + parsedTrack.mutationCount = 0; + parsedTrack.mutationTriggerPositions = mutationTriggerPositions; + parsedTrack.currentMutationZone = 0; + parsedTrack.mutationType = "within-group"; + if (mutationTriggerPositions.length === 1) { + parsedTrack.mutationTriggerPosition = mutationTriggerPositions[0]; + } + } + return { + tracks: [parsedTrack], + isSingleTrack: true, + type: "single" + }; + } + const parallelTracks = groups.map((groupContent, index) => { + const isDisabled = groupContent.startsWith("x"); + let processingContent = isDisabled ? groupContent.slice(1) : groupContent; + let contentForParsing = processingContent; + let mutationTriggerPositions = []; + let hasMutation = false; + const asteriskIndices = []; + for (let i2 = 0; i2 < processingContent.length; i2++) { + if (processingContent[i2] === "*") { + asteriskIndices.push(i2); + } + } + if (asteriskIndices.length > 0) { + hasMutation = true; + contentForParsing = processingContent.replace(/\*/g, ""); + mutationTriggerPositions = asteriskIndices.map((asteriskIndex) => { + let noteCount = 0; + for (let i2 = 0; i2 < asteriskIndex; i2++) { + const char = processingContent[i2]; + if (char === "{") { + while (i2 < processingContent.length && processingContent[i2] !== "}") { + i2++; + } + if (i2 < processingContent.length && processingContent[i2] === "}") { + i2++; + } + i2--; + } else if (/[a-g_-]/.test(char) || char.match(/[0-9]/) && i2 + 1 < processingContent.length && /[a-g]/.test(processingContent[i2 + 1])) { + noteCount++; + } + } + return noteCount; + }); + } + const hasLocalWaveType = /{(sine|sawtooth|saw|square|triangle|noise-white|sample|stample|custom|bubble)}/.test(contentForParsing); + const contentWithGlobalWave = hasLocalWaveType ? contentForParsing : `{${globalWaveType}} ${contentForParsing}`; + const parsedTrack = parseMelody(contentWithGlobalWave, startingOctave); + parsedTrack.isDisabled = isDisabled; + parsedTrack.hasMutation = hasMutation; + if (parsedTrack.hasMutation) { + parsedTrack.originalContent = contentForParsing; + parsedTrack.mutationCount = 0; + if (mutationTriggerPositions.length > 0) { + parsedTrack.mutationTriggerPositions = mutationTriggerPositions; + parsedTrack.currentMutationZone = 0; + parsedTrack.mutationType = "within-group"; + if (mutationTriggerPositions.length === 1) { + parsedTrack.mutationTriggerPosition = mutationTriggerPositions[0]; + } + } + } + return parsedTrack; + }); + const enabledTracks = parallelTracks.filter((track) => !track.isDisabled); + return { + tracks: enabledTracks, + isSingleTrack: false, + type: "parallel", + trackCount: enabledTracks.length, + maxLength: Math.max(...enabledTracks.map((track) => track.length)) + }; +} +function parseSequentialMelody(melodyString, startingOctave = 4) { + let hasSequenceSeparator = false; + let braceDepth = 0; + for (let i2 = 0; i2 < melodyString.length; i2++) { + const char = melodyString[i2]; + if (char === "{") braceDepth++; + else if (char === "}") braceDepth--; + else if (char === ">" && braceDepth === 0) { + hasSequenceSeparator = true; + break; + } + } + if (!hasSequenceSeparator) { + return parseSimultaneousMelody(melodyString, startingOctave); + } + const sequencePattern = /(\d*)>/g; + const parts = melodyString.split(sequencePattern); + const sequences = []; + let currentLoopCount = 1; + for (let i2 = 0; i2 < parts.length; i2++) { + const part = parts[i2]; + if (!part || part.trim().length === 0) continue; + if (/^\d+$/.test(part)) { + currentLoopCount = parseInt(part, 10) || 1; + continue; + } + const segmentContent = part.trim(); + if (segmentContent.length === 0) continue; + const parsedSegment = parseSimultaneousMelody(segmentContent, startingOctave); + let maxDurationBeats = 0; + const tracks = parsedSegment.tracks || [parsedSegment.notes]; + tracks.forEach((track) => { + if (track && track.length > 0) { + const trackDuration = track.reduce((sum, note2) => sum + (note2.duration || 2), 0); + maxDurationBeats = Math.max(maxDurationBeats, trackDuration); + } + }); + sequences.push({ + ...parsedSegment, + loopCount: currentLoopCount, + currentLoop: 0, + durationBeats: maxDurationBeats, + originalContent: segmentContent + }); + currentLoopCount = 1; + } + if (sequences.length <= 1) { + if (sequences.length === 1) { + return sequences[0]; + } + return parseSimultaneousMelody(melodyString, startingOctave); + } + return { + type: "sequential", + sequences, + currentSequence: 0, + totalSequences: sequences.length + }; +} +function buildSequenceState(seqParsed) { + if (!seqParsed) return null; + if (seqParsed.type === "parallel") { + return { + type: "parallel", + tracks: seqParsed.tracks, + trackStates: seqParsed.tracks.map((track, i2) => ({ + trackIndex: i2, + track, + noteIndex: 0, + nextNoteTargetTime: 0, + measurePosition: 0 + })) + }; + } + const notes = seqParsed.tracks ? seqParsed.tracks[0] : seqParsed.notes || []; + return { type: "single", notes, index: 0, nextNoteTargetTime: 0, measurePosition: 0 }; +} +function sequenceDurationBeats(seqParsed) { + if (!seqParsed) return 0; + const tracks = seqParsed.type === "parallel" ? seqParsed.tracks : [seqParsed.tracks ? seqParsed.tracks[0] : seqParsed.notes || []]; + let maxBeats = 0; + for (const tr of tracks) { + let b2 = 0; + for (const n2 of tr || []) b2 += n2.duration || 1; + if (b2 > maxBeats) maxBeats = b2; + } + return maxBeats; +} +function buildMelodyState(parsed, { baseTempo = 500 } = {}) { + if (!parsed) return null; + if (parsed.type === "sequential") { + return { + type: "sequential", + sequences: parsed.sequences, + currentSequence: 0, + currentSequenceState: buildSequenceState(parsed.sequences[0]), + baseTempo, + isFallback: false + }; + } + if (parsed.type === "parallel") { + return { + type: "parallel", + tracks: parsed.tracks, + trackStates: parsed.tracks.map((track, i2) => ({ + trackIndex: i2, + track, + noteIndex: 0, + nextNoteTargetTime: 0, + measurePosition: 0 + })), + baseTempo, + isFallback: false + }; + } + const notes = parsed.tracks ? parsed.tracks[0] : parsed.notes || []; + return { + type: "single", + notes, + index: 0, + nextNoteTargetTime: 0, + measurePosition: 0, + baseTempo, + isFallback: !!parsed.isFallback + }; +} +var P_CENTER_FRACTION = 0.6; +var P_CENTER_EPSILON_MS = 5e-3; +function perceivedOnsetLagMs(waveType) { + const t2 = WAVE_TIMBRE[waveType]; + return t2 ? t2.riseMs * P_CENTER_FRACTION : 0; +} +function pCenterShiftsMs(notes) { + const track = Array.isArray(notes) ? notes : notes?.notes || notes?.tracks?.[0] || []; + if (!track.length) return []; + const lags = track.map((n2) => perceivedOnsetLagMs(n2?.waveType)); + const sounding = track.map((n2, i2) => n2 && n2.note && n2.note !== "rest" && n2.note !== "_" ? lags[i2] : null).filter((v2) => v2 !== null); + if (!sounding.length) return track.map(() => 0); + const mean = sounding.reduce((a2, b2) => a2 + b2, 0) / sounding.length; + return lags.map((lag) => { + const shift = -(lag - mean); + return Math.abs(shift) < P_CENTER_EPSILON_MS ? 0 : shift; + }); +} +function applyPCenterShifts(starts, notes) { + const shifts = pCenterShiftsMs(notes); + if (shifts.length !== starts.length) return starts.slice(); + const out = new Array(starts.length); + let floorMs = 0; + for (let i2 = 0; i2 < starts.length; i2 += 1) { + out[i2] = Math.max(floorMs, starts[i2] + (shifts[i2] || 0)); + floorMs = out[i2]; + } + return out; +} + +// public/aesthetic.computer/lib/note-colors.mjs +var NOTE_COLOR_MAP_BASE = { + c: [255, 50, 50], + // Bright red + d: [255, 160, 0], + // Vivid orange + e: [255, 230, 0], + // Bright yellow + f: [50, 200, 50], + // Vivid green + g: [50, 120, 255], + // Bright blue + a: [130, 50, 200], + // Vivid purple + b: [180, 80, 255] + // Bright violet +}; +var NOTE_COLOR_BLACK = [0, 0, 0]; +var NOTE_COLOR_MAP = { + ...NOTE_COLOR_MAP_BASE, + // Sharps/flats are all black + "c#": NOTE_COLOR_BLACK, + cs: NOTE_COLOR_BLACK, + db: NOTE_COLOR_BLACK, + "d#": NOTE_COLOR_BLACK, + ds: NOTE_COLOR_BLACK, + eb: NOTE_COLOR_BLACK, + "f#": NOTE_COLOR_BLACK, + fs: NOTE_COLOR_BLACK, + gb: NOTE_COLOR_BLACK, + "g#": NOTE_COLOR_BLACK, + gs: NOTE_COLOR_BLACK, + ab: NOTE_COLOR_BLACK, + "a#": NOTE_COLOR_BLACK, + as: NOTE_COLOR_BLACK, + bb: NOTE_COLOR_BLACK +}; +function normalizeNoteName(noteName) { + if (!noteName) return null; + if (noteName === "rest") return null; + if (noteName === "speech") return "speech"; + const clean = noteName.toLowerCase().replace(/[0-9]/g, ""); + return clean; +} +function getNoteColor(noteName) { + if (!noteName || noteName === "rest") return [102, 102, 102]; + if (noteName === "speech") return [255, 200, 100]; + const cleanNote = normalizeNoteName(noteName); + return NOTE_COLOR_MAP[cleanNote] || [255, 255, 255]; +} + +// public/aesthetic.computer/lib/melody-highlighter.mjs +var _now = () => typeof performance !== "undefined" ? performance.now() : Date.now(); +function hsvToRgb(h, s2, v2) { + const c4 = v2 * s2; + const x = c4 * (1 - Math.abs(h / 60 % 2 - 1)); + const m = v2 - c4; + let r2, g, b2; + if (h >= 0 && h < 60) { + r2 = c4; + g = x; + b2 = 0; + } else if (h >= 60 && h < 120) { + r2 = x; + g = c4; + b2 = 0; + } else if (h >= 120 && h < 180) { + r2 = 0; + g = c4; + b2 = x; + } else if (h >= 180 && h < 240) { + r2 = 0; + g = x; + b2 = c4; + } else if (h >= 240 && h < 300) { + r2 = x; + g = 0; + b2 = c4; + } else { + r2 = c4; + g = 0; + b2 = x; + } + return [ + Math.round((r2 + m) * 255), + Math.round((g + m) * 255), + Math.round((b2 + m) * 255) + ]; +} +function computeCurrentNoteIndex(melodyState, trackIndex = 0, timingHasStarted = true) { + if (!timingHasStarted || !melodyState) return -1; + if (melodyState.type === "single" && melodyState.notes) { + const totalNotes = melodyState.notes.length; + return (melodyState.index - 1 + totalNotes) % totalNotes; + } else if (melodyState.type === "parallel" && melodyState.trackStates) { + const trackState = melodyState.trackStates[trackIndex]; + if (trackState && trackState.track) { + const totalNotes = trackState.track.length; + return (trackState.noteIndex - 1 + totalNotes) % totalNotes; + } + } + return -1; +} +function buildColoredMelodyString(melodyString, melodyState, opts = {}) { + if (!melodyString) return ""; + const now = opts.now ?? _now(); + const timingHasStarted = opts.timingHasStarted ?? false; + const getCurrentNoteIndex = opts.getCurrentNoteIndex || ((s2, t2 = 0) => computeCurrentNoteIndex(s2, t2, timingHasStarted)); + const { + noteIndex: recentlyMutatedNoteIndex = -1, + trackIndex: recentlyMutatedTrackIndex = -1 + } = opts.recentlyMutated || {}; + const shouldFlashGreen = opts.specialCharFlash ?? false; + const shouldFlashMutation = opts.mutationFlash?.active ?? false; + const triggeredAsteriskPositions = opts.triggeredAsteriskPositions || []; + const getStampleStatus = opts.getStampleStatus || (() => null); + let coloredMelodyString = ""; + let noteCharPositions = []; + let noteIndex = 0; + let inWaveform = false; + let currentNoteIndex = 0; + let effectiveMelodyState = melodyState; + let currentSequenceIndex = 0; + let isSequentialMelody = melodyState && melodyState.type === "sequential"; + if (isSequentialMelody) { + currentSequenceIndex = melodyState.currentSequence ?? 0; + if (melodyState.currentSequenceState) { + effectiveMelodyState = melodyState.currentSequenceState; + } else if (melodyState.sequences && melodyState.sequences.length > 0) { + effectiveMelodyState = melodyState.sequences[0].parsed; + } + } + if (timingHasStarted && effectiveMelodyState && effectiveMelodyState.type === "single") { + currentNoteIndex = getCurrentNoteIndex(effectiveMelodyState); + } else if (timingHasStarted && effectiveMelodyState && effectiveMelodyState.type === "parallel") { + } else { + currentNoteIndex = -1; + } + let sequences = []; + let sequenceDelimiterPositions = []; + if (isSequentialMelody) { + let lastSplitPos = 0; + for (let i2 = 0; i2 < melodyString.length; i2++) { + if (melodyString[i2] === ">") { + sequences.push(melodyString.substring(lastSplitPos, i2).trim()); + sequenceDelimiterPositions.push(i2); + lastSplitPos = i2 + 1; + } + } + sequences.push(melodyString.substring(lastSplitPos).trim()); + } + const groups = melodyString.trim().split(/\s+/); + let groupStartPositions = []; + let searchStart = 0; + for (let groupIdx = 0; groupIdx < groups.length; groupIdx++) { + const group = groups[groupIdx]; + const groupStart = melodyString.indexOf(group, searchStart); + groupStartPositions.push(groupStart); + searchStart = groupStart + group.length; + } + let effectiveGroupToTrackMap = []; + let currentSeqIdx = 0; + let currentTrackInSeq = 0; + if (isSequentialMelody) { + for (let groupIdx = 0; groupIdx < groups.length; groupIdx++) { + const group = groups[groupIdx]; + if (/^\d*>$/.test(group)) { + currentSeqIdx++; + currentTrackInSeq = 0; + effectiveGroupToTrackMap.push({ + isDelimiter: true, + sequenceIndex: currentSeqIdx - 1 + }); + } else { + effectiveGroupToTrackMap.push({ + isDelimiter: false, + sequenceIndex: currentSeqIdx, + trackIndex: currentTrackInSeq, + isCurrentSequence: currentSeqIdx === currentSequenceIndex + }); + currentTrackInSeq++; + } + } + } + for (let groupIdx = 0; groupIdx < groups.length; groupIdx++) { + const group = groups[groupIdx]; + const groupStartChar = groupStartPositions[groupIdx]; + noteIndex = 0; + let trackIdxForColoring = groupIdx; + let isInCurrentSequence = true; + if (isSequentialMelody && effectiveGroupToTrackMap[groupIdx]) { + const mapping = effectiveGroupToTrackMap[groupIdx]; + if (mapping.isDelimiter) continue; + trackIdxForColoring = mapping.trackIndex; + isInCurrentSequence = mapping.isCurrentSequence; + } + for (let i2 = 0; i2 < group.length; i2++) { + const char = group[i2]; + if (char === "{") { + inWaveform = true; + continue; + } else if (char === "}") { + inWaveform = false; + continue; + } else if (inWaveform) { + continue; + } + if (/[0-9]/.test(char) && i2 + 1 < group.length && /[a-gvswrqhijklmntyuop]/i.test(group[i2 + 1])) { + let noteStart = i2; + let noteEnd = i2 + 1; + if (noteEnd + 1 < group.length) { + if (group[noteEnd + 1] === "#") noteEnd++; + } + while (noteEnd + 1 < group.length) { + const nextChar = group[noteEnd + 1]; + if (nextChar === "." || nextChar === "-" || nextChar === "[" || nextChar === "]" || nextChar === "," || nextChar === "*") noteEnd++; + else break; + } + const noteIndexToUse = noteIndex; + const trackIndexToUse = isSequentialMelody ? trackIdxForColoring : effectiveMelodyState && effectiveMelodyState.type === "parallel" ? groupIdx : 0; + for (let j = noteStart; j <= noteEnd; j++) { + noteCharPositions.push({ + charIndex: groupStartChar + j, + noteIndex: noteIndexToUse, + trackIndex: trackIndexToUse, + isInCurrentSequence + }); + } + noteIndex++; + i2 = noteEnd; + } else if (char === "^") { + continue; + } else if (char === "-") { + let dashEnd = i2; + while (dashEnd + 1 < group.length && group[dashEnd + 1] === "-") dashEnd++; + if (dashEnd + 1 < group.length && /[a-gvswrqhijklmntyuop]/i.test(group[dashEnd + 1])) { + let noteStart = i2; + let noteEnd = dashEnd + 1; + if (noteEnd + 1 < group.length && group[noteEnd + 1] === "#") noteEnd++; + while (noteEnd + 1 < group.length) { + const nextChar = group[noteEnd + 1]; + if (nextChar === "." || nextChar === "," || nextChar === "*" || nextChar === "[" || nextChar === "]") noteEnd++; + else break; + } + const noteIndexToUse = noteIndex; + const trackIndexToUse = isSequentialMelody ? trackIdxForColoring : effectiveMelodyState && effectiveMelodyState.type === "parallel" ? groupIdx : 0; + for (let j = noteStart; j <= noteEnd; j++) { + noteCharPositions.push({ + charIndex: groupStartChar + j, + noteIndex: noteIndexToUse, + trackIndex: trackIndexToUse, + isInCurrentSequence + }); + } + noteIndex++; + i2 = noteEnd; + } else { + i2 = dashEnd; + continue; + } + } else if (char === "+") { + let plusEnd = i2; + while (plusEnd + 1 < group.length && group[plusEnd + 1] === "+") plusEnd++; + if (plusEnd + 1 < group.length && /[a-gvswrqhijklmntyuop]/i.test(group[plusEnd + 1])) { + let noteStart = i2; + let noteEnd = plusEnd + 1; + if (noteEnd + 1 < group.length && group[noteEnd + 1] === "#") noteEnd++; + while (noteEnd + 1 < group.length) { + const nextChar = group[noteEnd + 1]; + if (nextChar === "." || nextChar === "," || nextChar === "*" || nextChar === "[" || nextChar === "]") noteEnd++; + else break; + } + const noteIndexToUse = noteIndex; + const trackIndexToUse = isSequentialMelody ? trackIdxForColoring : effectiveMelodyState && effectiveMelodyState.type === "parallel" ? groupIdx : 0; + for (let j = noteStart; j <= noteEnd; j++) { + noteCharPositions.push({ + charIndex: groupStartChar + j, + noteIndex: noteIndexToUse, + trackIndex: trackIndexToUse, + isInCurrentSequence + }); + } + noteIndex++; + i2 = noteEnd; + } else { + i2 = plusEnd; + continue; + } + } else if (/[a-g#_vswrqhijklmntyuop]/i.test(char)) { + let noteStart = i2; + let noteEnd = i2; + if (noteEnd + 1 < group.length) { + if (group[noteEnd + 1] === "#") noteEnd++; + } + while (noteEnd + 1 < group.length) { + const nextChar = group[noteEnd + 1]; + if (nextChar === "." || nextChar === "-" || nextChar === "[" || nextChar === "]" || nextChar === "," || nextChar === "*") noteEnd++; + else break; + } + const noteIndexToUse = noteIndex; + const trackIndexToUse = isSequentialMelody ? trackIdxForColoring : effectiveMelodyState && effectiveMelodyState.type === "parallel" ? groupIdx : 0; + for (let j = noteStart; j <= noteEnd; j++) { + noteCharPositions.push({ + charIndex: groupStartChar + j, + noteIndex: noteIndexToUse, + trackIndex: trackIndexToUse, + isInCurrentSequence + }); + } + noteIndex++; + i2 = noteEnd; + } + } + } + let inWaveformForColoring = false; + function getRedNoteColor() { + return "red"; + } + function getStaticNoteColor(noteCharData) { + if (!noteCharData) return "gray"; + const stateToUse = effectiveMelodyState || melodyState; + if (!stateToUse) return "gray"; + let note2 = null; + if (stateToUse.type === "single" && stateToUse.notes) { + note2 = stateToUse.notes[noteCharData.noteIndex]; + } else if (stateToUse.type === "parallel" && stateToUse.tracks) { + const track = stateToUse.tracks[noteCharData.trackIndex]; + note2 = track && track[noteCharData.noteIndex]; + } + if (!note2) return "gray"; + const rgb = getNoteColor(note2.note); + return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + } + function isNoteMutated(noteCharData) { + if (!noteCharData) return false; + const stateToUse = effectiveMelodyState || melodyState; + if (!stateToUse) return false; + if (stateToUse.type === "single") { + const currentNote = stateToUse.notes && stateToUse.notes[noteCharData.noteIndex]; + return currentNote && currentNote.isMutation; + } else if (stateToUse.type === "parallel" && stateToUse.trackStates && noteCharData.trackIndex < stateToUse.trackStates.length) { + const track = stateToUse.tracks[noteCharData.trackIndex]; + const currentNote = track && track[noteCharData.noteIndex]; + return currentNote && currentNote.isMutation; + } + return false; + } + function getMutatedNoteColor(isCurrentlyPlaying = false, noteCharData = null) { + const isRecentlyMutatedNote = noteCharData && noteCharData.noteIndex === recentlyMutatedNoteIndex && noteCharData.trackIndex === recentlyMutatedTrackIndex; + const shouldFlashForThisNote = shouldFlashMutation && isRecentlyMutatedNote; + if (shouldFlashForThisNote) { + const time = now * 5e-3; + const hue = time % 1 * 360; + return hsvToRgb(hue, 1, 1); + } + if (isCurrentlyPlaying) return getRedNoteColor(); + return "goldenrod"; + } + for (let i2 = 0; i2 < melodyString.length; i2++) { + const char = melodyString[i2]; + let color3 = "yellow"; + if (char === ">") { + const prevChar = i2 > 0 ? melodyString[i2 - 1] : " "; + if (/\d/.test(prevChar)) color3 = "white"; + else color3 = "cyan"; + coloredMelodyString += `\\${color3}\\${char}`; + continue; + } + if (/\d/.test(char)) { + let lookAhead = i2 + 1; + while (lookAhead < melodyString.length && /\d/.test(melodyString[lookAhead])) lookAhead++; + if (lookAhead < melodyString.length && melodyString[lookAhead] === ">") { + color3 = "magenta"; + coloredMelodyString += `\\${color3}\\${char}`; + continue; + } + } + if (char === " ") { + color3 = timingHasStarted ? "yellow" : "gray"; + } else if (char === "x") { + let isGroupPrefix = false; + if (i2 === 0) { + isGroupPrefix = true; + } else { + let j = i2 - 1; + while (j >= 0 && melodyString[j] === " ") j--; + isGroupPrefix = j < 0 || melodyString[j] === " "; + } + if (isGroupPrefix) { + color3 = "brown"; + } else { + color3 = timingHasStarted ? "yellow" : "gray"; + } + } else { + let isInDisabledGroup = false; + { + let charCount = 0; + const groups2 = melodyString.trim().split(/\s+/); + for (let groupIdx = 0; groupIdx < groups2.length; groupIdx++) { + const group = groups2[groupIdx]; + const groupStart = melodyString.indexOf(group, charCount); + const groupEnd = groupStart + group.length; + if (i2 >= groupStart && i2 < groupEnd) { + isInDisabledGroup = group.startsWith("x"); + break; + } + charCount = groupEnd; + } + } + if (isInDisabledGroup) { + color3 = "gray"; + } else { + const noteCharData = noteCharPositions.find((ncp) => ncp.charIndex === i2); + let isCurrentlyPlayingNote = false; + if (noteCharData) { + if (melodyState && melodyState.type === "single") { + isCurrentlyPlayingNote = noteCharData.noteIndex === currentNoteIndex; + } else if (melodyState && melodyState.type === "parallel" && melodyState.trackStates && noteCharData.trackIndex < melodyState.trackStates.length) { + const currentPlayingIndex = getCurrentNoteIndex( + melodyState, + noteCharData.trackIndex + ); + isCurrentlyPlayingNote = noteCharData.noteIndex === currentPlayingIndex; + } else if (melodyState && melodyState.type === "sequential" && melodyState.currentSequenceState) { + if (noteCharData.isInCurrentSequence) { + const seqState = melodyState.currentSequenceState; + if (seqState.type === "single" && seqState.notes) { + const totalNotes = seqState.notes.length; + const currentPlayingIndex = (seqState.index - 1 + totalNotes) % totalNotes; + isCurrentlyPlayingNote = noteCharData.noteIndex === currentPlayingIndex; + } else if (seqState.type === "parallel" && seqState.trackStates) { + const trackState = seqState.trackStates[noteCharData.trackIndex]; + if (trackState && trackState.track) { + const totalNotes = trackState.track.length; + const currentPlayingIndex = (trackState.noteIndex - 1 + totalNotes) % totalNotes; + isCurrentlyPlayingNote = noteCharData.noteIndex === currentPlayingIndex; + } + } + } + } + } + if (char === "{") { + inWaveformForColoring = true; + color3 = shouldFlashGreen ? "green" : "yellow"; + } else if (char === "}") { + inWaveformForColoring = false; + color3 = shouldFlashGreen ? "green" : "yellow"; + } else if (inWaveformForColoring) { + let lookBack = i2 - 1; + let waveContent = ""; + while (lookBack >= 0 && melodyString[lookBack] !== "{") { + waveContent = melodyString[lookBack] + waveContent; + lookBack--; + } + waveContent = waveContent + char; + if (waveContent.startsWith("#") || lookBack >= 0 && melodyString[lookBack + 1] === "#") { + const hashIdx = waveContent.indexOf("#"); + const code2 = waveContent.substring(hashIdx + 1).replace(/[^a-zA-Z0-9]/g, ""); + const cached = getStampleStatus(code2); + if (cached && cached.loaded) color3 = "lime"; + else if (cached && cached.loading) color3 = "cyan"; + else if (cached && cached.error) color3 = [255, 100, 100]; + else color3 = "orange"; + } else { + color3 = "cyan"; + } + } else if (char === "*") { + if (shouldFlashMutation && (triggeredAsteriskPositions.includes(i2) || triggeredAsteriskPositions.includes("*"))) { + color3 = "white"; + } else { + const time = now * 5e-3; + const hue = time % 1 * 360; + color3 = hsvToRgb(hue, 1, 1); + } + } else if (melodyState && melodyState.isFallback) { + if (noteCharData) { + if (isCurrentlyPlayingNote) { + const isMutated = isNoteMutated(noteCharData); + if (/[.,]/i.test(char)) { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (/[s]/i.test(char)) { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (char === "_") { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (/[0-9+\-#<>]/i.test(char)) { + color3 = "green"; + } else { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } + } else { + const isMutated = isNoteMutated(noteCharData); + if (isMutated) { + color3 = getMutatedNoteColor(false, noteCharData); + } else { + color3 = timingHasStarted ? "yellow" : getStaticNoteColor(noteCharData); + } + } + } else { + color3 = timingHasStarted ? "yellow" : "gray"; + } + } else { + if (noteCharData) { + if (isCurrentlyPlayingNote) { + const isMutated = isNoteMutated(noteCharData); + if (/[.,]/i.test(char)) { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (/[s]/i.test(char)) { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (char === "_") { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } else if (/[0-9+\-#\[\]]/i.test(char)) { + color3 = "green"; + } else { + color3 = isMutated ? getMutatedNoteColor(true, noteCharData) : getRedNoteColor(); + } + } else { + const isMutated = isNoteMutated(noteCharData); + if (isMutated) { + color3 = getMutatedNoteColor(false, noteCharData); + } else { + color3 = timingHasStarted ? "yellow" : "gray"; + } + } + } else { + color3 = timingHasStarted ? "yellow" : "gray"; + } + } + } + } + const noteCharData2 = noteCharPositions.find((ncp) => ncp.charIndex === i2); + const shouldDim = isSequentialMelody && noteCharData2 && noteCharData2.isInCurrentSequence === false; + if (shouldDim && color3 !== "cyan" && color3 !== "magenta" && color3 !== "white") { + if (typeof color3 === "string") { + color3 = "gray"; + } else if (Array.isArray(color3)) { + color3 = [ + Math.round(color3[0] * 0.3), + Math.round(color3[1] * 0.3), + Math.round(color3[2] * 0.3) + ]; + } + } + if (Array.isArray(color3)) { + coloredMelodyString += `\\${color3[0]},${color3[1]},${color3[2]}\\${char}`; + } else { + coloredMelodyString += `\\${color3}\\${char}`; + } + } + return coloredMelodyString; +} + +// public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs +var mode_default = { + MODE_NUMBER: 1 << 0, + MODE_ALPHA_NUM: 1 << 1, + MODE_8BIT_BYTE: 1 << 2, + MODE_KANJI: 1 << 3 +}; +var QR8bitByte = class { + mode; + data; + constructor(data) { + this.mode = mode_default.MODE_8BIT_BYTE; + this.data = data; + } + getLength() { + return this.data.length; + } + // FIXME? + write(buffer) { + for (let i2 = 0; i2 < this.data.length; i2++) { + buffer.put(this.data.charCodeAt(i2), 8); + } + } +}; +var ErrorCorrectLevel = { + /** + * Allows recovery of up to 7% data loss + */ + L: 1, + /** + * Allows recovery of up to 15% data loss + */ + M: 0, + /** + * Allows recovery of up to 25% data loss + */ + Q: 3, + /** + * Allows recovery of up to 30% data loss + */ + H: 2 +}; +var QRRSBlock = class _QRRSBlock { + totalCount; + dataCount; + constructor(totalCount, dataCount) { + this.totalCount = totalCount; + this.dataCount = dataCount; + } + static RS_BLOCK_TABLE = [ + // L + // M + // Q + // H + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12], + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + ]; + static getRSBlocks(typeNumber, errorCorrectLevel) { + const rsBlock = _QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); + if (rsBlock == void 0) { + throw new Error( + "bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel + ); + } + const length5 = rsBlock.length / 3; + const list = []; + for (let i2 = 0; i2 < length5; i2++) { + const count = rsBlock[i2 * 3 + 0]; + const totalCount = rsBlock[i2 * 3 + 1]; + const dataCount = rsBlock[i2 * 3 + 2]; + for (let j = 0; j < count; j++) { + list.push(new _QRRSBlock(totalCount, dataCount)); + } + } + return list; + } + static getRsBlockTable(typeNumber, errorCorrectLevel) { + switch (errorCorrectLevel) { + case ErrorCorrectLevel.L: + return _QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case ErrorCorrectLevel.M: + return _QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case ErrorCorrectLevel.Q: + return _QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case ErrorCorrectLevel.H: + return _QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default: + return void 0; + } + } +}; +var QRBitBuffer = class { + buffer; + length; + constructor() { + this.buffer = []; + this.length = 0; + } + get(index) { + const bufIndex = Math.floor(index / 8); + return (this.buffer[bufIndex] >>> 7 - index % 8 & 1) == 1; + } + put(num, length5) { + for (let i2 = 0; i2 < length5; i2++) { + this.putBit((num >>> length5 - i2 - 1 & 1) == 1); + } + } + getLengthInBits() { + return this.length; + } + putBit(bit) { + const bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + if (bit) { + this.buffer[bufIndex] |= 128 >>> this.length % 8; + } + this.length++; + } +}; +var QRMath = { + glog: function(n2) { + if (n2 < 1) { + throw new Error("glog(" + n2 + ")"); + } + return QRMath.LOG_TABLE[n2]; + }, + gexp: function(n2) { + while (n2 < 0) { + n2 += 255; + } + while (n2 >= 256) { + n2 -= 255; + } + return QRMath.EXP_TABLE[n2]; + }, + EXP_TABLE: new Array(256), + LOG_TABLE: new Array(256) +}; +for (let i2 = 0; i2 < 8; i2++) { + QRMath.EXP_TABLE[i2] = 1 << i2; +} +for (let i2 = 8; i2 < 256; i2++) { + QRMath.EXP_TABLE[i2] = QRMath.EXP_TABLE[i2 - 4] ^ QRMath.EXP_TABLE[i2 - 5] ^ QRMath.EXP_TABLE[i2 - 6] ^ QRMath.EXP_TABLE[i2 - 8]; +} +for (let i2 = 0; i2 < 255; i2++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i2]] = i2; +} +var math_default = QRMath; +var QRPolynomial = class _QRPolynomial { + num; + constructor(num, shift) { + if (num.length == void 0) { + throw new Error(num.length + "/" + shift); + } + let offset = 0; + while (offset < num.length && num[offset] == 0) { + offset++; + } + this.num = new Array(num.length - offset + shift); + for (let i2 = 0; i2 < num.length - offset; i2++) { + this.num[i2] = num[i2 + offset]; + } + } + get(index) { + return this.num[index]; + } + getLength() { + return this.num.length; + } + multiply(e2) { + const num = new Array(this.getLength() + e2.getLength() - 1); + for (let i2 = 0; i2 < this.getLength(); i2++) { + for (let j = 0; j < e2.getLength(); j++) { + num[i2 + j] ^= math_default.gexp(math_default.glog(this.get(i2)) + math_default.glog(e2.get(j))); + } + } + return new _QRPolynomial(num, 0); + } + mod(e2) { + if (this.getLength() - e2.getLength() < 0) { + return this; + } + const ratio = math_default.glog(this.get(0)) - math_default.glog(e2.get(0)); + const num = new Array(this.getLength()); + for (let i2 = 0; i2 < this.getLength(); i2++) { + num[i2] = this.get(i2); + } + for (let i2 = 0; i2 < e2.getLength(); i2++) { + num[i2] ^= math_default.gexp(math_default.glog(e2.get(i2)) + ratio); + } + return new _QRPolynomial(num, 0).mod(e2); + } +}; +var QRMaskPattern = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 +}; +var QRUtil = { + PATTERN_POSITION_TABLE: [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ], + G15: 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0, + G18: 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0, + G15_MASK: 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1, + getBCHTypeInfo: function(data) { + let d2 = data << 10; + while (QRUtil.getBCHDigit(d2) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d2 ^= QRUtil.G15 << QRUtil.getBCHDigit(d2) - QRUtil.getBCHDigit(QRUtil.G15); + } + return (data << 10 | d2) ^ QRUtil.G15_MASK; + }, + getBCHTypeNumber: function(data) { + let d2 = data << 12; + while (QRUtil.getBCHDigit(d2) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d2 ^= QRUtil.G18 << QRUtil.getBCHDigit(d2) - QRUtil.getBCHDigit(QRUtil.G18); + } + return data << 12 | d2; + }, + getBCHDigit: function(data) { + let digit = 0; + while (data != 0) { + digit++; + data >>>= 1; + } + return digit; + }, + getPatternPosition: function(typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; + }, + getMask: function(maskPattern, i2, j) { + switch (maskPattern) { + case QRMaskPattern.PATTERN000: + return (i2 + j) % 2 == 0; + case QRMaskPattern.PATTERN001: + return i2 % 2 == 0; + case QRMaskPattern.PATTERN010: + return j % 3 == 0; + case QRMaskPattern.PATTERN011: + return (i2 + j) % 3 == 0; + case QRMaskPattern.PATTERN100: + return (Math.floor(i2 / 2) + Math.floor(j / 3)) % 2 == 0; + case QRMaskPattern.PATTERN101: + return i2 * j % 2 + i2 * j % 3 == 0; + case QRMaskPattern.PATTERN110: + return (i2 * j % 2 + i2 * j % 3) % 2 == 0; + case QRMaskPattern.PATTERN111: + return (i2 * j % 3 + (i2 + j) % 2) % 2 == 0; + default: + throw new Error("bad maskPattern:" + maskPattern); + } + }, + getErrorCorrectPolynomial: function(errorCorrectLength) { + let a2 = new QRPolynomial([1], 0); + for (let i2 = 0; i2 < errorCorrectLength; i2++) { + a2 = a2.multiply(new QRPolynomial([1, math_default.gexp(i2)], 0)); + } + return a2; + }, + getLengthInBits: function(mode, type) { + if (1 <= type && type < 10) { + switch (mode) { + case mode_default.MODE_NUMBER: + return 10; + case mode_default.MODE_ALPHA_NUM: + return 9; + case mode_default.MODE_8BIT_BYTE: + return 8; + case mode_default.MODE_KANJI: + return 8; + default: + throw new Error("mode:" + mode); + } + } else if (type < 27) { + switch (mode) { + case mode_default.MODE_NUMBER: + return 12; + case mode_default.MODE_ALPHA_NUM: + return 11; + case mode_default.MODE_8BIT_BYTE: + return 16; + case mode_default.MODE_KANJI: + return 10; + default: + throw new Error("mode:" + mode); + } + } else if (type < 41) { + switch (mode) { + case mode_default.MODE_NUMBER: + return 14; + case mode_default.MODE_ALPHA_NUM: + return 13; + case mode_default.MODE_8BIT_BYTE: + return 16; + case mode_default.MODE_KANJI: + return 12; + default: + throw new Error("mode:" + mode); + } + } else { + throw new Error("type:" + type); + } + }, + getLostPoint: function(qrCode) { + const moduleCount = qrCode.getModuleCount(); + let lostPoint = 0; + for (let row = 0; row < moduleCount; row++) { + for (let col = 0; col < moduleCount; col++) { + let sameCount = 0; + const dark = qrCode.isDark(row, col); + for (let r2 = -1; r2 <= 1; r2++) { + if (row + r2 < 0 || moduleCount <= row + r2) { + continue; + } + for (let c4 = -1; c4 <= 1; c4++) { + if (col + c4 < 0 || moduleCount <= col + c4) { + continue; + } + if (r2 == 0 && c4 == 0) { + continue; + } + if (dark == qrCode.isDark(row + r2, col + c4)) { + sameCount++; + } + } + } + if (sameCount > 5) { + lostPoint += 3 + sameCount - 5; + } + } + } + for (let row = 0; row < moduleCount - 1; row++) { + for (let col = 0; col < moduleCount - 1; col++) { + let count = 0; + if (qrCode.isDark(row, col)) + count++; + if (qrCode.isDark(row + 1, col)) + count++; + if (qrCode.isDark(row, col + 1)) + count++; + if (qrCode.isDark(row + 1, col + 1)) + count++; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + for (let row = 0; row < moduleCount; row++) { + for (let col = 0; col < moduleCount - 6; col++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) { + lostPoint += 40; + } + } + } + for (let col = 0; col < moduleCount; col++) { + for (let row = 0; row < moduleCount - 6; row++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) { + lostPoint += 40; + } + } + } + let darkCount = 0; + for (let col = 0; col < moduleCount; col++) { + for (let row = 0; row < moduleCount; row++) { + if (qrCode.isDark(row, col)) { + darkCount++; + } + } + } + const ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + return lostPoint; + } +}; +var util_default = QRUtil; +var QRCode = class _QRCode { + typeNumber; + errorCorrectLevel; + modules; + moduleCount; + dataCache; + dataList; + constructor(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + // TODO data may be anything, but we start with string + addData(data) { + const newData = new QR8bitByte(data); + this.dataList.push(newData); + this.dataCache = null; + } + isDark(row, col) { + if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { + throw new Error(row + "," + col); + } + if (this.modules === null) { + throw new Error("this.modules is null"); + } + return this.modules[row][col]; + } + getModuleCount() { + return this.moduleCount; + } + make() { + if (this.typeNumber < 1) { + let typeNumber = 1; + for (typeNumber = 1; typeNumber < 40; typeNumber++) { + const rsBlocks = QRRSBlock.getRSBlocks( + typeNumber, + this.errorCorrectLevel + ); + const buffer = new QRBitBuffer(); + let totalDataCount = 0; + for (let i2 = 0; i2 < rsBlocks.length; i2++) { + totalDataCount += rsBlocks[i2].dataCount; + } + for (let i2 = 0; i2 < this.dataList.length; i2++) { + const data = this.dataList[i2]; + buffer.put(data.mode, 4); + buffer.put( + data.getLength(), + util_default.getLengthInBits(data.mode, typeNumber) + ); + data.write(buffer); + } + if (buffer.getLengthInBits() <= totalDataCount * 8) + break; + } + this.typeNumber = typeNumber; + } + this.makeImpl(false, this.getBestMaskPattern()); + } + makeImpl(test, maskPattern) { + this.moduleCount = this.typeNumber * 4 + 17; + this.modules = new Array(this.moduleCount); + for (let row = 0; row < this.moduleCount; row++) { + this.modules[row] = new Array(this.moduleCount); + for (let col = 0; col < this.moduleCount; col++) { + this.modules[row][col] = null; + } + } + this.setupPositionProbePattern(0, 0); + this.setupPositionProbePattern(this.moduleCount - 7, 0); + this.setupPositionProbePattern(0, this.moduleCount - 7); + this.setupPositionAdjustPattern(); + this.setupTimingPattern(); + this.setupTypeInfo(test, maskPattern); + if (this.typeNumber >= 7) { + this.setupTypeNumber(test); + } + if (this.dataCache == null) { + this.dataCache = _QRCode.createData( + this.typeNumber, + this.errorCorrectLevel, + this.dataList + ); + } + this.mapData(this.dataCache, maskPattern); + } + setupPositionProbePattern(row, col) { + for (let r2 = -1; r2 <= 7; r2++) { + if (row + r2 <= -1 || this.moduleCount <= row + r2) + continue; + for (let c4 = -1; c4 <= 7; c4++) { + if (col + c4 <= -1 || this.moduleCount <= col + c4) + continue; + if (0 <= r2 && r2 <= 6 && (c4 == 0 || c4 == 6) || 0 <= c4 && c4 <= 6 && (r2 == 0 || r2 == 6) || 2 <= r2 && r2 <= 4 && 2 <= c4 && c4 <= 4) { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + this.modules[row + r2][col + c4] = true; + } else { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + this.modules[row + r2][col + c4] = false; + } + } + } + } + getBestMaskPattern() { + let minLostPoint = 0; + let pattern = 0; + for (let i2 = 0; i2 < 8; i2++) { + this.makeImpl(true, i2); + const lostPoint = util_default.getLostPoint(this); + if (i2 == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i2; + } + } + return pattern; + } + setupTimingPattern() { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + for (let r2 = 8; r2 < this.moduleCount - 8; r2++) { + if (this.modules[r2][6] != null) { + continue; + } + this.modules[r2][6] = r2 % 2 == 0; + } + for (let c4 = 8; c4 < this.moduleCount - 8; c4++) { + if (this.modules[6][c4] != null) { + continue; + } + this.modules[6][c4] = c4 % 2 == 0; + } + } + setupPositionAdjustPattern() { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + const pos = util_default.getPatternPosition(this.typeNumber); + for (let i2 = 0; i2 < pos.length; i2++) { + for (let j = 0; j < pos.length; j++) { + const row = pos[i2]; + const col = pos[j]; + if (this.modules[row][col] != null) { + continue; + } + for (let r2 = -2; r2 <= 2; r2++) { + for (let c4 = -2; c4 <= 2; c4++) { + if (r2 == -2 || r2 == 2 || c4 == -2 || c4 == 2 || r2 == 0 && c4 == 0) { + this.modules[row + r2][col + c4] = true; + } else { + this.modules[row + r2][col + c4] = false; + } + } + } + } + } + } + setupTypeNumber(test) { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + const bits = util_default.getBCHTypeNumber(this.typeNumber); + for (let i2 = 0; i2 < 18; i2++) { + const mod = !test && (bits >> i2 & 1) == 1; + this.modules[Math.floor(i2 / 3)][i2 % 3 + this.moduleCount - 8 - 3] = mod; + } + for (let i2 = 0; i2 < 18; i2++) { + const mod = !test && (bits >> i2 & 1) == 1; + this.modules[i2 % 3 + this.moduleCount - 8 - 3][Math.floor(i2 / 3)] = mod; + } + } + setupTypeInfo(test, maskPattern) { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + const data = this.errorCorrectLevel << 3 | maskPattern; + const bits = util_default.getBCHTypeInfo(data); + for (let i2 = 0; i2 < 15; i2++) { + const mod = !test && (bits >> i2 & 1) == 1; + if (i2 < 6) { + this.modules[i2][8] = mod; + } else if (i2 < 8) { + this.modules[i2 + 1][8] = mod; + } else { + this.modules[this.moduleCount - 15 + i2][8] = mod; + } + } + for (let i2 = 0; i2 < 15; i2++) { + const mod = !test && (bits >> i2 & 1) == 1; + if (i2 < 8) { + this.modules[8][this.moduleCount - i2 - 1] = mod; + } else if (i2 < 9) { + this.modules[8][15 - i2 - 1 + 1] = mod; + } else { + this.modules[8][15 - i2 - 1] = mod; + } + } + this.modules[this.moduleCount - 8][8] = !test; + } + mapData(data, maskPattern) { + if (this.modules === null) { + throw new Error("this.modules is null"); + } + let inc = -1; + let row = this.moduleCount - 1; + let bitIndex = 7; + let byteIndex = 0; + for (let col = this.moduleCount - 1; col > 0; col -= 2) { + if (col == 6) + col--; + while (true) { + for (let c4 = 0; c4 < 2; c4++) { + if (this.modules[row][col - c4] == null) { + let dark = false; + if (byteIndex < data.length) { + dark = (data[byteIndex] >>> bitIndex & 1) == 1; + } + const mask2 = util_default.getMask(maskPattern, row, col - c4); + if (mask2) { + dark = !dark; + } + this.modules[row][col - c4] = dark; + bitIndex--; + if (bitIndex == -1) { + byteIndex++; + bitIndex = 7; + } + } + } + row += inc; + if (row < 0 || this.moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + } + static PAD0 = 236; + static PAD1 = 17; + static createData(typeNumber, errorCorrectLevel, dataList) { + const rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); + const buffer = new QRBitBuffer(); + for (let i2 = 0; i2 < dataList.length; i2++) { + const data = dataList[i2]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), util_default.getLengthInBits(data.mode, typeNumber)); + data.write(buffer); + } + let totalDataCount = 0; + for (let i2 = 0; i2 < rsBlocks.length; i2++) { + totalDataCount += rsBlocks[i2].dataCount; + } + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw new Error( + "code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")" + ); + } + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + while (true) { + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(_QRCode.PAD0, 8); + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(_QRCode.PAD1, 8); + } + return _QRCode.createBytes(buffer, rsBlocks); + } + static createBytes(buffer, rsBlocks) { + let offset = 0; + let maxDcCount = 0; + let maxEcCount = 0; + const dcdata = new Array(rsBlocks.length); + const ecdata = new Array(rsBlocks.length); + for (let r2 = 0; r2 < rsBlocks.length; r2++) { + const dcCount = rsBlocks[r2].dataCount; + const ecCount = rsBlocks[r2].totalCount - dcCount; + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + dcdata[r2] = new Array(dcCount); + for (let i2 = 0; i2 < dcdata[r2].length; i2++) { + dcdata[r2][i2] = 255 & buffer.buffer[i2 + offset]; + } + offset += dcCount; + const rsPoly = util_default.getErrorCorrectPolynomial(ecCount); + const rawPoly = new QRPolynomial(dcdata[r2], rsPoly.getLength() - 1); + const modPoly = rawPoly.mod(rsPoly); + ecdata[r2] = new Array(rsPoly.getLength() - 1); + for (let i2 = 0; i2 < ecdata[r2].length; i2++) { + const modIndex = i2 + modPoly.getLength() - ecdata[r2].length; + ecdata[r2][i2] = modIndex >= 0 ? modPoly.get(modIndex) : 0; + } + } + let totalCodeCount = 0; + for (let i2 = 0; i2 < rsBlocks.length; i2++) { + totalCodeCount += rsBlocks[i2].totalCount; + } + const data = new Array(totalCodeCount); + let index = 0; + for (let i2 = 0; i2 < maxDcCount; i2++) { + for (let r2 = 0; r2 < rsBlocks.length; r2++) { + if (i2 < dcdata[r2].length) { + data[index++] = dcdata[r2][i2]; + } + } + } + for (let i2 = 0; i2 < maxEcCount; i2++) { + for (let r2 = 0; r2 < rsBlocks.length; r2++) { + if (i2 < ecdata[r2].length) { + data[index++] = ecdata[r2][i2]; + } + } + } + return data; + } +}; +var qrcode = (data, opt) => { + opt = opt || {}; + const qr = new QRCode( + opt.typeNumber || -1, + opt.errorCorrectLevel || ErrorCorrectLevel.H + ); + qr.addData(data); + qr.make(); + return qr; +}; + +// public/aesthetic.computer/lib/fade-state.mjs +var preserveFadeAlpha = false; +function setPreserveFadeAlpha(preserve) { + if (preserveFadeAlpha !== preserve) { + } + preserveFadeAlpha = preserve; +} +function getPreserveFadeAlpha() { + return preserveFadeAlpha; +} + +// public/aesthetic.computer/lib/pads.mjs +var perfNow = typeof performance !== "undefined" && performance.now ? () => performance.now() : () => Date.now(); +var voices = { + // Karplus–Strong plucked string — bright, decaying, great for arps. + pluck(synth, tone, o2 = {}) { + return synth({ + tone, + type: "harp", + beats: o2.beats ?? 0.6, + attack: o2.attack ?? 2e-3, + decay: o2.decay ?? 0.7, + volume: o2.volume ?? 0.5, + pan: o2.pan ?? 0 + }); + }, + // Layered sine+triangle bell — shimmering, long tail. + bell(synth, tone, o2 = {}) { + const v2 = o2.volume ?? 0.4; + synth({ tone, type: "sine", beats: o2.beats ?? 1.2, attack: 4e-3, decay: 0.9, volume: v2, pan: o2.pan ?? 0 }); + synth({ tone, type: "triangle", beats: (o2.beats ?? 1.2) * 0.6, attack: 2e-3, decay: 0.6, volume: v2 * 0.5, pan: o2.pan ?? 0 }); + }, + // Warm sub with a touch of body — sine + faint saw an octave down feel. + sub(synth, tone, o2 = {}) { + const v2 = o2.volume ?? 0.5; + synth({ tone, type: "sine", beats: o2.beats ?? 1.4, attack: o2.attack ?? 0.02, decay: o2.decay ?? 0.7, volume: v2, pan: o2.pan ?? 0 }); + synth({ tone, type: "sawtooth", beats: (o2.beats ?? 1.4) * 0.7, attack: 0.02, decay: 0.5, volume: v2 * 0.18, pan: o2.pan ?? 0 }); + }, + // Breathy waveguide flute/whistle — airy lead / pad top. + flute(synth, tone, o2 = {}) { + return synth({ tone, type: "flute", beats: o2.beats ?? 1, attack: o2.attack ?? 0.06, decay: o2.decay ?? 0.5, volume: o2.volume ?? 0.3, pan: o2.pan ?? 0 }); + }, + // Held, detuned triad — sustained pad. Returns the voice handles (kill/update). + padChord(synth, tones, o2 = {}) { + const v2 = o2.volume ?? 0.12; + return tones.map( + (tone, i2) => synth({ tone, type: o2.type ?? "sine", duration: "\u{1F501}", attack: o2.attack ?? 0.6, decay: o2.decay ?? 0.9, volume: v2, pan: (i2 - (tones.length - 1) / 2) * (o2.spread ?? 0.3) }) + ); + }, + // Soft closed-hat tick from filtered noise. + hat(synth, o2 = {}) { + return synth({ type: "noise-white", tone: o2.tone ?? 800, beats: o2.beats ?? 0.1, attack: 1e-3, decay: 0.18, volume: o2.volume ?? 0.14, pan: o2.pan ?? 0 }); + } +}; + +// public/aesthetic.computer/lib/pack-mode.mjs +var PACK_MODE = false; +var getPackMode = () => PACK_MODE; +var setPackMode = (value) => { + PACK_MODE = value; +}; +var checkPackMode = () => { + try { + if (typeof window !== "undefined" && window.acSPIDER) return false; + } catch (e2) { + } + if (PACK_MODE) return true; + try { + if (typeof window !== "undefined" && window.acPACK_MODE) return true; + if (typeof globalThis !== "undefined" && globalThis.acPACK_MODE) return true; + } catch (e2) { + } + return false; +}; + +// public/aesthetic.computer/lib/logs.mjs +var LOG_LEVELS = { NONE: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4, VERBOSE: 5 }; +var CATEGORY_STYLES = { + boot: { badge: "\u{1F97E}", color: "#22c55e", label: "boot" }, + disk: { badge: "\u{1F4BF}", color: "#8b5cf6", label: "disk" }, + piece: { badge: "\u{1F9E9}", color: "#f59e0b", label: "piece" }, + wallet: { badge: "\u{1F537}", color: "#3b82f6", label: "wallet" }, + auth: { badge: "\u{1F510}", color: "#ec4899", label: "auth" }, + socket: { badge: "\u{1F9E6}", color: "#06b6d4", label: "socket" }, + audio: { badge: "\u{1F50A}", color: "#f97316", label: "audio" }, + gpu: { badge: "\u{1F3A8}", color: "#a855f7", label: "gpu" }, + lisp: { badge: "\u{1F33F}", color: "#10b981", label: "lisp" }, + store: { badge: "\u{1F4BE}", color: "#6366f1", label: "store" }, + tape: { badge: "\u{1F3AC}", color: "#ef4444", label: "tape" }, + hid: { badge: "\u{1F5B1}\uFE0F", color: "#84cc16", label: "hid" }, + net: { badge: "\u{1F310}", color: "#0ea5e9", label: "net" }, + qr: { badge: "\u{1F50D}", color: "#d946ef", label: "qr" } +}; +var categoryLevels = { + boot: LOG_LEVELS.NONE, + // Disabled - too noisy + disk: LOG_LEVELS.NONE, + // Disabled + piece: LOG_LEVELS.INFO, + // Enabled - piece loading is useful + wallet: LOG_LEVELS.NONE, + // Disabled - too noisy + auth: LOG_LEVELS.INFO, + // Enabled - login status is useful + socket: LOG_LEVELS.NONE, + // Disabled - too noisy + audio: LOG_LEVELS.NONE, + // Disabled + gpu: LOG_LEVELS.NONE, + // Disabled - too noisy + lisp: LOG_LEVELS.INFO, + // Enabled - KidLisp init is useful + store: LOG_LEVELS.INFO, + // Enabled - code storage is useful + tape: LOG_LEVELS.INFO, + // Enabled - recording status + hid: LOG_LEVELS.NONE, + // Disabled + net: LOG_LEVELS.NONE, + // Disabled + qr: LOG_LEVELS.NONE + // Disabled +}; +function createCategoryLogger(category) { + const style = CATEGORY_STYLES[category] || { badge: "\u{1F4CB}", color: "#888", label: category }; + const getLevel = () => categoryLevels[category] ?? LOG_LEVELS.INFO; + const badgeStyle = `background: ${style.color}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold;`; + const debugStyle = `background: ${style.color}22; color: ${style.color}; padding: 2px 6px; border-radius: 3px; font-size: 10px; border: 1px solid ${style.color};`; + const successStyle = `background: #22c55e; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold;`; + const warnStyle = `background: #f59e0b; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold;`; + const errorStyle = `background: #ef4444; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold;`; + return { + log: (...args) => { + if (getLevel() >= LOG_LEVELS.INFO) { + console.log(`%c${style.badge} ${style.label}`, badgeStyle, ...args); + } + }, + debug: (...args) => { + if (getLevel() >= LOG_LEVELS.DEBUG) { + console.log(`%c${style.badge} ${style.label}`, debugStyle, ...args); + } + }, + verbose: (...args) => { + if (getLevel() >= LOG_LEVELS.VERBOSE) { + console.log(`%c ${style.badge}`, `color: ${style.color}; font-size: 9px; opacity: 0.6;`, ...args); + } + }, + warn: (...args) => { + if (getLevel() >= LOG_LEVELS.WARN) { + console.warn(`%c\u26A0\uFE0F ${style.label}`, warnStyle, ...args); + } + }, + error: (...args) => { + if (getLevel() >= LOG_LEVELS.ERROR) { + console.error(`%c\u274C ${style.label}`, errorStyle, ...args); + } + }, + success: (...args) => { + if (getLevel() >= LOG_LEVELS.INFO) { + console.log(`%c\u2705 ${style.label}`, successStyle, ...args); + } + } + }; +} +var log = { + boot: createCategoryLogger("boot"), + disk: createCategoryLogger("disk"), + piece: createCategoryLogger("piece"), + wallet: createCategoryLogger("wallet"), + auth: createCategoryLogger("auth"), + socket: createCategoryLogger("socket"), + audio: createCategoryLogger("audio"), + gpu: createCategoryLogger("gpu"), + lisp: createCategoryLogger("lisp"), + store: createCategoryLogger("store"), + tape: createCategoryLogger("tape"), + hid: createCategoryLogger("hid"), + net: createCategoryLogger("net"), + qr: createCategoryLogger("qr") +}; +var debug2 = { + quiet: () => { + Object.keys(categoryLevels).forEach((k) => categoryLevels[k] = LOG_LEVELS.ERROR); + console.log("\u{1F507} AC logs: quiet mode"); + }, + normal: () => { + Object.keys(categoryLevels).forEach((k) => categoryLevels[k] = LOG_LEVELS.INFO); + console.log("\u{1F50A} AC logs: normal mode"); + }, + verbose: () => { + Object.keys(categoryLevels).forEach((k) => categoryLevels[k] = LOG_LEVELS.VERBOSE); + console.log("\u{1F4E2} AC logs: verbose mode"); + }, + set: (category, level) => { + if (categoryLevels[category] !== void 0 && LOG_LEVELS[level] !== void 0) { + categoryLevels[category] = LOG_LEVELS[level]; + console.log(`\u{1F4CB} ${category}: ${level}`); + } + }, + status: () => console.table(Object.fromEntries(Object.entries(categoryLevels).map(([k, v2]) => [k, Object.keys(LOG_LEVELS).find((l2) => LOG_LEVELS[l2] === v2)]))), + help: () => console.log(` +\u{1F3AE} AC Debug Commands: + acDebug.quiet() - Errors only + acDebug.normal() - Standard logging + acDebug.verbose() - All logs + acDebug.set(cat, level) - Set category level (NONE/ERROR/WARN/INFO/DEBUG/VERBOSE) + acDebug.status() - Show current levels + +Categories: ${Object.keys(CATEGORY_STYLES).join(", ")} +`) +}; +if (typeof window !== "undefined") { + window.acDebug = debug2; +} +var logs = { + store: false, + frame: false, + loading: false, + session: false, + udp: false, + download: false, + audio: false, + hid: false, + painting: false, + glaze: false, + deps: false, + messaging: false, + chat: false, + history: false, + recorder: false +}; + +// public/aesthetic.computer/lib/frame-capture.mjs +var isOpaqueOrigin = (() => { + try { + if (typeof window !== "undefined" && window.origin === "null") return true; + if (typeof localStorage !== "undefined") localStorage.getItem("__sandbox_test__"); + return false; + } catch (e2) { + return true; + } +})(); +function formatTimestamp(date = /* @__PURE__ */ new Date()) { + return date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true + }); +} + +// public/aesthetic.computer/lib/kidlisp.mjs +var MELODY_BASE_MS = 500; +var NOTE_LITERAL = /^[a-gA-G](#|s|b|f)?[0-8]$/; +var note = (raw, fallback) => { + if (raw === void 0 || raw === null) return fallback; + const s2 = unquoteString(String(raw)).trim(); + return s2 || fallback; +}; +var KIDLISP_FUNCTIONS = /* @__PURE__ */ new Set([ + // Core drawing + "wipe", + "ink", + "line", + "box", + "flood", + "circle", + "write", + "paste", + "stamp", + "point", + "poly", + "embed", + "tape", + "tri", + "plot", + "shape", + "lines", + "rect", + "ellipse", + "arc", + "curve", + "bezier", + // Output + "print", + "debug", + "log", + "console", + // Math + "random", + "sin", + "cos", + "tan", + "floor", + "ceil", + "round", + "abs", + "sqrt", + "min", + "max", + "pow", + "noise", + "lerp", + "map", + "constrain", + "dist", + // Logic + "choose", + "?", + "...", + "..", + "if", + "cond", + "and", + "or", + "not", + "no", + "yes", + // Audio + "overtone", + "mic", + "amplitude", + "melody", + "speaker", + "tone", + "beep", + "sound", + // Display + "resolution", + "scroll", + "flip", + "spin", + "resetSpin", + "smoothspin", + "zoom", + "blur", + "contrast", + "pan", + "unpan", + "mask", + "unmask", + "steal", + "putback", + "fill", + "outline", + "stroke", + "nofill", + "nostroke", + // Animation + "wiggle", + "sort", + "fade", + "jump", + "bake", + "frame", + "fps", + "auto-density", + "scale", + // Variables + "def", + "set", + "let", + "var", + "const", + // Control + "tap", + "draw", + "now", + "die", + "later", + "repeat", + "loop", + "range", + "each", + "for", + "while", + // Data + "label", + "len", + "list", + "get", + "first", + "rest", + "push", + "pop", + "slice", + "concat", + "reverse", + // Misc + "mul", + "add", + "sub", + "div", + "mod", + "width", + "height", + "time", + "elapsed", + "delta", + // Special + "rainbow", + "zebra", + "gradient" +]); +var KIDLISP_COLORS = /* @__PURE__ */ new Set([ + ...Object.keys(cssColors2), + ...Array.from({ length: 151 }, (_, i2) => `c${i2}`), + "rainbow", + "zebra", + "gradient" +]); +var KIDLISP_VOCABULARY = /* @__PURE__ */ new Set([...KIDLISP_FUNCTIONS, ...KIDLISP_COLORS]); +function isChaoticSource(source) { + if (!source || typeof source !== "string") { + return { isChaotic: true, confidence: 1, reason: "empty or invalid input" }; + } + const trimmed = source.trim(); + if (trimmed.length < 3) { + if (KIDLISP_VOCABULARY.has(trimmed.toLowerCase())) { + return { isChaotic: false, confidence: 0, reason: "known word" }; + } + if (/^\d+$/.test(trimmed)) { + return { isChaotic: false, confidence: 0, reason: "valid number" }; + } + return { isChaotic: true, confidence: 0.8, reason: "too short to be valid" }; + } + const openParens = (source.match(/\(/g) || []).length; + const closeParens = (source.match(/\)/g) || []).length; + const parenDiff = Math.abs(openParens - closeParens); + const maxParens = Math.max(openParens, closeParens, 1); + const parenBalance = parenDiff / maxParens; + if (parenBalance > 0.7 && maxParens > 3) { + return { isChaotic: true, confidence: 0.85, reason: `severely unbalanced parentheses (${openParens} open, ${closeParens} close)` }; + } + const words = source.toLowerCase().match(/[a-z_][a-z0-9_]*/gi) || []; + const recognizedWords = words.filter((w) => KIDLISP_VOCABULARY.has(w.toLowerCase())); + const recognitionRatio = words.length > 0 ? recognizedWords.length / words.length : 0; + const alphanumeric = source.replace(/[^a-zA-Z0-9\s]/g, ""); + const specialRatio = 1 - alphanumeric.length / source.length; + let chaosScore = 0; + let reasons = []; + if (recognitionRatio < 0.2 && words.length >= 3) { + chaosScore += 0.4; + reasons.push(`low word recognition (${Math.round(recognitionRatio * 100)}%)`); + } else if (recognitionRatio < 0.35 && words.length >= 2) { + chaosScore += 0.25; + reasons.push(`moderate word recognition (${Math.round(recognitionRatio * 100)}%)`); + } + if (specialRatio > 0.6) { + chaosScore += 0.35; + reasons.push(`high special char ratio (${Math.round(specialRatio * 100)}%)`); + } else if (specialRatio > 0.45) { + chaosScore += 0.2; + reasons.push(`moderate special char ratio (${Math.round(specialRatio * 100)}%)`); + } + if (parenBalance > 0.4 && maxParens > 2) { + chaosScore += 0.2; + reasons.push(`unbalanced parens (${openParens}/${closeParens})`); + } + if (words.length === 0 && trimmed.length > 10) { + chaosScore += 0.5; + reasons.push("no recognizable words"); + } + if (openParens === 0 && closeParens === 0 && trimmed.length > 0 && !source.includes(",") && !source.includes("\n")) { + const isValidShorthand = KIDLISP_VOCABULARY.has(trimmed.toLowerCase()) || trimmed.startsWith("fade:") || /^c\d+$/.test(trimmed) || /^p\d+$/.test(trimmed) || isValidRGBString(trimmed.replace(/_/g, " ")); + if (!isValidShorthand) { + chaosScore += 0.6; + reasons.push("no parentheses and not a valid color/function name"); + } + } + if (openParens === 0 && closeParens === 0 && trimmed.length > 20 && !source.includes(",") && !source.includes("\n")) { + const isValidLongShorthand = KIDLISP_VOCABULARY.has(trimmed.toLowerCase()) || trimmed.startsWith("fade:") || /^c\d+$/.test(trimmed) || /^p\d+$/.test(trimmed) || isValidRGBString(trimmed.replace(/_/g, " ")); + if (!isValidLongShorthand) { + chaosScore += 0.3; + reasons.push("no parentheses in long input"); + } + } + const isChaotic = chaosScore >= 0.5; + return { + isChaotic, + confidence: Math.min(chaosScore, 1), + reason: reasons.length > 0 ? reasons.join(", ") : "appears valid", + stats: { + words: words.length, + recognized: recognizedWords.length, + recognitionRatio, + specialRatio, + openParens, + closeParens, + parenBalance + } + }; +} +var cacheRegistry = /* @__PURE__ */ new Map(); +function getSourceHash(source) { + let hash = 0; + for (let i2 = 0; i2 < source.length; i2++) { + const char = source.charCodeAt(i2); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash).toString(36); +} +function getCachedCode(source) { + const hash = getSourceHash(source); + return cacheRegistry.get(hash); +} +var __kidlispConsoleEnabled = false; +var __kidlispConsoleInitLogged = false; +var __kidlispTraceEnabled = false; +var __kidlispExecutionTrace = []; +var __kidlispTraceDepth = 0; +var __KIDLISP_MAX_TRACE_ENTRIES = 200; +function postToParent(content) { + const hasWindow = typeof window !== "undefined"; + const hasWorker = !hasWindow && typeof self !== "undefined" && typeof self.postMessage === "function"; + if (hasWindow) { + try { + window.postMessage({ type: "post-to-parent", content }, "*"); + } catch { + } + return; + } + if (hasWorker) { + try { + self.postMessage({ type: "post-to-parent", content }); + } catch { + } + } +} +function postKidlispConsole(level, message, meta) { + if (!isKidlispConsoleEnabled()) return; + const payload = { type: "kidlisp-console", level, message }; + if (meta && typeof meta === "object") { + if (meta.loc) payload.loc = meta.loc; + if (meta.kind) payload.kind = meta.kind; + if (meta.embeddedSource) { + payload.embeddedSource = meta.embeddedSource; + const source = globalCodeCache.get(meta.embeddedSource); + if (source) { + payload.embeddedSourceCode = source; + } + } + } + postToParent(payload); +} +function postKidlispConsoleImage(imageDataUrl, meta = {}) { + if (!isKidlispConsoleEnabled()) return; + const payload = { + type: "kidlisp-console-image", + imageDataUrl, + frameCount: meta.frameCount, + timestamp: meta.timestamp || formatTimestamp(), + dimensions: meta.dimensions, + pieceCode: meta.pieceCode, + // The $code identifier without $ (e.g., "nece") + pieceLabel: meta.pieceLabel, + // Full label with $ (e.g., "$nece") + filename: meta.filename, + // Full filename (e.g., "$nece-@jeffrey-2025-12-17.png") + userHandle: meta.userHandle, + // User handle (e.g., "@jeffrey") + embeddedSource: meta.embeddedSource + }; + postToParent(payload); +} +function enableKidlispTrace() { + __kidlispTraceEnabled = true; + __kidlispExecutionTrace = []; + __kidlispTraceDepth = 0; + if (typeof window !== "undefined") { + window.__acKidlispTraceEnabled = true; + } +} +function disableKidlispTrace() { + __kidlispTraceEnabled = false; + if (typeof window !== "undefined") { + window.__acKidlispTraceEnabled = false; + } +} +function isKidlispTraceEnabled() { + if (__kidlispTraceEnabled) return true; + try { + if (typeof window !== "undefined") return window.__acKidlispTraceEnabled === true; + return false; + } catch { + return false; + } +} +function clearExecutionTrace() { + __kidlispExecutionTrace = []; + __kidlispTraceDepth = 0; +} +function recordTraceEntry(type, expr, result, source, startPos, endPos) { + if (!isKidlispTraceEnabled()) return; + if (__kidlispExecutionTrace.length >= __KIDLISP_MAX_TRACE_ENTRIES) return; + const entry = { + id: __kidlispExecutionTrace.length, + type, + // 'enter', 'exit', 'call', 'primitive', 'value' + depth: __kidlispTraceDepth, + timestamp: performance.now(), + expr: formatExprForTrace(expr), + exprType: getExprType(expr) + }; + if (result !== void 0) entry.result = formatExprForTrace(result); + if (source) entry.source = source.substring(0, 100); + if (startPos !== void 0) entry.startPos = startPos; + if (endPos !== void 0) entry.endPos = endPos; + __kidlispExecutionTrace.push(entry); +} +function traceEnter(expr, source, startPos, endPos) { + recordTraceEntry("enter", expr, void 0, source, startPos, endPos); + __kidlispTraceDepth++; +} +function traceExit(expr, result) { + __kidlispTraceDepth = Math.max(0, __kidlispTraceDepth - 1); + recordTraceEntry("exit", expr, result); +} +function getExecutionTrace() { + return __kidlispExecutionTrace.slice(); +} +function postExecutionTrace() { + if (!isKidlispTraceEnabled()) { + return; + } + const trace = getExecutionTrace(); + if (trace.length === 0) { + return; + } + postToParent({ + type: "kidlisp-trace", + trace, + timestamp: performance.now() + }); +} +function formatExprForTrace(expr) { + if (expr === null || expr === void 0) return String(expr); + if (typeof expr === "number" || typeof expr === "string" || typeof expr === "boolean") { + return expr; + } + if (Array.isArray(expr)) { + if (expr.length === 0) return "()"; + const head = expr[0]; + if (expr.length <= 3) { + return `(${expr.map(formatExprForTrace).join(" ")})`; + } + return `(${head} ...)`; + } + if (typeof expr === "function") return ""; + if (typeof expr === "object") return "{...}"; + return String(expr); +} +function getExprType(expr) { + if (expr === null) return "null"; + if (expr === void 0) return "undefined"; + if (typeof expr === "number") return "number"; + if (typeof expr === "string") return "string"; + if (typeof expr === "boolean") return "boolean"; + if (Array.isArray(expr)) { + if (expr.length === 0) return "empty-list"; + const head = expr[0]; + if (typeof head === "string") { + const specials = ["def", "if", "later", "repeat", "once", "tap", "drag", "let"]; + if (specials.includes(head)) return "special-form"; + return "function-call"; + } + return "list"; + } + if (typeof expr === "function") return "function"; + return "object"; +} +function enableKidlispConsole() { + if (__kidlispConsoleEnabled) return; + __kidlispConsoleEnabled = true; + if (typeof window !== "undefined") { + window.__acKidlispConsoleEnabled = true; + } + postToParent({ type: "kidlisp-console-enabled" }); + __kidlispConsoleInitLogged = true; +} +function isKidlispConsoleEnabled() { + if (__kidlispConsoleEnabled) return true; + try { + if (typeof window !== "undefined") return window.__acKidlispConsoleEnabled === true; + if (typeof globalThis !== "undefined") return globalThis.__acKidlispConsoleEnabled === true; + if (typeof self !== "undefined") return self.__acKidlispConsoleEnabled === true; + return false; + } catch { + return false; + } +} +function kidlispOffsetToLineCol(source, offset) { + const clamped = Math.max(0, Math.min(Number(offset) || 0, source.length)); + let line2 = 1; + let col = 1; + for (let i2 = 0; i2 < clamped; i2++) { + if (source[i2] === "\n") { + line2++; + col = 1; + } else { + col++; + } + } + return { line: line2, col }; +} +function formatConsoleArgs(args) { + return args.map((arg) => { + if (arg instanceof Error) return arg.message || String(arg); + if (typeof arg === "string") return arg; + try { + return JSON.stringify(arg); + } catch { + return String(arg); + } + }).join(" "); +} +function withKidlispConsoleCapture(fn) { + if (!isKidlispConsoleEnabled() || typeof console === "undefined") return fn(); + const shouldForward = (args) => { + if (!args || args.length === 0) return false; + const first = args[0]; + if (typeof first !== "string") return false; + return first.startsWith("\u274C KidLisp") || first.startsWith("\u26A0\uFE0F KidLisp") || first.startsWith("\u2757 Invalid `") || first.startsWith("\u26D4 Evaluation failure") || first.startsWith("\u{1F4DD} LOG:") || first.startsWith("\u{1F527} DEBUG:"); + }; + const original = { + log: console.log, + info: console.info, + warn: console.warn, + error: console.error + }; + const wrap2 = (level, originalFn) => (...args) => { + try { + if (shouldForward(args)) { + postKidlispConsole(level, formatConsoleArgs(args)); + } + } catch { + } + return originalFn.apply(console, args); + }; + try { + console.log = wrap2("log", original.log); + console.info = wrap2("info", original.info); + console.warn = wrap2("warn", original.warn); + console.error = wrap2("error", original.error); + return fn(); + } finally { + console.log = original.log; + console.info = original.info; + console.warn = original.warn; + console.error = original.error; + } +} +if (typeof window !== "undefined" && !window.__acKidlispConsoleListenerInstalled) { + window.__acKidlispConsoleListenerInstalled = true; + window.addEventListener("message", (event) => { + if (event?.data?.type === "kidlisp-console-enable") { + enableKidlispConsole(); + } + }); +} +if (typeof window === "undefined" && typeof self !== "undefined" && !self.__acKidlispConsoleListenerInstalled) { + self.__acKidlispConsoleListenerInstalled = true; + self.addEventListener("message", (event) => { + if (event?.data?.type === "kidlisp-console-enable") { + enableKidlispConsole(); + } + }); +} +function setCachedCode(source, code2) { + const hash = getSourceHash(source); + cacheRegistry.set(hash, code2); +} +var cachingInProgressMap = /* @__PURE__ */ new Map(); +function isCachingInProgress(source) { + const hash = getSourceHash(source); + return cachingInProgressMap.get(hash) || false; +} +function setCachingInProgress(source, inProgress) { + const hash = getSourceHash(source); + if (inProgress) { + cachingInProgressMap.set(hash, true); + } else { + cachingInProgressMap.delete(hash); + } +} +var VERBOSE = false; +var PERF_LOG = false; +var { floor: floor8, max: max6 } = Math; +var globalCodeCache = /* @__PURE__ */ new Map(); +var persistentStoreRef = null; +function initPersistentCache(store2) { + if (!persistentStoreRef && store2) { + persistentStoreRef = store2; + } + const globalScope = (function() { + if (typeof window !== "undefined") return window; + if (typeof globalThis !== "undefined") return globalThis; + if (typeof global !== "undefined") return global; + if (typeof self !== "undefined") return self; + return {}; + })(); + if (globalScope.acPREFILL_CODE_CACHE && !globalCodeCache.size) { + const prefillData = globalScope.acPREFILL_CODE_CACHE; + for (const [cacheId, source] of Object.entries(prefillData)) { + globalCodeCache.set(cacheId, source); + } + } +} +async function getFromPersistentCache(cacheId) { + if (!persistentStoreRef) return null; + try { + const data = await persistentStoreRef.retrieve(`kidlisp-code:${cacheId}`, "local:db"); + if (data && data.source) { + return data.source; + } + } catch (error) { + console.warn(`Failed to retrieve from persistent cache: ${cacheId}`, error); + } + return null; +} +async function saveToPersistentCache(cacheId, source) { + if (!persistentStoreRef) return; + try { + persistentStoreRef[`kidlisp-code:${cacheId}`] = { + source, + cached: Date.now(), + version: 1 + }; + persistentStoreRef.persist(`kidlisp-code:${cacheId}`, "local:db"); + } catch (error) { + console.warn(`Failed to save to persistent cache: ${cacheId}`, error); + } +} +async function getCachedCodeMultiLevel(cacheId) { + if (globalCodeCache.has(cacheId)) { + return globalCodeCache.get(cacheId); + } + const globalScope = (function() { + if (typeof window !== "undefined") return window; + if (typeof globalThis !== "undefined") return globalThis; + if (typeof global !== "undefined") return global; + if (typeof self !== "undefined") return self; + return {}; + })(); + if (globalScope.objktKidlispCodes && globalScope.objktKidlispCodes[cacheId]) { + const teiaSource = globalScope.objktKidlispCodes[cacheId]; + globalCodeCache.set(cacheId, teiaSource); + return teiaSource; + } + const persistentSource = await getFromPersistentCache(cacheId); + if (persistentSource) { + globalCodeCache.set(cacheId, persistentSource); + return persistentSource; + } + const networkSource = await fetchCachedCode(cacheId); + if (networkSource) { + globalCodeCache.set(cacheId, networkSource); + await saveToPersistentCache(cacheId, networkSource); + return networkSource; + } + return null; +} +function clearAllCaches() { + globalCodeCache.clear(); +} +function saveCodeToAllCaches(cacheId, source) { + globalCodeCache.set(cacheId, source); + saveToPersistentCache(cacheId, source); +} +var perfTimers = {}; +var perfLogs = []; +var perfOrder = [ + "parse", + "precompile", + "frame-evaluation", + "repeat-setup", + "repeat-with-iterator", + "fast-draw-loop" +]; +function perfStart(label) { + if (PERF_LOG) perfTimers[label] = performance.now(); +} +function perfEnd(label) { + if (PERF_LOG && perfTimers[label]) { + const duration = performance.now() - perfTimers[label]; + if (duration > 0.1) { + const logEntry = `${label}: ${duration.toFixed(2)}ms`; + const existingIndex = perfLogs.findIndex( + (log3) => log3.startsWith(label + ":") + ); + if (existingIndex !== -1) { + perfLogs.splice(existingIndex, 1); + } + const orderIndex = perfOrder.indexOf(label); + if (orderIndex !== -1) { + let insertIndex = 0; + for (let i2 = 0; i2 < perfLogs.length; i2++) { + const logLabel = perfLogs[i2].split(":")[0]; + const logOrderIndex = perfOrder.indexOf(logLabel); + if (logOrderIndex !== -1 && logOrderIndex < orderIndex) { + insertIndex = i2 + 1; + } + } + perfLogs.splice(insertIndex, 0, logEntry); + } else { + perfLogs.push(logEntry); + } + if (perfLogs.length > 8) perfLogs.splice(8); + } + delete perfTimers[label]; + } +} +var identifierRegex = /[$a-zA-Z_]\w*/g; +var validIdentifierRegex = /^[$a-zA-Z_]\w*$/; +function tokenize(input3) { + if (VERBOSE) console.log("\u{1FA99} Tokenizing:", input3); + const regex = /\s*(;.*|[(),]|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s()";',]+)/g; + const tokens = []; + let match; + while ((match = regex.exec(input3)) !== null) { + const token = match[1]; + if (!token.startsWith(";")) { + tokens.push(token); + } + } + let parenBalance = 0; + for (const token of tokens) { + if (token === "(") { + parenBalance++; + } else if (token === ")") { + parenBalance--; + } + } + while (parenBalance > 0) { + tokens.push(")"); + parenBalance--; + } + if (VERBOSE) console.log("\u{1FA99} Tokens:", tokens); + return tokens; +} +function tokenizeForParser(input3) { + if (VERBOSE) console.log("\u{1FA99} Tokenizing (pos):", input3); + const regex = /\s*(;.*|[(),]|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s()";',]+)/g; + const tokens = []; + let match; + while ((match = regex.exec(input3)) !== null) { + const token = match[1]; + if (!token.startsWith(";")) { + const tokenStart = match.index + match[0].indexOf(token); + tokens.push({ value: token, pos: tokenStart }); + } + } + tokens._inputLength = input3.length; + let parenBalance = 0; + for (const token of tokens) { + if (token.value === "(") { + parenBalance++; + } else if (token.value === ")") { + parenBalance--; + } + } + while (parenBalance > 0) { + tokens.push({ value: ")", pos: input3.length }); + parenBalance--; + } + if (VERBOSE) console.log("\u{1FA99} Tokens (pos):", tokens.map((t2) => t2.value)); + return tokens; +} +function readFromTokens(tokens) { + const result = []; + while (tokens.length > 0) { + if (tokens[0].value === ")") { + const err = new Error("Unexpected ')'"); + err.kidlispOffset = tokens[0].pos; + throw err; + } + if (tokens[0].value === ",") { + tokens.shift(); + continue; + } + const currentToken = tokens[0].value; + if (/^\d*\.?\d+[s]\.\.\.?$/.test(currentToken) || /^\d*\.?\d+[s]!?$/.test(currentToken)) { + const timingExpr = [readExpression(tokens)]; + while (tokens.length > 0 && tokens[0].value !== ")" && tokens[0].value !== "," && // Stop at commas too + !/^\d*\.?\d+[s]\.\.\.?$/.test(tokens[0].value) && !/^\d*\.?\d+[s]!?$/.test(tokens[0].value)) { + const nextExpr = readExpression(tokens); + timingExpr.push(nextExpr); + } + result.push(timingExpr); + } else { + const expr = readExpression(tokens); + result.push(expr); + } + } + return result; +} +function readExpression(tokens) { + if (tokens.length === 0) { + const err = new Error("Unexpected end of input"); + err.kidlispOffset = typeof tokens._inputLength === "number" ? tokens._inputLength : 0; + throw err; + } + const tokenObj = tokens.shift(); + const token = tokenObj.value; + if (token === "(") { + const list = []; + while (tokens.length > 0 && tokens[0].value !== ")") { + list.push(readExpression(tokens)); + } + if (tokens.length === 0) { + console.warn("\u{1F527} Auto-closing: Missing ')' was handled by tokenizer"); + return list; + } + tokens.shift(); + return list; + } else { + return atom(token); + } +} +function atom(token) { + if (token[0] === '"' && token[token.length - 1] === '"' || token[0] === "'" && token[token.length - 1] === "'") { + return token; + } else if (/^\d*\.?\d+[s]\.\.\.?$/.test(token)) { + return token; + } else if (/^\d*\.?\d+[s]$/.test(token)) { + return token; + } else { + const num = parseFloat(token); + const result = isNaN(num) ? token : num; + return result; + } +} +function processArgStringTypes(args) { + if (!Array.isArray(args)) { + if (args === void 0) { + return void 0; + } + return args?.toString(); + } + return args.map((arg) => { + if (typeof arg === "string" && (arg.startsWith('"') && arg.endsWith('"') || arg.startsWith("'") && arg.endsWith("'"))) { + return arg.slice(1, -1); + } + return arg; + }); +} +function unquoteString(str7) { + if (str7.startsWith('"') && str7.endsWith('"') || str7.startsWith("'") && str7.endsWith("'")) { + return str7.slice(1, -1); + } else { + return str7; + } +} +function existing(item) { + return item !== void 0 && item !== null; +} +function getNestedValue(obj, path) { + return path?.split(".").reduce((acc, part) => acc && acc[part], obj); +} +function readLoggingFlag(value) { + if (value === void 0 || value === null) return void 0; + if (typeof value === "boolean") return value; + if (typeof value === "number") return value !== 0; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (!normalized) return void 0; + if (["false", "0", "off", "no"].includes(normalized)) return false; + if (["true", "1", "on", "yes"].includes(normalized)) return true; + } + return void 0; +} +function kidlispInkLoggingEnabled() { + try { + if (typeof globalThis !== "undefined") { + const disable = readLoggingFlag( + globalThis.AC_DISABLE_LINE_COLOR_LOGS ?? globalThis.AC_DISABLE_INK_COLOR_LOGS + ); + if (disable === true) return false; + const enable = readLoggingFlag( + globalThis.AC_LOG_LINE_COLORS ?? globalThis.AC_LOG_INK_COLORS + ); + if (enable !== void 0) return enable; + } + if (typeof process !== "undefined" && process?.env) { + const { env } = process; + const disable = readLoggingFlag( + env.AC_DISABLE_LINE_COLOR_LOGS ?? env.AC_DISABLE_INK_COLOR_LOGS + ); + if (disable === true) return false; + const enable = readLoggingFlag( + env.AC_LOG_LINE_COLORS ?? env.AC_LOG_INK_COLORS + ); + if (enable !== void 0) return enable; + } + } catch (err) { + } + return true; +} +function kidlispInkLogPrefix() { + try { + if (typeof process !== "undefined" && process?.env?.AC_LOG_INK_LABEL) { + return `[${process.env.AC_LOG_INK_LABEL}] `; + } + if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_LABEL) { + return `[${globalThis.AC_LOG_INK_LABEL}] `; + } + } catch (err) { + } + return ""; +} +function cloneValueForInkLog(value) { + if (Array.isArray(value)) { + return value.map((item) => cloneValueForInkLog(item)); + } + if (value && typeof value === "object") { + try { + if (typeof structuredClone === "function") { + return structuredClone(value); + } + return JSON.parse(JSON.stringify(value)); + } catch (err) { + return { ...value }; + } + } + return value; +} +var KidLisp = class _KidLisp { + constructor() { + this.ast = null; + this.networkCache = { sources: {} }; + this.globalDef = {}; + this.globalDef.amp = 0; + this.globalDef.leftAmp = 0; + this.globalDef.rightAmp = 0; + this.globalDef.beat = 0; + this.globalDef.kick = 0; + this.globalDef.mic = 0; + this.globalDef.down = 0; + this.globalDef.penx = 0; + this.globalDef.peny = 0; + this.localEnvStore = [{}]; + this.localEnv = this.localEnvStore[0]; + this.localEnvLevel = 0; + this.tapper = null; + this.drawer = null; + this.lifter = null; + this.frameCount = 0; + this.lastSecondExecutions = {}; + this.instantTriggersExecuted = {}; + this.randomSeed = Date.now() + Math.random(); + this.randomState = this.randomSeed; + this.shortUrl = null; + this.cachedCode = null; + this.cachingInProgress = false; + this.embeddedLayers = []; + this.embeddedLayerCache = /* @__PURE__ */ new Map(); + this.embeddedSourceCache = /* @__PURE__ */ new Map(); + this.loadingEmbeddedLayers = /* @__PURE__ */ new Set(); + this.loadedEmbeddedLayers = /* @__PURE__ */ new Set(); + this.functionCache = /* @__PURE__ */ new Map(); + this.globalEnvCache = null; + this.embeddedApiCache = /* @__PURE__ */ new Map(); + this.lastScreenWidth = null; + this.lastScreenHeight = null; + this.alphaBufferCache = /* @__PURE__ */ new Map(); + this.cachedComposite = null; + this.compositeInvalidated = false; + this.bufferPool = /* @__PURE__ */ new Map(); + this.maxPooledBuffers = 8; + this.lastContrastArgs = null; + this.contrastCacheInvalidated = true; + this.clearEmbeddedLayerCache(); + this.clearBakedLayers(); + this.microphoneConnected = false; + this.microphoneApi = null; + this.micPermissionRequested = false; + this.micDefaultMode = "sine"; + this.tapeEmbeds = /* @__PURE__ */ new Map(); + this.tapeLoadingCallbacks = /* @__PURE__ */ new Map(); + this.tapeFrameBuffers = /* @__PURE__ */ new Map(); + this.fastPathFunctions = /* @__PURE__ */ new Set([ + "line", + "ink", + "wipe", + "backdrop", + "box", + "repeat", + "bunch", + "bake", + "+", + "-", + "*", + "/", + "=", + ">", + "<", + "mic", + "paste", + "stamp", + "jump" + ]); + this.reusableTimingKey = ""; + this.mathCache = /* @__PURE__ */ new Map(); + this.sequenceCounters = /* @__PURE__ */ new Map(); + this.syntaxHighlightSource = null; + this.expressionPositions = []; + this.currentlyHighlighted = /* @__PURE__ */ new Set(); + this.executionHistory = []; + this.currentExecutingExpression = null; + this.lastExecutionTime = 0; + this.flashDuration = 500; + this.isEditMode = false; + this.unknownWordsLogged = /* @__PURE__ */ new Set(); + this.cachedOwnerHandle = null; + this.cachedOwnerSub = null; + this.melodies = /* @__PURE__ */ new Map(); + this.melodyByString = /* @__PURE__ */ new Map(); + this.melodyTimers = /* @__PURE__ */ new Map(); + this._clockOffset = 0; + this.halfResolutionApplied = false; + this.thirdResolutionApplied = false; + this.fourthResolutionApplied = false; + this.fillMode = true; + this.onceExecuted = /* @__PURE__ */ new Set(); + this.currentSource = null; + this.timingBlinks = /* @__PURE__ */ new Map(); + this.blinkDuration = 200; + this.delayTimerActivePeriods = /* @__PURE__ */ new Map(); + this.delayTimerActiveDuration = 280; + this.activeTimingExpressions = /* @__PURE__ */ new Map(); + this.currentTimingContext = null; + this.syntaxSignals = /* @__PURE__ */ new Map(); + this.expressionRegistry = /* @__PURE__ */ new Map(); + this.nextExpressionId = 0; + this.lastValidationErrors = null; + this.errorPositions = null; + this.targetFps = 60; + this.forcedFps = null; + this.perf = { + enabled: false, + // Toggle for performance monitoring + showHUD: false, + // Toggle for performance HUD overlay + samples: 100, + // Number of samples to keep for rolling averages + lastLogTime: 0, + // Last time we logged to console + logInterval: 1e3, + // Log every 1 second + history: [], + // Historical snapshots for bar graph (keep last 20) + // Timing categories + timings: { + parse: [], + // Parse time samples + evaluate: [], + // Evaluation time samples + render: [], + // Render time samples (if available) + total: [] + // Total frame time samples + }, + // Function call tracking + functions: /* @__PURE__ */ new Map(), + // functionName -> { count, totalTime, avgTime } + // Memory usage tracking + memory: { + ast: 0, + // AST size estimate + env: 0, + // Environment size estimate + cache: 0 + // Cache size estimate + }, + // Current frame stats + current: { + parseTime: 0, + evaluateTime: 0, + renderTime: 0, + totalTime: 0, + frameStart: 0 + }, + // Rolling averages (calculated from samples) + avg: { + parse: 0, + evaluate: 0, + render: 0, + total: 0, + fps: 0 + } + }; + this.inkState = void 0; + this.inkStateSet = false; + this.bakedLayers = []; + this.bakeCallCount = 0; + this.hasBakedContent = false; + this.postBakeLayer = null; + this.frameRoutingContext = null; + this.performanceEnabled = false; + this.frameTimings = []; + this.evaluationCounts = /* @__PURE__ */ new Map(); + this.totalEvaluations = 0; + this.currentFrameStart = 0; + this.autoDensity = { + enabled: false, + // Toggle for auto-density scaling + targetFpsMin: 30, + // Minimum acceptable FPS (reduce density if below this) + targetFpsMax: 55, + // Target FPS ceiling (increase density if consistently above) + densityMin: 0.5, + // Minimum density (largest pixels) + densityMax: 4, + // Maximum density (smallest pixels) + densityStep: 0.5, + // Step size for density changes + stabilityFrames: 30, + // Number of frames to average before adjusting + cooldownFrames: 60, + // Minimum frames between adjustments + lastAdjustFrame: 0, + // Frame count at last adjustment + currentDensity: null, + // Current density (null = use system default) + fpsHistory: [], + // Recent FPS samples for stability detection + adjustmentCount: 0, + // Count of density adjustments made + direction: null + // 'up', 'down', or null - tracks trend + }; + } + // 🎯 Performance monitoring methods + startPerformanceMonitoring() { + this.perf.enabled = true; + this.perf.showHUD = true; + if (typeof window !== "undefined") { + if (window.graphPerf) { + window.graphPerf.enabled = true; + } else { + } + } + if (typeof globalThis !== "undefined" && globalThis.graphPerf) { + globalThis.graphPerf.enabled = true; + } + try { + if (typeof graphPerf !== "undefined") { + graphPerf.enabled = true; + } + } catch (e2) { + } + } + stopPerformanceMonitoring() { + this.perf.enabled = false; + this.perf.showHUD = false; + if (typeof window !== "undefined" && window.graphPerf) { + window.graphPerf.enabled = false; + } + } + togglePerformanceHUD() { + this.perf.showHUD = !this.perf.showHUD; + if (this.perf.showHUD) { + this.perf.enabled = true; + } + return this.perf.showHUD; + } + startFrame() { + if (!this.perf.enabled) return; + this.perf.current.frameStart = performance.now(); + resetRainbowCache(); + } + endFrame() { + if (!this.perf.enabled) return; + const now = performance.now(); + this.perf.current.totalTime = now - this.perf.current.frameStart; + this.addSample("total", this.perf.current.totalTime); + this.updateAverages(); + } + startTiming(category) { + return performance.now(); + } + endTiming(category, startTime) { + if (!this.perf.enabled) return; + const duration = performance.now() - startTime; + this.perf.current[category + "Time"] = duration; + this.addSample(category, duration); + } + addSample(category, value) { + if (!this.perf.timings[category]) return; + this.perf.timings[category].push(value); + if (this.perf.timings[category].length > this.perf.samples) { + this.perf.timings[category].shift(); + } + } + updateAverages() { + Object.keys(this.perf.timings).forEach((category) => { + const samples = this.perf.timings[category]; + if (samples.length > 0) { + this.perf.avg[category] = samples.reduce((a2, b2) => a2 + b2, 0) / samples.length; + } + }); + if (this.perf.avg.total > 0) { + this.perf.avg.fps = 1e3 / this.perf.avg.total; + if (globalThis.graphPerf) { + globalThis.graphPerf.lastFPS = this.perf.avg.fps; + } + if (this.autoDensity.enabled) { + this.checkAutoDensity(this.perf.avg.fps); + } + } + } + // 🎯 Auto-density system - automatically adjust pixel density based on FPS + enableAutoDensity(options = {}) { + this.autoDensity.enabled = true; + this.perf.enabled = true; + if (options.targetFpsMin !== void 0) this.autoDensity.targetFpsMin = options.targetFpsMin; + if (options.targetFpsMax !== void 0) this.autoDensity.targetFpsMax = options.targetFpsMax; + if (options.densityMin !== void 0) this.autoDensity.densityMin = options.densityMin; + if (options.densityMax !== void 0) this.autoDensity.densityMax = options.densityMax; + if (options.densityStep !== void 0) this.autoDensity.densityStep = options.densityStep; + if (this.autoDensity.currentDensity === null) { + this.autoDensity.currentDensity = typeof window !== "undefined" && window.acPACK_DENSITY || 2; + } + return this; + } + disableAutoDensity() { + this.autoDensity.enabled = false; + return this; + } + checkAutoDensity(currentFps) { + if (typeof window !== "undefined" && window.acAutoDensityOverride) { + this.autoDensityOverride = true; + this.autoDensity.enabled = false; + return; + } + const ad = this.autoDensity; + ad.fpsHistory.push(currentFps); + if (ad.fpsHistory.length > ad.stabilityFrames) { + ad.fpsHistory.shift(); + } + if (ad.fpsHistory.length < ad.stabilityFrames) return; + if (this.frameCount - ad.lastAdjustFrame < ad.cooldownFrames) return; + const avgFps = ad.fpsHistory.reduce((a2, b2) => a2 + b2, 0) / ad.fpsHistory.length; + const currentDensity = ad.currentDensity || window.acPACK_DENSITY || 2; + let newDensity = currentDensity; + let reason = ""; + if (avgFps < ad.targetFpsMin) { + newDensity = Math.max(ad.densityMin, currentDensity - ad.densityStep); + reason = `FPS ${avgFps.toFixed(1)} < ${ad.targetFpsMin} min`; + ad.direction = "down"; + } else if (avgFps > ad.targetFpsMax && currentDensity < ad.densityMax) { + newDensity = Math.min(ad.densityMax, currentDensity + ad.densityStep); + reason = `FPS ${avgFps.toFixed(1)} > ${ad.targetFpsMax} target`; + ad.direction = "up"; + } + if (newDensity !== currentDensity) { + this.setDensity(newDensity); + ad.lastAdjustFrame = this.frameCount; + ad.adjustmentCount++; + ad.fpsHistory = []; + console.log(`\u{1F3AF} Auto-density: ${currentDensity} \u2192 ${newDensity} (${reason})`); + } + } + setDensity(newDensity) { + newDensity = Math.round(newDensity / this.autoDensity.densityStep) * this.autoDensity.densityStep; + newDensity = Math.max(this.autoDensity.densityMin, Math.min(this.autoDensity.densityMax, newDensity)); + this.autoDensity.currentDensity = newDensity; + if (typeof window !== "undefined") window.acPACK_DENSITY = newDensity; + try { + localStorage.setItem("ac-density", newDensity.toString()); + } catch { + } + if (typeof window !== "undefined") { + window.postMessage({ type: "ac-density-change", density: newDensity }, "*"); + } + return newDensity; + } + getAutoDensityStatus() { + const ad = this.autoDensity; + return { + enabled: ad.enabled, + currentDensity: ad.currentDensity || window.acPACK_DENSITY || 2, + currentFps: this.perf.avg.fps, + targetFpsMin: ad.targetFpsMin, + targetFpsMax: ad.targetFpsMax, + adjustmentCount: ad.adjustmentCount, + direction: ad.direction + }; + } + trackFunction(name, duration) { + if (!this.perf.enabled) return; + if (!this.perf.functions.has(name)) { + this.perf.functions.set(name, { count: 0, totalTime: 0, avgTime: 0 }); + } + const stats = this.perf.functions.get(name); + stats.count++; + stats.totalTime += duration; + stats.avgTime = stats.totalTime / stats.count; + if (false) { + console.warn(`\u{1F525} HOT FUNCTION: ${name} called ${stats.count} times (${stats.avgTime.toFixed(3)}ms avg)`); + } + } + // 📊 Render performance HUD in top-right corner with tiny matrix font + renderPerformanceHUD(api) { + if (!this.perf.showHUD || !api.ink || !api.write || !api.box) return; + const now = performance.now(); + if (now - this.perf.lastLogTime > this.perf.logInterval) { + this.logPerformanceSnapshot(); + this.perf.lastLogTime = now; + } + const screen2 = api.screen || { width: 256, height: 256 }; + const hudWidth = 100; + const hudHeight = 45; + const x = screen2.width - hudWidth - 2; + const y = 2; + api.ink(0, 0, 0, 200); + api.box(x - 1, y - 1, hudWidth + 2, hudHeight + 2, "fill"); + api.ink(100, 255, 100, 255); + api.box(x - 1, y - 1, hudWidth + 2, hudHeight + 2, "outline"); + let lineY = y + 1; + const lineHeight = 8; + const fps = this.perf.avg.fps; + const totalTime = this.perf.avg.total; + let fpsColor = fps >= 55 ? [0, 255, 0] : fps >= 45 ? [255, 255, 0] : fps >= 30 ? [255, 165, 0] : [255, 0, 0]; + api.ink(fpsColor[0], fpsColor[1], fpsColor[2]); + api.write(`FPS:${fps.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + api.ink(100, 200, 255); + const logicTime = (this.perf.avg.parse || 0) + (this.perf.avg.evaluate || 0); + api.write(`LOGIC:${logicTime.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + this.drawPerfBar(api, x + 48, lineY, logicTime, 8, 50, [100, 200, 255]); + lineY += lineHeight; + api.ink(255, 100, 200); + api.write(`PAINT:${this.perf.avg.render.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + this.drawPerfBar(api, x + 48, lineY, this.perf.avg.render, 6, 50, [255, 100, 200]); + lineY += lineHeight; + const memEstimate = this.estimateMemoryUsage(); + api.ink(150, 150, 255); + api.write(`MEMORY:${(memEstimate / 1024).toFixed(1)}KB`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + api.ink(200, 200, 200); + api.write(`HEALTH`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + const fpsHealth = Math.min(100, fps / 60 * 100); + const timeHealth = Math.max(0, 100 - (totalTime - 16.67) / 16.67 * 100); + const memHealth = Math.max(0, 100 - Math.max(0, (memEstimate - 1024 * 1024) / (1024 * 1024)) * 100); + const overallHealth = (fpsHealth + timeHealth + memHealth) / 3; + let healthColor = overallHealth >= 80 ? [0, 255, 0] : overallHealth >= 60 ? [255, 255, 0] : overallHealth >= 40 ? [255, 165, 0] : [255, 0, 0]; + this.drawPerfBar(api, x + 48, lineY, overallHealth, 100, 50, healthColor); + lineY += lineHeight; + if (this.perf.history.length > 1) { + api.ink(80, 80, 80); + api.write(`HIST`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + const graphX = x + 20; + const graphY = lineY + 2; + const graphWidth = 70; + const graphHeight = 6; + api.ink(20, 20, 20); + api.box(graphX, graphY, graphWidth, graphHeight, "fill"); + const barWidth = Math.max(1, Math.floor(graphWidth / this.perf.history.length)); + for (let i2 = 0; i2 < this.perf.history.length; i2++) { + const snapshot = this.perf.history[i2]; + const normalizedFps = Math.min(75, snapshot.fps); + const barHeight = Math.min(graphHeight, Math.max(1, normalizedFps / 75 * graphHeight)); + const barX = graphX + i2 * barWidth; + const barY = graphY + graphHeight - barHeight; + if (snapshot.fps >= 55) { + api.ink(0, 255, 0); + } else if (snapshot.fps >= 45) { + api.ink(255, 255, 0); + } else if (snapshot.fps >= 30) { + api.ink(255, 165, 0); + } else { + api.ink(255, 0, 0); + } + api.box(barX, barY, Math.max(1, barWidth - 1), barHeight, "fill"); + } + const targetY = graphY + graphHeight - Math.floor(60 / 75 * graphHeight); + api.ink(128, 128, 128); + api.box(graphX, targetY, graphWidth, 1, "fill"); + } + } + // Draw a tiny performance bar with 60fps-oriented health colors + drawPerfBar(api, x, y, value, maxValue, width2, baseColor) { + const fillWidth = Math.min(width2, value / maxValue * width2); + const fillRatio = value / maxValue; + api.ink(50, 50, 50); + api.box(x, y + 2, width2, 3, "fill"); + if (fillWidth > 0) { + let barColor; + if (fillRatio <= 0.5) { + barColor = [0, 255, 0]; + } else if (fillRatio <= 0.75) { + barColor = [255, 255, 0]; + } else if (fillRatio <= 1) { + barColor = [255, 165, 0]; + } else { + barColor = [255, 0, 0]; + } + api.ink(barColor[0], barColor[1], barColor[2]); + api.box(x, y + 2, fillWidth, 3, "fill"); + } + if (value > maxValue) { + api.ink(255, 0, 0); + api.box(x + width2 - 2, y + 1, 2, 5, "fill"); + } + } + // Estimate memory usage + estimateMemoryUsage() { + let size = 0; + size += JSON.stringify(this.localEnv || {}).length; + size += this.perf.timings.total.length * 8; + size += this.perf.functions.size * 64; + return size; + } + // Log performance snapshot to console and store in history + logPerformanceSnapshot() { + let totalFunctionCalls = 0; + let graphCalls = 0; + let apiCalls = 0; + let userCalls = 0; + let optCalls = 0; + this.perf.functions.forEach((stats, name) => { + totalFunctionCalls += stats.count; + if (name.startsWith("api:")) { + apiCalls += stats.count; + } else if (name.startsWith("user:")) { + userCalls += stats.count; + } else if (name.startsWith("opt:")) { + optCalls += stats.count; + } else { + graphCalls += stats.count; + } + }); + const parseTime = this.perf.avg.parse || this.perf.current.parseTime || 0; + const evalTime = this.perf.avg.evaluate || this.perf.current.evaluateTime || 0; + const snapshot = { + time: (/* @__PURE__ */ new Date()).toLocaleTimeString(), + fps: this.perf.avg.fps, + total: this.perf.avg.total, + parse: parseTime, + evaluate: evalTime, + render: this.perf.avg.render, + functions: totalFunctionCalls, + memory: (this.estimateMemoryUsage() / 1024).toFixed(1) + }; + if (false) { + const sortedFunctions = Array.from(this.perf.functions.entries()).sort((a2, b2) => b2[1].count - a2[1].count).slice(0, 8); + console.log(`\u{1F525} TOP HOTSPOTS:`); + sortedFunctions.forEach(([name, stats], i2) => { + const callsPerSecond = Math.round(stats.count); + const totalTime = stats.totalTime.toFixed(1); + const category = name.startsWith("api:") ? "\u{1F4F1}" : name.startsWith("user:") ? "\u{1F464}" : name.startsWith("opt:") ? "\u26A1" : "\u{1F3A8}"; + console.log(` ${i2 + 1}. ${category} ${name}: ${callsPerSecond} calls (${totalTime}ms total, ${stats.avgTime.toFixed(3)}ms avg)`); + }); + if (this.perf.evalCallCount > 0 || this.perf.bodyProcessCount > 0) { + console.log(`\u{1F504} LOOPS: evaluate() called ${this.perf.evalCallCount || 0} times, body processing ${this.perf.bodyProcessCount || 0} times`); + } + if (false) { + if (window.graphPerf.functions.size > 0) { + const graphStats = window.graphPerf.getStats(); + console.log(`\u{1F3A8} GRAPH HOTSPOTS:`); + graphStats.slice(0, 5).forEach((stat, i2) => { + console.log(` ${i2 + 1}. ${stat.name}: ${stat.count} calls (${stat.totalTime.toFixed(1)}ms total, ${stat.avgTime.toFixed(2)}ms avg, ${stat.maxTime.toFixed(2)}ms max)`); + }); + window.graphPerf.reset(); + } else { + console.log(`\u{1F3A8} GRAPH DEBUG: graphPerf enabled=${window.graphPerf.enabled}, functions.size=${window.graphPerf.functions.size}`); + } + } else { + } + this.perf.functions.clear(); + this.perf.evalCallCount = 0; + this.perf.bodyProcessCount = 0; + } + this.perf.history.push(snapshot); + if (this.perf.history.length > 20) { + this.perf.history.shift(); + } + } + // 🎯 Set API context for this KidLisp instance + setAPI(api) { + this.api = api; + this.embeddedApiCache.clear(); + } + // 🎚️ Slide update - re-parse and update AST without resetting state/layers + // Used for real-time parameter tweaking in slide mode + slideUpdate(source) { + if (!source) return; + try { + const parsed = this.parse(source); + this.ast = JSON.parse(JSON.stringify(parsed)); + this.currentSource = source; + if (this.layer0 && this.layer0.pixels) { + this.layer0.pixels.fill(0); + } + if (this.bakes) { + this.bakes = []; + this.currentBakeIndex = -1; + } + } catch (e2) { + console.warn("\u{1F39A}\uFE0F Slide update parse error:", e2.message); + } + } + // �🎵 Update audio-related global variables (called from pieces with audio data) + updateAudioGlobals(audioData) { + if (audioData && typeof audioData === "object") { + if (typeof audioData.amp === "number") { + if (this.frameCount % 60 === 0 && audioData.amp > 0) console.log("\u{1F50A} updateAudioGlobals amp:", audioData.amp); + this.globalDef.amp = audioData.amp; + } + if (typeof audioData.leftAmp === "number") { + this.globalDef.leftAmp = audioData.leftAmp; + } + if (typeof audioData.rightAmp === "number") { + this.globalDef.rightAmp = audioData.rightAmp; + } + if (typeof audioData.beat === "number") { + this.globalDef.beat = audioData.beat; + } + if (typeof audioData.kick === "number") { + this.globalDef.kick = audioData.kick; + } + if (typeof audioData.bass === "number") { + this.globalDef.bass = audioData.bass; + } + if (typeof audioData.mid === "number") { + this.globalDef.mid = audioData.mid; + } + if (typeof audioData.treble === "number") { + this.globalDef.treble = audioData.treble; + } + if (typeof audioData.highMid === "number") { + this.globalDef.highMid = audioData.highMid; + } + if (typeof audioData.presence === "number") { + this.globalDef.presence = audioData.presence; + } + let shouldTraceEnv = false; + try { + shouldTraceEnv = typeof globalThis !== "undefined" && globalThis.AC_KL_ENV_TRACE === true || typeof localStorage !== "undefined" && localStorage.getItem("ac:kidlisp:env-trace") === "1"; + } catch (_err) { + shouldTraceEnv = false; + } + if (shouldTraceEnv) { + const frame = Number.isFinite(this.frameCount) ? this.frameCount : 0; + if (!this.lastEnvTraceFrame || frame - this.lastEnvTraceFrame >= 60) { + const source = audioData.__source || "unknown"; + console.log("\u{1F9ED} KL env globals", { + source, + frame, + amp: this.globalDef.amp, + leftAmp: this.globalDef.leftAmp, + rightAmp: this.globalDef.rightAmp, + beat: this.globalDef.beat, + kick: this.globalDef.kick, + bass: this.globalDef.bass, + mid: this.globalDef.mid, + treble: this.globalDef.treble, + highMid: this.globalDef.highMid, + presence: this.globalDef.presence, + mic: this.globalDef.mic + }); + this.lastEnvTraceFrame = frame; + } + } + } + } + // Reset all state for a fresh KidLisp instance + reset(clearOnceExecuted = false, sourceChanged = false) { + this.ast = null; + this.globalDef = {}; + this.globalDef.amp = 0; + this.globalDef.leftAmp = 0; + this.globalDef.rightAmp = 0; + this.globalDef.beat = 0; + this.globalDef.kick = 0; + this.globalDef.mic = 0; + this.globalDef.bass = 0; + this.globalDef.mid = 0; + this.globalDef.treble = 0; + this.globalDef.highMid = 0; + this.globalDef.presence = 0; + this.globalDef.down = 0; + this.globalDef.penx = 0; + this.globalDef.peny = 0; + this.localEnvStore = [{}]; + this.localEnv = this.localEnvStore[0]; + this.localEnvLevel = 0; + this.tapper = null; + this.drawer = null; + this.lifter = null; + this.frameCount = 0; + this.lastSecondExecutions = {}; + this.instantTriggersExecuted = {}; + const globalCachedCode = this.currentSource ? getCachedCode(this.currentSource) : null; + if (globalCachedCode) { + this.cachedCode = globalCachedCode; + this.shortUrl = `aesthetic.computer/$${globalCachedCode}`; + } else { + this.cachedCode = null; + this.shortUrl = null; + } + this.cacheInitiated = false; + this.microphoneConnected = false; + this.microphoneApi = null; + this.micPermissionRequested = false; + this.functionCache.clear(); + this.globalEnvCache = null; + this.mathCache.clear(); + this.sequenceCounters.clear(); + if (this.choiceCache) { + this.choiceCache.clear(); + } + if (this.globalFunctionCache) { + this.globalFunctionCache.clear(); + } + if (sourceChanged) { + this.loadingEmbeddedLayers.clear(); + this.loadedEmbeddedLayers.clear(); + } + this.syntaxHighlightSource = null; + this.expressionPositions = []; + this.currentlyHighlighted.clear(); + this.executionHistory = []; + this.currentExecutingExpression = null; + this.lastExecutionTime = 0; + this.cachedOwnerHandle = null; + this.cachedOwnerSub = null; + this.melodies.clear(); + this.melodyTimers.clear(); + this.halfResolutionApplied = false; + this.thirdResolutionApplied = false; + this.fourthResolutionApplied = false; + this.fillMode = true; + if (clearOnceExecuted) { + this.onceExecuted.clear(); + if (sourceChanged) { + this.layer0 = null; + this.bakes = []; + this.currentBakeIndex = -1; + } + } + this.timingBlinks.clear(); + this.delayTimerActivePeriods.clear(); + this.activeTimingExpressions.clear(); + this.postEmbedCommands = []; + this.postCompositeCommands = []; + this.preserveLayer0NextFrame = false; + this.bufferPool.clear(); + if (this.unknownWordsLogged) this.unknownWordsLogged.clear(); + this.lastValidationErrors = null; + this.lastParseError = null; + this.errorPositions = null; + } + // Register an expression and get its unique ID + registerExpression(expr) { + const key = JSON.stringify(expr); + if (!this.expressionRegistry.has(key)) { + this.expressionRegistry.set(key, this.nextExpressionId++); + } + return this.expressionRegistry.get(key); + } + // Signal syntax highlighting for an expression + signalSyntaxHighlight(expr, color3) { + const id = this.registerExpression(expr); + this.syntaxSignals.set(id, color3); + if (this.frameCount % 60 === 0) { + } + } + // Clear all syntax signals (called each frame) + clearSyntaxSignals() { + this.syntaxSignals.clear(); + } + // Count active (non-cache-based) embedded layers for deferral logic + countActiveEmbeddedLayers() { + if (!this.embeddedLayers) return 0; + return this.embeddedLayers.filter((layer) => { + return !(layer.originalCacheId && /^[0-9A-Za-z]{3,12}$/.test(layer.originalCacheId)); + }).length; + } + // Performance monitoring methods + enablePerformanceMonitoring(enable = true) { + this.performanceEnabled = enable; + if (enable) { + } else { + } + } + startFrameTiming() { + if (!this.performanceEnabled) return; + this.currentFrameStart = performance.now(); + this.totalEvaluations = 0; + this.evaluationCounts.clear(); + } + endFrameTiming() { + if (!this.performanceEnabled) return; + const frameDuration = performance.now() - this.currentFrameStart; + this.frameTimings.push(frameDuration); + if (this.frameTimings.length > 60) { + this.frameTimings.shift(); + } + if (this.frameTimings.length === 60) { + const avgFrameTime = this.frameTimings.reduce((a2, b2) => a2 + b2) / 60; + const maxFrameTime = Math.max(...this.frameTimings); + const minFrameTime = Math.min(...this.frameTimings); + console.log(`\u{1F52C} KidLisp Performance (60 frames): + Avg: ${avgFrameTime.toFixed(2)}ms, Min: ${minFrameTime.toFixed(2)}ms, Max: ${maxFrameTime.toFixed(2)}ms + Total evaluations: ${this.totalEvaluations} + Top expressions:`, Array.from(this.evaluationCounts.entries()).sort((a2, b2) => b2[1] - a2[1]).slice(0, 5)); + this.frameTimings = []; + } + } + trackEvaluation(type) { + if (!this.performanceEnabled) return; + this.totalEvaluations++; + this.evaluationCounts.set(type, (this.evaluationCounts.get(type) || 0) + 1); + } + // Parse timing tokens used by syntax highlighting animation. + // Supports second (0.5s, 1s...) and frame (1f, 2f...) forms. + parseTimingTokenMeta(timingToken) { + if (typeof timingToken !== "string") return null; + let match = timingToken.match(/^(\d*\.?\d+)([sf])(\.\.\.?)$/); + if (match) { + const value = parseFloat(match[1]); + if (!Number.isFinite(value)) return null; + const unit = match[2]; + const intervalMs = unit === "s" ? value * 1e3 : value / 60 * 1e3; + return { + token: timingToken, + value, + unit, + isCycle: true, + isDelay: false, + isInstant: false, + intervalMs: Math.max(0, intervalMs) + }; + } + match = timingToken.match(/^(\d*\.?\d+)([sf])(!)?$/); + if (match) { + const value = parseFloat(match[1]); + if (!Number.isFinite(value)) return null; + const unit = match[2]; + const intervalMs = unit === "s" ? value * 1e3 : value / 60 * 1e3; + return { + token: timingToken, + value, + unit, + isCycle: false, + isDelay: true, + isInstant: Boolean(match[3]), + intervalMs: Math.max(0, intervalMs) + }; + } + return null; + } + getTimingIntervalMs(timingToken) { + const timingMeta = this.parseTimingTokenMeta(timingToken); + if (!timingMeta) return 1e3; + return Math.max(16, timingMeta.intervalMs); + } + getTimingBlinkProfile(timingToken) { + const timingMeta = this.parseTimingTokenMeta(timingToken); + if (!timingMeta) { + return { + isFast: false, + intervalMs: 1e3, + pulseMs: 1e3, + blinkWindowMs: 80 + }; + } + const intervalMs = Math.max(16, timingMeta.intervalMs); + const isFast = intervalMs < 1e3; + if (!isFast) { + return { + isFast: false, + intervalMs, + pulseMs: intervalMs, + blinkWindowMs: Math.min(200, Math.max(80, intervalMs * 0.25)) + }; + } + const pulseMs = Math.max(60, Math.min(160, intervalMs * 4)); + const blinkWindowMs = Math.max(20, Math.min(80, pulseMs * 0.45)); + return { + isFast: true, + intervalMs, + pulseMs, + blinkWindowMs + }; + } + getTimingEditBlinkState(timingToken, now = this._now(), offsetMs = 0) { + const profile = this.getTimingBlinkProfile(timingToken); + const adjustedNow = now + offsetMs; + const pulseMs = Math.max(1, profile.pulseMs); + const phaseMs = (adjustedNow % pulseMs + pulseMs) % pulseMs; + return { + ...profile, + phaseMs, + isBlinking: phaseMs < profile.blinkWindowMs + }; + } + getTimingCyclePosition(timingToken, totalArgs, now = this._now(), offsetMs = 0) { + if (!Number.isFinite(totalArgs) || totalArgs <= 0) return 0; + const adjustedNow = now + offsetMs; + const intervalMs = this.getTimingIntervalMs(timingToken); + return Math.floor(adjustedNow / intervalMs) % totalArgs; + } + // Mark a timing expression as triggered for blinking + markTimingTriggered(timingToken) { + const now = performance.now(); + const blinkProfile = this.getTimingBlinkProfile(timingToken); + const isDelayTimer = /^\d*\.?\d+[sf]!?$/.test(timingToken); + const existingBlink = this.timingBlinks.get(timingToken); + if (blinkProfile.isFast && existingBlink && now - existingBlink.triggerTime < blinkProfile.pulseMs) { + this.timingBlinks.set(timingToken, { + ...existingBlink, + duration: Math.max(existingBlink.duration || 0, 200), + blinkWindowMs: blinkProfile.blinkWindowMs, + pulseMs: blinkProfile.pulseMs, + isFast: true, + isDelayTimer + }); + return; + } + const flashDuration = isDelayTimer ? Math.max(200, blinkProfile.pulseMs) : this.blinkDuration; + this.timingBlinks.set(timingToken, { + triggerTime: now, + duration: flashDuration, + blinkWindowMs: blinkProfile.blinkWindowMs, + pulseMs: blinkProfile.pulseMs, + isFast: blinkProfile.isFast, + isDelayTimer + }); + } + // Check if a timing token should be blinking + isTimingBlinking(timingToken) { + if (!this.timingBlinks.has(timingToken)) return false; + const blinkInfo = this.timingBlinks.get(timingToken); + const now = performance.now(); + const elapsed = now - blinkInfo.triggerTime; + if (elapsed > blinkInfo.duration) { + this.timingBlinks.delete(timingToken); + return false; + } + const blinkWindowMs = blinkInfo.blinkWindowMs ?? 80; + if (blinkInfo.isFast) { + const pulseMs = Math.max(1, blinkInfo.pulseMs ?? 120); + const pulsePhase = elapsed % pulseMs; + return pulsePhase < blinkWindowMs; + } + const isDelayTimer = blinkInfo.isDelayTimer ?? /^\d*\.?\d+[sf]!?$/.test(timingToken); + if (isDelayTimer) { + return elapsed < blinkWindowMs; + } + return elapsed < blinkWindowMs; + } + // Scan source code for $codes and mark them as loading for syntax highlighting + scanAndMarkEmbeddedCodes(source) { + const codeMatches = source.match(/\$[0-9A-Za-z]+/g); + if (codeMatches) { + codeMatches.forEach((fullCode) => { + const cacheId = fullCode.substring(1); + if (!this.loadedEmbeddedLayers.has(cacheId)) { + this.loadingEmbeddedLayers.add(cacheId); + } + }); + this.preloadEmbeddedCodes(codeMatches); + } + } + // Preload all embedded codes in parallel for faster loading + preloadEmbeddedCodes(codeMatches) { + const uniqueCacheIds = [...new Set(codeMatches.map((code2) => code2.substring(1)))]; + const needsFetching = uniqueCacheIds.filter((cacheId) => { + if (this.embeddedSourceCache.has(cacheId)) { + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + return false; + } + if (globalCodeCache.has(cacheId)) { + this.embeddedSourceCache.set(cacheId, globalCodeCache.get(cacheId)); + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + return false; + } + const fetchKey = `${cacheId}_fetching_source`; + if (this.embeddedLayerCache.has(fetchKey)) { + return false; + } + return true; + }); + if (needsFetching.length === 0) { + return; + } + needsFetching.forEach((cacheId) => { + const fetchKey = `${cacheId}_fetching_source`; + this.embeddedLayerCache.set(fetchKey, true); + getCachedCodeMultiLevel(cacheId).then((source) => { + this.embeddedLayerCache.delete(fetchKey); + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + if (source) { + this.embeddedSourceCache.set(cacheId, source); + } else { + console.warn(`\u274C Failed to preload $code: ${cacheId}`); + } + }).catch((error) => { + console.error(`\u274C Preload error for ${cacheId}:`, error); + this.embeddedLayerCache.delete(fetchKey); + this.loadingEmbeddedLayers.delete(cacheId); + }); + }); + } + // Mark a delay timer as entering its active display period + markDelayTimerActive(timingToken) { + const now = performance.now(); + this.delayTimerActivePeriods.set(timingToken, { + activateTime: now, + duration: this.delayTimerActiveDuration + }); + } + // Check if a delay timer is in its active display period + isDelayTimerActive(timingToken) { + if (!this.delayTimerActivePeriods.has(timingToken)) { + return false; + } + const activeInfo = this.delayTimerActivePeriods.get(timingToken); + const now = performance.now(); + const elapsed = now - activeInfo.activateTime; + if (elapsed > activeInfo.duration) { + this.delayTimerActivePeriods.delete(timingToken); + return false; + } + return true; + } + // Format an expression for HUD display (convert to readable text) + formatExpressionForHUD(expr) { + if (typeof expr === "string") { + return expr; + } + if (typeof expr === "number") { + return expr.toString(); + } + if (Array.isArray(expr)) { + if (expr.length === 0) return "()"; + if (expr.length === 1) return `(${expr[0]})`; + const head = expr[0]; + const args = expr.slice(1); + if (head === "ink" && args.length === 1) { + return `(ink ${args[0]})`; + } + if (head === "line" && args.length === 0) { + return "(line)"; + } + if (head === "line" && args.length > 0) { + return `(line ${args.join(" ")})`; + } + if (head === "box" && args.length === 0) { + return "(box)"; + } + if (head === "box" && args.length > 0) { + return `(box ${args.join(" ")})`; + } + const displayArgs = args.slice(0, 3); + const argsText = displayArgs.join(" "); + const ellipsis = args.length > 3 ? "..." : ""; + return `(${head} ${argsText}${ellipsis})`; + } + return String(expr); + } + // Detect first-line color from AST without executing code + detectFirstLineColor() { + if (!this.ast) return; + const firstItem = Array.isArray(this.ast) && this.ast.length > 0 ? this.ast[0] : this.ast; + let colorName = null; + if (typeof firstItem === "string") { + colorName = firstItem; + } else if (Array.isArray(firstItem) && firstItem.length >= 1 && typeof firstItem[0] === "string") { + colorName = firstItem[0]; + } + if (colorName) { + const globalEnv = this.getGlobalEnv(); + if (isValidRGBString(colorName)) { + const rgbValues = parseRGBString(colorName); + if (rgbValues) { + this.firstLineColor = rgbValues; + this.persistentFirstLineColor = rgbValues; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(rgbValues); + } else if (typeof globalThis !== "undefined" && globalThis.storePersistentFirstLineColor) { + globalThis.storePersistentFirstLineColor(rgbValues); + } + return; + } + } + if (globalEnv[colorName] && typeof globalEnv[colorName] === "function") { + try { + const dummyApi = { + screen: { width: 100, height: 100 }, + isSafe: true + }; + const safeApi = new Proxy(dummyApi, { + get: (target, prop) => { + if (prop in target) return target[prop]; + return () => { + }; + } + }); + const result = globalEnv[colorName](safeApi); + const isValidColor = Array.isArray(result) || typeof result === "string" && (result.startsWith("fade:") || result === "rainbow" || result === "zebra"); + if (isValidColor) { + this.firstLineColor = colorName; + this.persistentFirstLineColor = colorName; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(colorName); + } else if (typeof globalThis !== "undefined" && globalThis.storePersistentFirstLineColor) { + globalThis.storePersistentFirstLineColor(colorName); + } + } + } catch (e2) { + } + } else if (colorName.match(/^c\d+$/)) { + const colorIndex = parseInt(colorName.substring(1)); + if (staticColorMap[colorIndex]) { + this.firstLineColor = colorName; + this.persistentFirstLineColor = colorName; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(colorName); + } + } + } else if (colorName.match(/^p\d+$/)) { + const patternIndex = parseInt(colorName.substring(1)); + if (patternIndex === 0 || patternIndex === 1) { + this.firstLineColor = colorName; + this.persistentFirstLineColor = colorName; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(colorName); + } + } + } else if (colorName.startsWith("fade:")) { + const fadeColors2 = this.parseFadeString(colorName); + if (fadeColors2 && fadeColors2.length >= 2) { + this.firstLineColor = colorName; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(colorName); + } + } + } + } + } + // Color a fade expression like "fade:red-blue-yellow" or "fade:red-blue:vertical" with each color in its own color + colorFadeExpression(fadeToken) { + if (!fadeToken.startsWith("fade:")) { + return fadeToken; + } + const parts = fadeToken.split(":"); + if (parts.length < 2) { + return fadeToken; + } + let isNeat = true; + let colorPart = parts[1]; + let direction = parts[2]; + if (parts[1] === "dirty" && parts[2]) { + isNeat = false; + colorPart = parts[2]; + direction = parts[3]; + } else if (parts[2] === "dirty") { + isNeat = false; + direction = void 0; + } else if (parts[3] === "dirty") { + isNeat = false; + direction = parts[2]; + } else if (parts.includes("dirty")) { + isNeat = false; + const filteredParts = parts.filter((p) => p !== "dirty"); + if (filteredParts.length >= 2) { + colorPart = filteredParts[1]; + direction = filteredParts[2]; + } + } else if (parts[1] === "neat" && parts[2]) { + isNeat = true; + colorPart = parts[2]; + direction = parts[3]; + } else if (parts[2] === "neat") { + isNeat = true; + direction = void 0; + } else if (parts[3] === "neat") { + isNeat = true; + direction = parts[2]; + } else if (parts.includes("neat")) { + isNeat = true; + const filteredParts = parts.filter((p) => p !== "neat"); + if (filteredParts.length >= 2) { + colorPart = filteredParts[1]; + direction = filteredParts[2]; + } + } + const colorNames = colorPart.split("-"); + let result = "\\mediumseagreen\\fade\\lime\\:"; + if (isNeat && parts.includes("neat")) { + result += "\\cyan\\neat\\lime\\:"; + } else if (!isNeat) { + result += "\\orange\\dirty\\lime\\:"; + } + for (let i2 = 0; i2 < colorNames.length; i2++) { + const colorName = colorNames[i2]; + let colorValue = "white"; + if (cssColors2 && cssColors2[colorName]) { + const rgbColor = cssColors2[colorName]; + if (Array.isArray(rgbColor) && rgbColor.length >= 3) { + colorValue = `${rgbColor[0]},${rgbColor[1]},${rgbColor[2]}`; + } + } else if (colorName.match(/^c\d+$/)) { + const index = parseInt(colorName.substring(1)); + if (staticColorMap && staticColorMap[index]) { + const rgbColor = staticColorMap[index]; + if (Array.isArray(rgbColor) && rgbColor.length >= 3) { + colorValue = `${rgbColor[0]},${rgbColor[1]},${rgbColor[2]}`; + } + } + } else if (colorName === "rainbow") { + colorValue = "RAINBOW"; + } else if (colorName === "zebra") { + colorValue = "ZEBRA"; + } + if (colorValue === "RAINBOW") { + const rainbowColors2 = ["red", "orange", "yellow", "lime", "blue", "purple", "magenta"]; + for (let charIndex = 0; charIndex < colorName.length; charIndex++) { + const charColor = rainbowColors2[charIndex % rainbowColors2.length]; + result += `\\${charColor}\\${colorName[charIndex]}`; + } + } else if (colorValue === "ZEBRA") { + const zebraColors2 = ["black", "white"]; + for (let charIndex = 0; charIndex < colorName.length; charIndex++) { + const charColor = zebraColors2[charIndex % zebraColors2.length]; + result += `\\${charColor}\\${colorName[charIndex]}`; + } + } else { + result += `\\${colorValue}\\${colorName}`; + } + if (i2 < colorNames.length - 1) { + result += "\\mediumseagreen\\-"; + } + } + if (direction) { + result += "\\lime\\:"; + const numericAngle = parseFloat(direction); + if (!isNaN(numericAngle)) { + result += `\\yellow\\${direction}`; + } else { + result += `\\cyan\\${direction}`; + } + } + return result; + } + // Helper method to parse and validate fade strings + // Helper method to validate if a string is a valid color + isValidColorString(colorStr) { + if (cssColors2[colorStr]) return true; + if (colorStr.match(/^c\d+$/)) { + const index = parseInt(colorStr.substring(1)); + return staticColorMap[index] !== void 0; + } + if (colorStr === "rainbow" || colorStr === "zebra") return true; + return false; + } + parseFadeString(fadeString) { + if (!fadeString.startsWith("fade:")) return null; + const parts = fadeString.split(":"); + if (parts.length < 2) return null; + const modifiers = /* @__PURE__ */ new Set(["neat", "dirty"]); + let colorPart = null; + for (let i2 = 1; i2 < parts.length; i2 += 1) { + const segment = parts[i2]; + if (!segment || modifiers.has(segment)) { + continue; + } + colorPart = segment; + break; + } + if (!colorPart) return null; + const colorNames = colorPart.split("-"); + if (colorNames.length < 2) return null; + const validColors = []; + for (const colorName of colorNames) { + if (this.isValidColorString(colorName)) { + if (cssColors2[colorName]) { + validColors.push(cssColors2[colorName]); + } else if (colorName.match(/^c\d+$/)) { + const index = parseInt(colorName.substring(1)); + if (staticColorMap[index]) { + validColors.push(staticColorMap[index]); + } + } else if (colorName === "rainbow") { + validColors.push([255, 0, 0]); + } else if (colorName === "zebra") { + validColors.push([0, 0, 0]); + } + } else { + return null; + } + } + const result = validColors.length >= 2 ? validColors : null; + return result; + } + // Get the background fill color for reframe operations + getBackgroundFillColor() { + if (!this.firstLineColor) { + if (typeof window !== "undefined" && window.getPersistentFirstLineColor) { + const persistentColor = window.getPersistentFirstLineColor(); + if (persistentColor) { + this.firstLineColor = persistentColor; + } + } + if (!this.firstLineColor && this.persistentFirstLineColor) { + this.firstLineColor = this.persistentFirstLineColor; + } + if (!this.firstLineColor && typeof globalThis !== "undefined" && globalThis.getPersistentFirstLineColor) { + const globalPersistentColor = globalThis.getPersistentFirstLineColor(); + if (globalPersistentColor) { + this.firstLineColor = globalPersistentColor; + } + } + if (!this.firstLineColor && this.ast) { + this.detectFirstLineColor(); + } + } + return this.firstLineColor; + } + // Clear KidLisp ink state (reset to undefined) + clearInkState() { + this.inkState = void 0; + this.inkStateSet = false; + } + // Get current KidLisp ink state + getInkState() { + return this.inkStateSet ? this.inkState : void 0; + } + // Resolve a color name/value to RGBA array [r, g, b, a] + // Used for safely filling buffers without calling wipe + resolveColorToRGBA(color3, api) { + if (Array.isArray(color3)) { + return color3.length === 3 ? [...color3, 255] : color3; + } + if (typeof color3 === "string") { + if (cssColors2 && cssColors2[color3]) { + const rgb = cssColors2[color3]; + return Array.isArray(rgb) ? [...rgb, 255] : [rgb, rgb, rgb, 255]; + } + if (color3.match(/^c\d+$/)) { + const colorIndex = parseInt(color3.substring(1)); + if (staticColorMap[colorIndex]) { + const rgb = staticColorMap[colorIndex]; + return [...rgb, 255]; + } + } + if (isValidRGBString(color3)) { + const rgb = parseRGBString(color3); + if (rgb) { + return rgb.length === 3 ? [...rgb, 255] : rgb; + } + } + } + return [0, 0, 0, 0]; + } + // Check if the AST contains microphone-related functions + containsMicrophoneFunctions(ast) { + if (!ast) return false; + if (typeof ast === "string") { + return ast === "mic"; + } + if (Array.isArray(ast)) { + return ast.some((item) => this.containsMicrophoneFunctions(item)); + } + return false; + } + // Optimize common patterns - this could be expanded for other patterns + compileOptimizedRepeat(ast) { + return null; + } + // Method to pre-compile AST for optimizations + precompileAST(ast) { + if (!Array.isArray(ast)) return ast; + const optimized = []; + for (const item of ast) { + if (Array.isArray(item)) { + const compiledOptimization = this.compileOptimizedRepeat(item); + if (compiledOptimization) { + optimized.push({ optimized: true, func: compiledOptimization }); + } else { + const expanded = this.expandFastMathMacros(item); + optimized.push(this.precompileAST(expanded)); + } + } else { + optimized.push(this.expandFastMathMacros(item)); + } + } + return optimized; + } + // Macro expansion step: convert fastmath expressions like "i*2" to ["*", "i", 2] + expandFastMathMacros(expr) { + if (typeof expr === "string") { + const parenMatch = expr.match( + /^(\([^)]+\))\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)$/ + ); + if (parenMatch) { + const [, parenExpr, op, right] = parenMatch; + const innerExpr = parenExpr.slice(1, -1).trim(); + const tokens = innerExpr.split(/\s+/).map((token) => { + if (/^\d+(?:\.\d+)?$/.test(token)) { + return parseFloat(token); + } + return token; + }); + const rightValue = /^\d+(?:\.\d+)?$/.test(right) ? parseFloat(right) : right; + return [op, tokens, rightValue]; + } + const chainedMatch = expr.match( + /^(\w+)\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)$/ + ); + if (chainedMatch) { + const [, a2, op1, b2, op2, c4] = chainedMatch; + const conv = (t2) => /^\d+(?:\.\d+)?$/.test(t2) ? parseFloat(t2) : t2; + const A = conv(a2), B = conv(b2), C = conv(c4); + const mulDiv = (op) => op === "*" || op === "/" || op === "%"; + if (!mulDiv(op1) && mulDiv(op2)) { + return [op1, A, [op2, B, C]]; + } + return [op2, [op1, A, B], C]; + } + const infixMatch = expr.match( + /^(\w+)\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)$/ + ); + if (infixMatch) { + const [, left, op, right] = infixMatch; + const rightValue = /^\d+(?:\.\d+)?$/.test(right) ? parseFloat(right) : right; + const result = [op, left, rightValue]; + return result; + } + } else if (Array.isArray(expr)) { + const expanded = []; + for (let i2 = 0; i2 < expr.length; i2++) { + const current = expr[i2]; + const next = expr[i2 + 1]; + if (Array.isArray(current) && typeof next === "string") { + const mathMatch = next.match(/^([+\-*/%])(\d+(?:\.\d+)?)$/); + if (mathMatch) { + const [, op, num] = mathMatch; + expanded.push([ + op, + this.expandFastMathMacros(current), + parseFloat(num) + ]); + i2++; + continue; + } + } + expanded.push(this.expandFastMathMacros(current)); + } + return expanded; + } + return expr; + } + // Update browser URL to show the short $prefixed code + updateBrowserUrl(shortCode, api) { + try { + if (typeof window !== "undefined" && (window.acPACK_MODE || window.location?.origin === "null")) { + return; + } + if (typeof window !== "undefined" && window.history && window.location && !api.net?.iframe) { + const currentPath2 = window.location.pathname; + const newPath = `/$${shortCode}`; + if (currentPath2 !== newPath) { + window.history.replaceState(null, "", newPath); + console.log(`%c\u{1F517} ${newPath}`, "color: #3F51B5; font-weight: bold;"); + } + } + } catch (error) { + console.warn("Failed to update browser URL:", error); + } + } + // Generate QR code as a data URI for a given code + async generateQRDataUri(code2) { + try { + const url = `https://prompt.ac/$${code2}`; + const { ErrorCorrectLevel: ErrorCorrectLevel2 } = await import("../dep/@akamfoad/qr/qr.mjs"); + const cells = qrcode(url, { errorCorrectLevel: ErrorCorrectLevel2.M }).modules; + const cellSize = 4; + const qrSize = cells.length * cellSize; + if (typeof document === "undefined") { + console.warn("\u26A0\uFE0F QR generation requires browser environment (document not available)"); + return null; + } + const canvas = document.createElement("canvas"); + canvas.width = qrSize; + canvas.height = qrSize; + const ctx = canvas.getContext("2d"); + for (let y = 0; y < cells.length; y++) { + for (let x = 0; x < cells.length; x++) { + const isBlack = cells[y][x]; + ctx.fillStyle = isBlack ? "black" : "white"; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } + return canvas.toDataURL("image/png"); + } catch (error) { + console.warn("Failed to generate QR data URI:", error); + return null; + } + } + // Cache kidlisp source code for QR generation + async cacheKidlispSource(source, api) { + if (this.isLispFile) { + return; + } + const alreadyCached = this.cachedCode || getCachedCode(source); + if (alreadyCached || isCachingInProgress(source)) { + return; + } + const cacheEnabled = api.store?.["kidlisp:cache-enabled"] !== false; + if (!cacheEnabled || !source || source.trim().length === 0) { + return; + } + setCachingInProgress(source, true); + try { + const isObjktMode = checkPackMode(); + if (isObjktMode) { + const simpleHash = source.split("").reduce((a2, b2) => { + a2 = (a2 << 5) - a2 + b2.charCodeAt(0); + return a2 & a2; + }, 0); + const teiaCode = Math.abs(simpleHash).toString(36).substring(0, 8); + this.shortUrl = `teia/$${teiaCode}`; + this.cachedCode = teiaCode; + setCachedCode(source, teiaCode); + setCachingInProgress(source, false); + return; + } + if (typeof window !== "undefined" && window.acSPIDER) { + const simpleHash = source.split("").reduce((a2, b2) => { + a2 = (a2 << 5) - a2 + b2.charCodeAt(0); + return a2 & a2; + }, 0); + const spiderCode = Math.abs(simpleHash).toString(36).substring(0, 8); + this.shortUrl = `spider/$${spiderCode}`; + this.cachedCode = spiderCode; + setCachedCode(source, spiderCode); + setCachingInProgress(source, false); + return; + } + const loc = typeof window !== "undefined" ? window.location : typeof self !== "undefined" ? self.location : null; + if (loc) { + const urlParams = new URLSearchParams(loc.search); + if (urlParams.has("nocache")) { + setCachingInProgress(source, false); + return; + } + } + const headers2 = { "Content-Type": "application/json" }; + try { + let token = api.kidlispAuthToken; + if (!token) { + token = await api.authorize(); + } + if (token) { + headers2.Authorization = `Bearer ${token}`; + log.auth.debug("Using auth token for store-kidlisp"); + } + } catch (err) { + } + const endpoints = ["/api/store-kidlisp"]; + if (typeof window !== "undefined") { + const originHost = window.location?.host || ""; + if (!originHost.includes("aesthetic.computer")) { + endpoints.push("https://aesthetic.computer/api/store-kidlisp"); + } + } else { + endpoints.push("https://aesthetic.computer/api/store-kidlisp"); + } + let response; + let lastError; + for (const endpoint of endpoints) { + const attemptLabel = endpoint.includes("aesthetic.computer") ? "production" : "local"; + log.store.debug(`store-kidlisp attempt (${attemptLabel}):`, endpoint); + if (api.kidlispCreateCode) { + console.log("\u{1F4BE} Caching NEW code:", { + sourceLength: source.length, + sourcePreview: source.substring(0, 100), + endpoint + }); + } + try { + const attempt = await fetch(endpoint, { + method: "POST", + headers: headers2, + body: JSON.stringify({ source }) + }); + if (attempt.ok) { + response = attempt; + log.store.success(`store-kidlisp success via ${attemptLabel}`); + break; + } + lastError = new Error(`HTTP ${attempt.status}: ${attempt.statusText || "Unknown"} (${endpoint})`); + log.store.warn(`store-kidlisp ${attemptLabel} failed:`, attempt.status, attempt.statusText); + } catch (err) { + lastError = err; + log.store.warn(`store-kidlisp ${attemptLabel} fetch error:`, err?.message || err); + } + } + if (response?.ok) { + const data = await response.json(); + this.shortUrl = `aesthetic.computer/$${data.code}`; + this.cachedCode = data.code; + setCachedCode(source, data.code); + const { logKidlispCode } = await import("./headers.mjs"); + logKidlispCode(source, data.code, api.dark, { quiet: api.inIframe }); + if (api.kidlispCreateCode) { + console.log("\u{1F680} createCode flag detected, sending code to parent..."); + console.log("\u{1F4E6} Code to send:", data.code); + let qrDataUri = null; + if (api.send) { + api.send({ + type: "post-to-parent", + content: { + type: "kidlisp-code-created", + code: data.code, + qr: qrDataUri + } + }); + api.send({ + type: "post-to-parent", + content: { + type: "setCode", + value: data.code, + qr: qrDataUri + } + }); + console.log("\u2705 Messages sent to parent via api.send"); + } else { + console.warn("\u26A0\uFE0F api.send not available"); + } + } + this.updateBrowserUrl(data.code, api); + } else { + log.store.warn("store-kidlisp failed on all endpoints:", lastError?.message || lastError); + const fallbackCode = Math.abs(source.split("").reduce((hash, ch) => { + hash = (hash << 5) - hash + ch.charCodeAt(0); + return hash & hash; + }, 0)).toString(36).substring(0, 8); + this.shortUrl = `local/$${fallbackCode}`; + this.cachedCode = fallbackCode; + setCachedCode(source, fallbackCode); + log.store.warn(`Using local fallback code $${fallbackCode} (not stored remotely)`); + } + } catch (error) { + } finally { + setCachingInProgress(source, false); + } + } + // Parse and evaluate a lisp source module + // into a running aesthetic computer piece. + module(source, isLispFile = false) { + if (!this.codeSubstitutionDepth) this.codeSubstitutionDepth = 0; + if (this.codeSubstitutionDepth > 5) { + console.warn(`\u274C KidLisp: Maximum $code substitution depth exceeded, preventing infinite recursion`); + return null; + } + resetZebraCache(); + const sourceChanged = this.currentSource !== source; + this.currentSource = source; + const shouldClearOnce = !isLispFile || sourceChanged; + this.reset(shouldClearOnce, sourceChanged); + this.firstLineColor = null; + this.isLispFile = isLispFile; + if (!this.preserveEmbeddedLayers) { + this.clearEmbeddedLayerCache(); + if (sourceChanged) { + this.clearBakedLayers(); + } + } else { + console.log(`\u{1F512} HEADLESS: Preserving ${this.embeddedLayers?.length || 0} embedded layers (preserveEmbeddedLayers=true)`); + } + const chaosResult = isChaoticSource(source); + if (chaosResult.isChaotic) { + console.log(`\u{1F300} Chaos mode detected (${Math.round(chaosResult.confidence * 100)}% confidence): ${chaosResult.reason}`); + this.chaosMode = { + active: true, + source, + confidence: chaosResult.confidence, + reason: chaosResult.reason, + stats: chaosResult.stats + }; + return [["__chaos__", source]]; + } + this.chaosMode = null; + perfStart("parse"); + const parsed = this.parse(source); + perfEnd("parse"); + if (parsed.length === 1) { + } + if (!this.isEmbeddedContext && parsed.length === 1 && // Only substitute if it's a bare $code reference, not a function call with args + (typeof parsed[0] === "string" && parsed[0].startsWith("$") || Array.isArray(parsed[0]) && parsed[0].length === 1 && typeof parsed[0][0] === "string" && parsed[0][0].startsWith("$"))) { + const cacheCode = typeof parsed[0] === "string" ? parsed[0] : parsed[0][0]; + const cacheId = cacheCode.slice(1); + console.log(`\u{1F517} KidLisp: Detected single $code (${cacheCode}), fetching cached source...`); + if (/^[0-9A-Za-z]{3,12}$/.test(cacheId)) { + try { + if (globalCodeCache.has(cacheId)) { + const cachedSource = globalCodeCache.get(cacheId); + console.log(`\u2705 Found cached source in memory for ${cacheCode}, substituting...`); + this.codeSubstitutionDepth++; + const result = this.module(cachedSource, isLispFile); + this.codeSubstitutionDepth--; + return result; + } else { + if (this.isEmbeddedContext) { + console.warn(`\u26A0\uFE0F $code ${cacheCode} not in cache and embedded context - skipping network fetch`); + } else { + console.log(`\u23F3 Cache not in memory for ${cacheCode}, starting async fetch...`); + getCachedCodeMultiLevel(cacheId).then((cachedSource) => { + if (cachedSource) { + console.log(`\u2705 Loaded ${cacheCode} from network/storage, cached for future use`); + globalCodeCache.set(cacheId, cachedSource); + } + }).catch((error) => { + console.warn(`\u274C Failed to load cached source for ${cacheCode}:`, error); + }); + if (typeof window !== "undefined") { + console.log(`\u{1F50D} DEBUG: Navigating to ${cacheCode} (window exists)`); + window.location.href = `/${cacheCode}`; + return null; + } else { + console.log(`\u{1F50D} DEBUG: No window object, not navigating (returning null anyway to prevent running stale code)`); + return null; + } + } + } + } catch (error) { + console.warn(`\u274C Error processing ${cacheCode}:`, error); + if (typeof window !== "undefined") { + window.location.href = `/${cacheCode}`; + return null; + } + } + } else { + console.warn(`\u274C Invalid cache ID format: ${cacheId}`); + if (typeof window !== "undefined") { + window.location.href = `/${cacheCode}`; + return null; + } + } + } + perfStart("ast-copy"); + this.ast = JSON.parse(JSON.stringify(parsed)); + perfEnd("ast-copy"); + perfStart("precompile"); + this.ast = this.precompileAST(this.ast); + perfEnd("precompile"); + this.initializeSyntaxHighlighting(source); + this.cachedOwnerHandle = null; + this.cachedOwnerSub = null; + return { + boot: ({ wipe, params, clock, screen: screen2, sound: sound2, delay, pieceCount, net, backgroundFill, fps, colon }) => { + this.lastUsedApi = { wipe, params, clock, screen: screen2, sound: sound2, delay, pieceCount, net, backgroundFill, fps }; + clock?.resync?.(); + net?.preloadTypeface?.("MatrixChunky8"); + this.forcedFps = null; + this.targetFps = 60; + if (colon && colon.length > 0) { + const colonFps = parseFloat(colon[0]); + if (!isNaN(colonFps) && colonFps > 0 && colonFps <= 240 && /^\d+(\.\d+)?$/.test(String(colon[0]))) { + this.targetFps = colonFps; + this.forcedFps = colonFps; + } + } + if (fps && typeof fps === "function") { + fps(this.targetFps); + if (!getPackMode()) { + log.lisp.debug(`Default FPS set to: ${this.targetFps}${this.forcedFps ? " (forced)" : ""}`); + } + } + this.globalDef.paramA = params[0]; + this.globalDef.paramB = params[1]; + this.globalDef.paramC = params[2]; + if (sound2?.microphone) { + this.microphoneApi = sound2.microphone; + const usesMicrophone = this.containsMicrophoneFunctions(this.ast); + if (usesMicrophone && this.microphoneApi.permission === "granted" && sound2.enabled?.()) { + console.log( + "\u{1F3A4} Boot: Auto-connecting microphone (piece uses mic functions)" + ); + delay(() => { + this.microphoneApi.connect(); + }, 15); + } else if (usesMicrophone) { + console.log( + "\u{1F3A4} Boot: Piece uses microphone but permission not granted or sound disabled" + ); + } + } else { + } + if (!this.firstLineColor && this.ast) { + this.detectFirstLineColor(); + } + this.needsInitialWipe = true; + if (this.firstLineColor) { + wipe(this.firstLineColor); + } else { + wipe("erase"); + } + this.inkState = void 0; + this.inkStateSet = false; + }, + paint: ($) => { + if (typeof self !== "undefined" && self.__gpuFailoverOccurred) { + self.__gpuFailoverOccurred = false; + this.frameCount = 0; + this.needsInitialWipe = true; + console.log("\u{1F3AE} GPU failover detected \u2014 resetting piece for clean feedback loop"); + } + this.startFrame(); + const cacheDelayFrames = 0; + if (!this.isEmbeddedContext && this.frameCount >= cacheDelayFrames && !this.cachedCode && !this.cacheInitiated) { + this.cacheInitiated = true; + if ($.kidlispCreateCode) { + console.log("\u{1F3A8} paint() triggering cache for NEW code:", { + sourceLength: source.length, + sourcePreview: source.substring(0, 100) + }); + } + this.cacheKidlispSource(source, $); + } + this.clearSyntaxSignals(); + this.updateHUDWithSyntaxHighlighting($); + if (this.perf.enabled) { + this.perf.evalCallCount = 0; + this.perf.bodyProcessCount = 0; + if (this.frameCache) { + this.frameCache.clear(); + } + } + if (this.inkStateSet && this.inkState !== void 0) { + $.ink?.(this.inkState); + } else { + $.ink?.(void 0); + } + perfStart("frame-evaluation"); + try { + this.postEmbedCommands = []; + this.postCompositeCommands = []; + this.inEmbedPhase = false; + this.scanAndMarkEmbeddedCodes(source); + const totalFrameStart = performance.now(); + if (!this.performanceMode) this.performanceMode = { enabled: false, lastCheck: 0 }; + if (performance.now() - this.performanceMode.lastCheck > 1e3) { + const fps = $.system?.fps || 60; + this.performanceMode.enabled = fps < 30; + this.performanceMode.lastCheck = performance.now(); + if (this.performanceMode.enabled) { + } + } + this.frameCache = /* @__PURE__ */ new Map(); + this.localEnvLevel = 0; + this.localEnv = this.localEnvStore[this.localEnvLevel]; + const mainEvalStart = performance.now(); + const displayPixels = $.screen.pixels; + const screen2 = { + width: $.screen.width, + height: $.screen.height, + pixels: displayPixels + }; + this.displayBuffer = screen2; + if (this.needsInitialWipe) { + if (this.firstLineColor) { + $.wipe(this.firstLineColor); + } else { + if (screen2 && screen2.pixels) { + screen2.pixels.fill(0); + } + } + this.needsInitialWipe = false; + } + if (!this.layer0) { + this.layer0 = { + width: screen2.width, + height: screen2.height, + pixels: new Uint8ClampedArray(screen2.width * screen2.height * 4) + }; + if (this.firstLineColor) { + const color3 = this.resolveColorToRGBA(this.firstLineColor, $); + if (color3) { + const pixels2 = this.layer0.pixels; + for (let i2 = 0; i2 < pixels2.length; i2 += 4) { + pixels2[i2] = color3[0]; + pixels2[i2 + 1] = color3[1]; + pixels2[i2 + 2] = color3[2]; + pixels2[i2 + 3] = color3[3] !== void 0 ? color3[3] : 255; + } + } + this.layer0NeedsFirstLineWipe = true; + } + } else if (this.layer0.width !== screen2.width || this.layer0.height !== screen2.height) { + const oldPixels = this.layer0.pixels; + const oldWidth = this.layer0.width; + const oldHeight = this.layer0.height; + this.layer0.width = screen2.width; + this.layer0.height = screen2.height; + this.layer0.pixels = new Uint8ClampedArray(screen2.width * screen2.height * 4); + if (this.firstLineColor) { + const color3 = this.resolveColorToRGBA(this.firstLineColor, $); + if (color3) { + const pixels2 = this.layer0.pixels; + for (let i2 = 0; i2 < pixels2.length; i2 += 4) { + pixels2[i2] = color3[0]; + pixels2[i2 + 1] = color3[1]; + pixels2[i2 + 2] = color3[2]; + pixels2[i2 + 3] = color3[3] !== void 0 ? color3[3] : 255; + } + } + } + if (oldPixels && oldPixels.length > 0 && !(oldPixels.buffer && oldPixels.buffer.detached)) { + const copyWidth = Math.min(oldWidth, screen2.width); + const copyHeight = Math.min(oldHeight, screen2.height); + for (let y = 0; y < copyHeight; y++) { + const srcOffset = y * oldWidth * 4; + const destOffset = y * screen2.width * 4; + const rowLength = copyWidth * 4; + if (srcOffset + rowLength <= oldPixels.length && destOffset + rowLength <= this.layer0.pixels.length) { + this.layer0.pixels.set(oldPixels.subarray(srcOffset, srcOffset + rowLength), destOffset); + } + } + } + } + if (screen2 && screen2.pixels) { + $.unmask(); + if (this.bakes && this.bakes[0]) { + const aboutToClearBake = screen2.pixels === this.bakes[0].pixels; + } + if (this.firstLineColor) { + $.wipe(this.firstLineColor); + } else { + screen2.pixels.fill(0); + } + } + if (this.bakes) { + this.currentBakeIndex = -1; + for (let i2 = 0; i2 < this.bakes.length; i2++) { + const bakeLayer = this.bakes[i2]; + if (bakeLayer && (bakeLayer.width !== screen2.width || bakeLayer.height !== screen2.height)) { + const oldPixels = bakeLayer.pixels; + const oldWidth = bakeLayer.width; + const oldHeight = bakeLayer.height; + const newPixels = new Uint8ClampedArray(screen2.width * screen2.height * 4); + newPixels.fill(0); + if (oldPixels && oldPixels.length > 0 && !(oldPixels.buffer && oldPixels.buffer.detached)) { + const copyWidth = Math.min(oldWidth, screen2.width); + const copyHeight = Math.min(oldHeight, screen2.height); + for (let y = 0; y < copyHeight; y++) { + const srcOffset = y * oldWidth * 4; + const destOffset = y * screen2.width * 4; + const rowLength = copyWidth * 4; + if (srcOffset + rowLength <= oldPixels.length && destOffset + rowLength <= newPixels.length) { + newPixels.set(oldPixels.subarray(srcOffset, srcOffset + rowLength), destOffset); + } + } + } + bakeLayer.width = screen2.width; + bakeLayer.height = screen2.height; + bakeLayer.pixels = newPixels; + bakeLayer.burned = false; + } + } + } + this.burnedBuffer = null; + this.preserveLayer0NextFrame = false; + $.page(this.layer0); + if (!$.screen) { + $.screen = { width: this.layer0.width, height: this.layer0.height, pixels: this.layer0.pixels }; + } else { + $.screen.width = this.layer0.width; + $.screen.height = this.layer0.height; + $.screen.pixels = this.layer0.pixels; + } + if (this.layer0NeedsFirstLineWipe && this.firstLineColor) { + try { + $.wipe(this.firstLineColor); + } catch (err) { + console.warn("\u26A0\uFE0F layer0 first-line wipe failed:", err?.message); + } + if ($.backgroundFill) { + try { + $.backgroundFill(this.firstLineColor); + } catch (_) { + } + } + this.layer0NeedsFirstLineWipe = false; + } + withKidlispConsoleCapture(() => this.evaluate(this.ast, $, void 0, void 0, true)); + if (this.currentBakeIndex >= 0 && this.bakes) { + const currentPixels = this.bakes[this.currentBakeIndex] ? Array.from(this.bakes[this.currentBakeIndex].pixels).filter((_, i2) => i2 % 4 === 3 && this.bakes[this.currentBakeIndex].pixels[i2] > 0).length : 0; + this.currentBakeIndex++; + const width2 = $.screen?.width || 256; + const height2 = $.screen?.height || 256; + const bakeBuffer = { + width: width2, + height: height2, + pixels: new Uint8ClampedArray(width2 * height2 * 4) + }; + bakeBuffer.pixels.fill(0); + this.bakes[this.currentBakeIndex] = bakeBuffer; + this.activeBakeBuffer = bakeBuffer; + } + if (VERBOSE) console.log("\u{1F3AC} Finished evaluation"); + const mainEvalTime = performance.now() - mainEvalStart; + this.inEmbedPhase = true; + const embedStart = performance.now(); + const screenBeforeEmbeds = { + width: $.screen.width, + height: $.screen.height, + pixels: $.screen.pixels + // Save the actual buffer reference + }; + if (this.embeddedLayers && this.embeddedLayers.length > 0) { + const frameValue = $.frame || this.frameCount || 0; + this.embeddedLayers.forEach((embeddedLayer, index) => { + if (embeddedLayer.lastFrameEvaluated === frameValue) return; + const shouldEvaluate = this.shouldLayerExecuteThisFrame($, embeddedLayer); + this.renderSingleLayer($, embeddedLayer, frameValue, shouldEvaluate); + }); + } + $.page(screenBeforeEmbeds); + $.screen.width = screenBeforeEmbeds.width; + $.screen.height = screenBeforeEmbeds.height; + $.screen.pixels = screenBeforeEmbeds.pixels; + const embedTime = performance.now() - embedStart; + const totalFrameTime = performance.now() - totalFrameStart; + this.inEmbedPhase = false; + this.postEmbedCommands.forEach((cmd, i2) => { + try { + cmd.func(...cmd.args); + } catch (err) { + console.error(`Error executing post-embed command ${cmd.name}:`, err); + } + }); + this.postEmbedCommands = []; + $.page(screen2); + $.unmask(); + if ($.setEraseTarget) $.setEraseTarget(null, 0); + $.screen.width = screen2.width; + $.screen.height = screen2.height; + $.screen.pixels = screen2.pixels; + const hasBurnedBuffer = this.burnedBuffer && this.burnedBuffer.pixels; + if (hasBurnedBuffer) { + if (this.burnedBuffer.width === screen2.width && this.burnedBuffer.height === screen2.height) { + try { + $.paste(this.burnedBuffer, 0, 0, 1, true); + } catch (error) { + console.warn(`\u26A0\uFE0F Error pasting burnedBuffer:`, error.message); + } + } else { + console.warn(`\u26A0\uFE0F Skipping burnedBuffer paste due to dimension mismatch: burned=${this.burnedBuffer.width}x${this.burnedBuffer.height}, screen=${screen2.width}x${screen2.height}`); + } + if (this.embeddedLayers && this.embeddedLayers.length > 0) { + const layersForComposite = []; + for (let i2 = 0, len5 = this.embeddedLayers.length; i2 < len5; i2++) { + const embeddedLayer = this.embeddedLayers[i2]; + if (embeddedLayer.buffer && embeddedLayer.buffer.pixels) { + layersForComposite.push({ + pixels: embeddedLayer.buffer.pixels, + width: embeddedLayer.buffer.width, + height: embeddedLayer.buffer.height, + x: typeof embeddedLayer.x === "number" ? Math.round(embeddedLayer.x) : 0, + y: typeof embeddedLayer.y === "number" ? Math.round(embeddedLayer.y) : 0, + alpha: typeof embeddedLayer.alpha === "number" ? embeddedLayer.alpha : 255 + }); + } + } + if (layersForComposite.length > 0 && $.compositeLayers) { + try { + $.compositeLayers(layersForComposite); + } catch (error) { + console.warn(`\u26A0\uFE0F Error in batch composite (burn path), falling back:`, error.message); + for (let i2 = 0; i2 < layersForComposite.length; i2++) { + const layer = layersForComposite[i2]; + this.pasteWithAlpha($, { pixels: layer.pixels, width: layer.width, height: layer.height }, layer.x, layer.y, layer.alpha); + } + } + } else if (layersForComposite.length > 0) { + for (let i2 = 0; i2 < layersForComposite.length; i2++) { + const layer = layersForComposite[i2]; + try { + this.pasteWithAlpha($, { pixels: layer.pixels, width: layer.width, height: layer.height }, layer.x, layer.y, layer.alpha); + } catch (error) { + console.warn(`\u26A0\uFE0F Error pasting embedded layer ${i2} (burn path):`, error.message, error.stack); + } + } + } + } + } else { + if (this.layer0 && this.layer0.pixels) { + if (this.layer0.width === screen2.width && this.layer0.height === screen2.height) { + if (this.frameCount % 8 === 0 && this.frameCount <= 64) { + let nonZero = 0; + const l0 = this.layer0.pixels; + const step = Math.max(1, l0.length / 200 | 0); + for (let i2 = 0; i2 < l0.length; i2 += step) { + if (l0[i2] !== 0) nonZero++; + } + const screenNZ = (() => { + let nz = 0; + const sp = screen2.pixels; + const st = Math.max(1, sp.length / 200 | 0); + for (let i2 = 0; i2 < sp.length; i2 += st) { + if (sp[i2] !== 0) nz++; + } + return nz; + })(); + console.log(`\u{1F3A8} [f${this.frameCount}] layer0: ${this.layer0.width}x${this.layer0.height} nonZero=${nonZero}/200 | screen: ${screen2.width}x${screen2.height} nonZero=${screenNZ}/200 | embeds=${this.embeddedLayers?.length || 0} bakes=${this.bakes?.length || 0}`); + } + try { + this.pasteWithAlpha($, this.layer0, 0, 0, 255, false); + } catch (error) { + console.warn(`\u26A0\uFE0F Error pasting layer0:`, error.message); + } + } else { + console.warn(`\u26A0\uFE0F Skipping layer0 paste due to dimension mismatch: layer0=${this.layer0.width}x${this.layer0.height}, screen=${screen2.width}x${screen2.height}`); + } + } + if (this.bakes) { + for (let i2 = 0, len5 = this.bakes.length; i2 < len5; i2++) { + const bakeLayer = this.bakes[i2]; + if (bakeLayer && bakeLayer.pixels && !bakeLayer.burned) { + if (bakeLayer.width === screen2.width && bakeLayer.height === screen2.height) { + try { + $.paste(bakeLayer, 0, 0); + } catch (error) { + console.warn(`\u26A0\uFE0F Error pasting bake layer ${i2}:`, error.message); + } + if (bakeLayer.eraseMask) { + const dst = screen2.pixels; + const mask2 = bakeLayer.eraseMask; + const totalPixels = bakeLayer.width * bakeLayer.height; + for (let p = 0; p < totalPixels; p++) { + if (mask2[p] > 0) { + const di = p * 4; + const normalAlpha = 1 - mask2[p] / 255; + dst[di + 3] = dst[di + 3] * normalAlpha + 0.5 | 0; + if (dst[di + 3] === 0) { + dst[di] = 32; + dst[di + 1] = 32; + dst[di + 2] = 32; + } + } + } + } + } else { + console.warn(`\u26A0\uFE0F Skipping bake layer ${i2} paste due to dimension mismatch: bake=${bakeLayer.width}x${bakeLayer.height}, screen=${screen2.width}x${screen2.height}`); + } + } + } + } + if (this.embeddedLayers && this.embeddedLayers.length > 0) { + const layersForComposite = []; + for (let i2 = 0, len5 = this.embeddedLayers.length; i2 < len5; i2++) { + const embeddedLayer = this.embeddedLayers[i2]; + if (embeddedLayer.buffer && embeddedLayer.buffer.pixels) { + layersForComposite.push({ + pixels: embeddedLayer.buffer.pixels, + width: embeddedLayer.buffer.width, + height: embeddedLayer.buffer.height, + x: typeof embeddedLayer.x === "number" ? Math.round(embeddedLayer.x) : 0, + y: typeof embeddedLayer.y === "number" ? Math.round(embeddedLayer.y) : 0, + alpha: typeof embeddedLayer.alpha === "number" ? embeddedLayer.alpha : 255 + }); + } + } + if (layersForComposite.length > 0 && $.compositeLayers) { + try { + $.compositeLayers(layersForComposite); + } catch (error) { + console.warn(`\u26A0\uFE0F Error in batch composite, falling back to individual pastes:`, error.message); + for (let i2 = 0; i2 < layersForComposite.length; i2++) { + const layer = layersForComposite[i2]; + this.pasteWithAlpha($, { pixels: layer.pixels, width: layer.width, height: layer.height }, layer.x, layer.y, layer.alpha); + } + } + } else if (layersForComposite.length > 0) { + for (let i2 = 0; i2 < layersForComposite.length; i2++) { + const layer = layersForComposite[i2]; + try { + this.pasteWithAlpha($, { pixels: layer.pixels, width: layer.width, height: layer.height }, layer.x, layer.y, layer.alpha); + } catch (error) { + console.warn(`\u26A0\uFE0F Error pasting embedded layer ${i2}:`, error.message, error.stack); + } + } + } + } + } + if (this.postCompositeCommands && this.postCompositeCommands.length > 0) { + if (this.frameCount % 8 === 0 && this.frameCount <= 64) { + console.log(`\u{1F3A8} [f${this.frameCount}] post-composite: [${this.postCompositeCommands.map((c4) => c4.name).join(", ")}]`); + } + for (const cmd of this.postCompositeCommands) { + try { + cmd.func(); + } catch (err) { + console.error(`Error executing post-composite command ${cmd.name}:`, err); + } + } + this.postCompositeCommands = []; + } + if (this.perf.showHUD) { + this.renderPerformanceHUD($); + } + } catch (err) { + console.error("\u26D4 Evaluation failure:", err); + } + perfEnd("frame-evaluation"); + if (PERF_LOG && perfLogs.length > 0) { + $.ink?.("yellow"); + perfLogs.forEach((log3, index) => { + $.write?.(log3, { x: 2, y: 24 + index * 12 }, "black"); + }); + } + $.page(this.displayBuffer); + $.screen.width = this.displayBuffer.width; + $.screen.height = this.displayBuffer.height; + $.screen.pixels = this.displayBuffer.pixels; + const isProjectionMode = typeof location !== "undefined" && location.search?.indexOf("nolabel") > -1 || typeof window !== "undefined" && !!window.acKEEP_MODE; + if (this.lastValidationErrors && this.lastValidationErrors.length > 0 && !isProjectionMode) { + const errorText = `\u274C ${this.lastValidationErrors.join(", ")}`; + const textY = $.screen.height - 10; + $.ink("red").write(errorText, { x: 2, y: textY }, void 0, void 0, false, "MatrixChunky8"); + } + if (this.frameCount === 1) { + postToParent({ type: "kidlisp-first-frame", timestamp: performance.now() }); + } + if (isKidlispTraceEnabled() && this.frameCount === 1) { + postExecutionTrace(); + } + this.endFrame(); + }, + sim: ({ sound: sound2 }) => { + this.frameCount++; + resetRainbowCache(); + sound2.speaker?.poll(); + if (!this.microphoneApi && sound2?.microphone) { + this.microphoneApi = sound2.microphone; + } + if (this.microphoneApi) { + this.microphoneApi.poll?.(); + if (this.microphoneApi.connected && !this.microphoneConnected) { + this.microphoneConnected = true; + console.log("\u{1F3A4} Microphone connected in kidlisp"); + } + if (this.microphoneApi.connected) { + this.globalDef.mic = this.microphoneApi.amplitude || 0; + } else { + this.globalDef.mic = this.getMicDefaultValue(); + } + } else { + this.globalDef.mic = this.getMicDefaultValue(); + } + if (typeof window !== "undefined" && window.__acAudioData) { + const ad = window.__acAudioData; + this.updateAudioGlobals({ + ...ad, + __source: "module:window" + }); + } + if (this.frameCount % 60 === 0 && this.microphoneApi?.connected) { + console.log("\u{1F3A4} Mic amplitude:", this.microphoneApi.amplitude); + } + this.updateMelodies({ sound: sound2 }); + }, + act: ({ event: e2, api }) => { + if (e2.is("touch")) { + this.globalDef.down = 1; + this.globalDef.penx = e2.x ?? 0; + this.globalDef.peny = e2.y ?? 0; + } + if (e2.is("draw")) { + this.globalDef.penx = e2.x ?? this.globalDef.penx; + this.globalDef.peny = e2.y ?? this.globalDef.peny; + } + if (e2.is("lift")) { + this.globalDef.down = 0; + api.needsPaint(); + if (this.lifter) + withKidlispConsoleCapture(() => this.evaluate(this.lifter, api)); + } + if (e2.is("touch")) { + const hasTapHandler = this.tapper !== null; + const hasDrawHandler = this.drawer !== null; + if (hasTapHandler || hasDrawHandler) { + api.needsPaint(); + if (hasTapHandler) this.tap(api); + } else { + if (!getPackMode()) { + const skipSound = !this.isLispFile; + api.toggleHUD(false, skipSound); + } + } + } + if (e2.is("draw")) { + const hasDrawHandler = this.drawer !== null; + if (hasDrawHandler) { + api.needsPaint(); + this.draw(api, { dy: e2.delta.y, dx: e2.delta.x }); + } + } + if (e2.is("microphone-connect:success")) { + console.log("\u{1F3A4} Microphone connected successfully!"); + this.microphoneConnected = true; + } + if (e2.is("microphone-connect:failure")) { + console.warn("\u{1F3A4} Failed to connect microphone:", e2.reason); + } + if (e2.is("tape:frames")) { + const { tapeId, frames, totalFrames } = e2.content; + const tapeCode = tapeId?.startsWith("kidlisp-") ? tapeId.slice(8) : tapeId; + const tapeEmbed = this.tapeEmbeds.get(tapeCode); + if (tapeEmbed) { + console.log(`\u{1F4FC} KidLisp: Received ${frames?.length || 0} frames for tape !${tapeCode}`); + tapeEmbed.frames = frames || []; + tapeEmbed.totalFrames = totalFrames; + tapeEmbed.isLoading = false; + if (frames && frames.length > 0) { + tapeEmbed.width = frames[0].width; + tapeEmbed.height = frames[0].height; + } + api.needsPaint(); + } + } + if (e2.is("tape:preloaded")) { + const { tapeId, frameCount } = e2.content; + const tapeCode = tapeId?.startsWith("kidlisp-") ? tapeId.slice(8) : tapeId; + const tapeEmbed = this.tapeEmbeds.get(tapeCode); + if (tapeEmbed) { + console.log(`\u{1F4FC} KidLisp: Tape !${tapeCode} preloaded with ${frameCount} frames`); + } + } + if (e2.is("tape:preload-error")) { + const { tapeId, error } = e2.content; + const tapeCode = tapeId?.startsWith("kidlisp-") ? tapeId.slice(8) : tapeId; + const tapeEmbed = this.tapeEmbeds.get(tapeCode); + if (tapeEmbed) { + console.error(`\u{1F4FC} KidLisp: Failed to load tape !${tapeCode}:`, error); + tapeEmbed.isLoading = false; + tapeEmbed.loadError = error; + } + } + } + }; + } + // 🎤 Get fun default mic value when microphone is not connected + // Returns a value from 0 to 1 based on the selected animation mode + getMicDefaultValue() { + const { frameCount } = this; + switch (this.micDefaultMode) { + case "sine": + return 0.2 + Math.sin(frameCount * 0.05) * 0.1; + case "noise": + return Math.random() * 0.15; + // Low amplitude noise + case "pulse": + const pulseFreq = 240; + const pulsePhase = frameCount % pulseFreq; + return pulsePhase < 30 ? Math.sin(pulsePhase / 30 * Math.PI) * 0.4 : 0; + case "static": + return 0; + default: + return 0.2 + Math.sin(frameCount * 0.05) * 0.1; + } + } + // 🎤 Request microphone connection (called when mic is accessed) + requestMicrophoneConnection(api) { + if (this.microphoneApi && !this.microphoneApi.connected) { + if (this.microphoneApi.permission === "granted" && api?.sound?.enabled?.()) { + console.log("\u{1F3A4} Auto-connecting microphone (mic accessed)"); + this.microphoneApi.connect(); + } else if (this.microphoneApi.permission !== "granted") { + if (!this.micPermissionRequested) { + this.microphoneApi.requestPermission?.(); + console.log("\u{1F3A4} Microphone permission needed for 'mic' global - please grant access"); + this.micPermissionRequested = true; + } + } + } + } + // Seeded random number generator for isolated state per instance + seededRandom() { + this.randomState = (this.randomState * 1664525 + 1013904223) % 4294967296; + return this.randomState / 4294967296; + } + // 🌀 CHAOS MODE EVALUATOR + // Generate artistic visuals from invalid/random input. + // The input is hashed to create deterministic but varied output. + evaluateChaos(source, api) { + if (!api.wipe || !api.ink || !api.box) { + console.log("\u{1F300} Chaos mode: Missing required API functions"); + return; + } + const chaos = this.chaosMode; + const width2 = api.screen?.width || 256; + const height2 = api.screen?.height || 256; + let seed = 0; + for (let i2 = 0; i2 < source.length; i2++) { + seed = (seed << 5) - seed + source.charCodeAt(i2); + seed = seed & seed; + } + const seededRand = () => { + seed = (seed * 1664525 + 1013904223) % 4294967296; + return (seed >>> 0) / 4294967296; + }; + const frame = api.num?.frame || this.frameCount || 0; + const mode = Math.abs(seed) % 6; + console.log(`\u{1F300} Chaos mode rendering (mode ${mode}): "${source.substring(0, 30)}..."`); + switch (mode) { + case 0: + api.wipe(0, 0, 0); + for (let i2 = 0; i2 < source.length && i2 < 1e3; i2++) { + const char = source.charCodeAt(i2); + const x = (i2 * 7 + char) % width2; + const y = Math.floor(i2 / (width2 / 7)) * 3 % height2; + const hue = (char * 37 + frame) % 360; + const [r2, g, b2] = this.hueToRgb(hue); + api.ink(r2, g, b2); + api.box(x, y, 3, 3); + } + break; + case 1: + { + const bg = Math.floor(seededRand() * 40); + api.wipe(bg, bg, bg + 20); + const numCircles = Math.min(source.length / 5, 20); + for (let i2 = 0; i2 < numCircles; i2++) { + const radius = (i2 + 1) * (Math.min(width2, height2) / numCircles / 2); + const pulse = Math.sin((frame + i2 * 10) * 0.05) * 0.3 + 0.7; + const hue = (i2 * 30 + seed) % 360; + const [r2, g, b2] = this.hueToRgb(hue); + api.ink(Math.floor(r2 * pulse), Math.floor(g * pulse), Math.floor(b2 * pulse), 150); + this.drawChaosCircle(api, width2 / 2, height2 / 2, radius * pulse, 2); + } + } + break; + case 2: + { + api.wipe(10, 10, 30); + const step = 4; + for (let y = 0; y < height2; y += step) { + for (let x = 0; x < width2; x += step) { + const wave1 = Math.sin((x + frame) * 0.05 + seed * 0.01); + const wave2 = Math.cos((y - frame * 0.5) * 0.03 + source.length * 0.1); + const wave3 = Math.sin((x + y + frame * 0.3) * 0.02); + const intensity = (wave1 + wave2 + wave3 + 3) / 6; + const hue = (x + y + seed) % 360; + const [r2, g, b2] = this.hueToRgb(hue); + api.ink(Math.floor(r2 * intensity), Math.floor(g * intensity), Math.floor(b2 * intensity)); + api.box(x, y, step, step); + } + } + } + break; + case 3: + { + api.wipe(20, 10, 30); + const chars = source.slice(0, 100); + for (let i2 = 0; i2 < chars.length; i2++) { + const char = chars.charCodeAt(i2); + const t2 = i2 / chars.length * Math.PI * 4 + frame * 0.02; + const x1 = width2 / 2 + Math.sin(t2) * (width2 / 4); + const x2 = width2 / 2 - Math.sin(t2) * (width2 / 4); + const y = i2 / chars.length * height2; + const depth = (Math.sin(t2) + 1) / 2; + const size = 3 + depth * 5; + const [r1, g1, b1] = this.hueToRgb((char * 10 + frame) % 360); + api.ink(r1, g1, b1, Math.floor(100 + depth * 155)); + api.box(x1 - size / 2, y, size, size); + const [r2, g2, b2] = this.hueToRgb((char * 10 + 180 + frame) % 360); + api.ink(r2, g2, b2, Math.floor(100 + (1 - depth) * 155)); + api.box(x2 - size / 2, y, size, size); + if (i2 % 5 === 0) { + api.ink(100, 100, 100, 80); + const lineY = y; + const minX = Math.min(x1, x2); + const maxX = Math.max(x1, x2); + api.box(minX, lineY, maxX - minX, 1); + } + } + } + break; + case 4: + { + api.wipe(0, 10, 0); + const cols = Math.floor(width2 / 8); + for (let col = 0; col < cols; col++) { + const colSeed = seed + col * 97 & 65535; + const speed = 1 + colSeed % 3; + const offset = (frame * speed + colSeed) % (height2 + 100); + for (let i2 = 0; i2 < 15; i2++) { + const charIdx = (col + i2 * 7) % source.length; + const y = offset - i2 * 10; + if (y >= 0 && y < height2) { + const fade = 1 - i2 / 15; + const bright = Math.floor(100 + fade * 155); + api.ink(0, bright, 0); + api.box(col * 8, y, 6, 8); + } + } + } + } + break; + case 5: + // Kaleidoscope from source + default: + { + const bgHue = seed % 360; + const [bgR, bgG, bgB] = this.hueToRgb(bgHue); + api.wipe(Math.floor(bgR * 0.2), Math.floor(bgG * 0.2), Math.floor(bgB * 0.2)); + const segments = 8; + const cx = width2 / 2; + const cy = height2 / 2; + for (let i2 = 0; i2 < Math.min(source.length, 50); i2++) { + const char = source.charCodeAt(i2); + const r2 = char % 100 + 20 + Math.sin(frame * 0.02 + i2) * 20; + const baseAngle = i2 / 50 * Math.PI * 2 + frame * 0.01; + for (let seg = 0; seg < segments; seg++) { + const angle3 = baseAngle + seg / segments * Math.PI * 2; + const x = cx + Math.cos(angle3) * r2; + const y = cy + Math.sin(angle3) * r2; + const hue = (char * 7 + seg * 45 + frame) % 360; + const [pr, pg, pb] = this.hueToRgb(hue); + api.ink(pr, pg, pb, 200); + api.box(x - 2, y - 2, 4, 4); + } + } + } + break; + } + return void 0; + } + // Helper: Convert hue (0-360) to RGB + hueToRgb(hue) { + const h = hue / 60; + const c4 = 255; + const x = Math.floor(c4 * (1 - Math.abs(h % 2 - 1))); + if (h < 1) return [c4, x, 0]; + if (h < 2) return [x, c4, 0]; + if (h < 3) return [0, c4, x]; + if (h < 4) return [0, x, c4]; + if (h < 5) return [x, 0, c4]; + return [c4, 0, x]; + } + // Helper: Draw a circle using boxes (for chaos mode) + drawChaosCircle(api, cx, cy, radius, thickness) { + const steps = Math.max(16, Math.floor(radius * 0.5)); + for (let i2 = 0; i2 < steps; i2++) { + const angle3 = i2 / steps * Math.PI * 2; + const x = cx + Math.cos(angle3) * radius - thickness / 2; + const y = cy + Math.sin(angle3) * radius - thickness / 2; + api.box(x, y, thickness, thickness); + } + } + // Main parsing method - handles both single expressions and multi-line input + parse(input3) { + const parseStart = this.startTiming("parse"); + if (input3.includes(",") && !input3.includes("\n")) { + const expressions = input3.split(",").map((expr) => expr.trim()).filter((expr) => expr.length > 0); + const wrappedExpressions = expressions.map((expr) => { + if (expr.startsWith("(") && expr.endsWith(")")) { + return expr; + } + if (/^[a-zA-Z_$]\w*/.test(expr)) { + return `(${expr})`; + } + return expr; + }); + input3 = wrappedExpressions.join(" "); + } + if (input3.includes("\n")) { + const lines = input3.split("\n").map((line2) => { + const commentIndex = line2.indexOf(";"); + if (commentIndex !== -1) { + line2 = line2.substring(0, commentIndex); + } + return line2.trim(); + }).filter((line2) => line2.length > 0); + const wrappedLines = lines.map((line2, index) => { + if (line2.includes(",")) { + const expressions = line2.split(",").map((expr) => expr.trim()).filter((expr) => expr.length > 0); + const wrappedExpressions = expressions.map((expr) => { + if (expr.startsWith("(") && expr.endsWith(")")) { + return expr; + } + if (/^[a-zA-Z_$]\w*/.test(expr)) { + return `(${expr})`; + } + return expr; + }); + return wrappedExpressions.join(" "); + } + const isContinuation = index > 0 && // Line starts with an identifier but not at the beginning of the input + /^[a-zA-Z_]\w*/.test(line2) && !line2.startsWith("(") && // Previous line ends with an incomplete expression (has opening paren or is incomplete) + lines[index - 1].includes("(") && lines[index - 1].split("(").length > lines[index - 1].split(")").length; + const timingMatch = line2.match(/^(\d*\.?\d+[s]\.\.\.?)\s+(.+)$/); + if (timingMatch && !line2.startsWith("(")) { + return line2; + } + if (!line2.startsWith("(") && /^[a-zA-Z_]\w*/.test(line2) && !isContinuation) { + return `(${line2})`; + } + return line2; + }); + input3 = wrappedLines.join(" "); + } + const validationErrors = []; + const errorPositions = /* @__PURE__ */ new Set(); + let parenBalance = 0; + let lastOpenParen = -1; + for (let i2 = 0; i2 < input3.length; i2++) { + const char = input3[i2]; + if (char === "(") { + parenBalance++; + lastOpenParen = i2; + } + if (char === ")") { + parenBalance--; + if (parenBalance < 0) { + validationErrors.push("Too many closing parentheses"); + errorPositions.add(i2); + break; + } + } + } + if (parenBalance > 0) { + validationErrors.push(`Missing ${parenBalance} closing parenthes${parenBalance === 1 ? "is" : "es"}`); + if (lastOpenParen >= 0) { + errorPositions.add(lastOpenParen); + } + let tempBalance = 0; + for (let i2 = 0; i2 < input3.length; i2++) { + if (input3[i2] === "(") { + tempBalance++; + if (tempBalance > (parenBalance === 0 ? -1 : 0)) { + errorPositions.add(i2); + } + } + if (input3[i2] === ")") tempBalance--; + } + } + let lastDoubleQuote = -1; + let lastSingleQuote = -1; + let doubleQuoteCount = 0; + let singleQuoteCount = 0; + for (let i2 = 0; i2 < input3.length; i2++) { + if (input3[i2] === '"') { + doubleQuoteCount++; + lastDoubleQuote = i2; + } + if (input3[i2] === "'") { + singleQuoteCount++; + lastSingleQuote = i2; + } + } + if (doubleQuoteCount % 2 !== 0) { + validationErrors.push("Unmatched double quote"); + if (lastDoubleQuote >= 0) errorPositions.add(lastDoubleQuote); + } + if (singleQuoteCount % 2 !== 0) { + validationErrors.push("Unmatched single quote"); + if (lastSingleQuote >= 0) errorPositions.add(lastSingleQuote); + } + if (validationErrors.length > 0) { + const sourcePrefix = this.embeddedSourceId ? `[$${this.embeddedSourceId}] ` : ""; + console.error(`\u274C ${sourcePrefix}KidLisp Validation Failed:`, validationErrors.join(", ")); + if (isKidlispConsoleEnabled()) { + let firstPos = null; + if (errorPositions && errorPositions.size > 0) { + firstPos = Math.min(...Array.from(errorPositions)); + } + const loc = typeof firstPos === "number" ? kidlispOffsetToLineCol(input3, firstPos) : void 0; + postKidlispConsole( + "error", + `\u274C ${sourcePrefix}KidLisp validation failed: ${validationErrors.join(", ")}`, + loc ? { kind: "validation", loc, embeddedSource: this.embeddedSourceId } : { kind: "validation", embeddedSource: this.embeddedSourceId } + ); + } + this.lastValidationErrors = validationErrors; + this.errorPositions = errorPositions; + } else { + this.lastValidationErrors = null; + this.errorPositions = null; + } + let parserInput = input3; + if (validationErrors.length > 0 && /Missing \d+ closing parenthesis/.test(validationErrors.join(" "))) { + const missingCountMatch = validationErrors.join(" ").match(/Missing (\d+) closing parenthes/); + const missingCount = missingCountMatch ? parseInt(missingCountMatch[1], 10) : 0; + if (missingCount > 0) { + const sourcePrefix = this.embeddedSourceId ? `[$${this.embeddedSourceId}] ` : ""; + parserInput = `${input3} +${")".repeat(missingCount)}`; + console.warn(`\u26A0\uFE0F ${sourcePrefix}KidLisp auto-balanced ${missingCount} missing parenthesis${missingCount === 1 ? "" : "es"}`); + } + } + try { + const tokens = tokenizeForParser(parserInput); + const parsed = readFromTokens(tokens); + this.ast = parsed; + this.endTiming("parse", parseStart); + return parsed; + } catch (error) { + const sourcePrefix = this.embeddedSourceId ? `[$${this.embeddedSourceId}] ` : ""; + console.error(`\u274C ${sourcePrefix}KidLisp Parse Error:`, error.message); + if (isKidlispConsoleEnabled()) { + const offset = typeof error?.kidlispOffset === "number" ? error.kidlispOffset : null; + const loc = typeof offset === "number" ? kidlispOffsetToLineCol(parserInput, offset) : void 0; + postKidlispConsole( + "error", + `\u274C ${sourcePrefix}KidLisp parse error: ${error.message}`, + loc ? { kind: "parse", loc, embeddedSource: this.embeddedSourceId } : { kind: "parse", embeddedSource: this.embeddedSourceId } + ); + } + this.lastParseError = error; + this.endTiming("parse", parseStart); + return []; + } + } + // 🫵 Tap + tap(api) { + if (this.tapper) { + withKidlispConsoleCapture(() => this.evaluate(this.tapper, api)); + } + } + // ✏️ Draw + draw(api, env) { + if (this.drawer) { + withKidlispConsoleCapture(() => this.evaluate(this.drawer, api, env)); + } + } + // 🎵 Melody playback methods + parseMelodyString(melodyString) { + return parseMelody(melodyString); + } + // Register + start a melody. Shared by the `melody` and `clock` builtins + // (the latter delegates here when given a string arg — "nice synergy" with + // the clock piece). Returns the melody string (def-like: re-runs are no-ops). + runMelody(api, args = []) { + if (args.length === 0) return; + const isNum = (s2) => /^-?\d*\.?\d+$/.test(s2); + const parts = []; + for (let i2 = 0; i2 < args.length; i2++) { + const a2 = unquoteString(String(args[i2])); + if (isNum(a2)) continue; + parts.push(a2); + } + const melodyString = parts.join(" ").trim(); + if (!melodyString) return; + const melodyId = melodyString; + if (this.melodies.has(melodyId)) return; + api.clock?.resync?.(); + const parsed = parseSequentialMelody(melodyString, 4); + const melodyState = buildMelodyState(parsed, { baseTempo: MELODY_BASE_MS }); + if (!melodyState) return; + melodyState.isPlaying = false; + this.melodies.set(melodyId, melodyState); + this.melodyByString.set(melodyString, melodyState); + this.updateMelodies(api); + return melodyString; + } + // ── Global synced clock (AC time authority) ─────────────────────────────── + // Mirror clock.mjs: read the server-synced UTC time so every client agrees and + // animation/notes are musically locked. Capture the smoothed offset each frame. + _syncClock(api) { + try { + const c4 = api?.clock; + if (!c4?.time) return; + const local = Date.now(); + if (!this._lastResync || local - this._lastResync > 2e3) { + c4.resync?.(); + this._lastResync = local; + } + const t2 = c4.time(); + if (t2 && typeof t2.getTime === "function") { + this._clockOffset = t2.getTime() - Date.now(); + } + } catch { + } + } + // Synced UTC ms (falls back to local time when no clock API / static stub). + _now() { + return Date.now() + (this._clockOffset || 0); + } + // ── Multi-track melody scheduler (absolute UTC time) ────────────────────── + // Each channel/track derives its CURRENT note from the global synced clock + // (this._now()), so all clients are phase-locked and notes align with Ns timers + // (1s = 1 beat = 1 plain note). No accumulation/drift; a backgrounded tab just + // resumes at the correct absolute note. Called once per frame from sim(). + updateMelodies(api) { + this._syncClock(api); + const now = this._now(); + for (const [, ms] of this.melodies) this._advanceMelody(api, ms, now); + } + _advanceMelody(api, ms, now) { + if (!ms) return; + if (ms.type === "parallel") { + for (const ts of ms.trackStates) this._selectNote(api, ms, ts, ts.track, now); + } else if (ms.type === "sequential") { + this._advanceSequential(api, ms, now); + } else { + this._selectNote(api, ms, ms, ms.notes, now); + } + } + // Cache a cumulative-start timeline (ms offsets) for a note array on its state. + // + // The starts then get p-center corrected: a note is pulled earlier by its own + // perceptual attack lag, so what lands on the beat is where the note is HEARD + // to begin rather than the sample at which it physically starts. Without this + // a line that alternates a fast wave with a slow one plays perfectly on the + // grid and sounds like it is limping — the effect Wessel describes on p. 50 + // and asks synthesis software to give composers control over. The correction + // is mean-zero and never changes `_loopMs`, so tempo and loop alignment are + // untouched; only the spacing inside the bar moves. + _timeline(st, notes) { + if (st._starts && st._starts.length === notes.length) return; + const starts = []; + let acc = 0; + for (const n2 of notes) { + starts.push(acc); + acc += (n2.duration || 1) * MELODY_BASE_MS; + } + st._starts = applyPCenterShifts(starts, notes); + st._loopMs = acc; + } + // Pick the note for the absolute time `now` and trigger the synth on change. + _selectNote(api, ms, st, notes, now) { + if (!notes || notes.length === 0) return; + this._timeline(st, notes); + if (st._loopMs <= 0) return; + const pos = (now % st._loopMs + st._loopMs) % st._loopMs; + let idx = 0; + for (let i2 = 0; i2 < st._starts.length; i2++) { + if (st._starts[i2] <= pos) idx = i2; + else break; + } + if (st._lastIdx === idx) return; + st._lastIdx = idx; + ms.isPlaying = true; + const playhead = (idx + 1) % notes.length; + if (st === ms) ms.index = playhead; + else st.noteIndex = playhead; + this._playNote(api, ms, notes[idx], st.trackIndex || 0); + } + // Sequential `>`: a global timeline over all sequences (each loopCount×loopMs); + // map the absolute time into the active sequence, then select within it. + _advanceSequential(api, ms, now) { + const seqs = ms.sequences || []; + if (seqs.length === 0) return; + if (!ms._seqTimeline || ms._seqTimeline.length !== seqs.length) { + const segs = []; + let acc = 0; + for (const s2 of seqs) { + const loopMs = sequenceDurationBeats(s2) * MELODY_BASE_MS; + const segMs = loopMs * (s2.loopCount || 1); + segs.push({ start: acc, segMs, loopMs }); + acc += segMs; + } + ms._seqTimeline = segs; + ms._seqTotalMs = acc; + } + if (ms._seqTotalMs <= 0) return; + const pos = (now % ms._seqTotalMs + ms._seqTotalMs) % ms._seqTotalMs; + let si = 0; + for (let i2 = 0; i2 < ms._seqTimeline.length; i2++) { + if (ms._seqTimeline[i2].start <= pos) si = i2; + else break; + } + if (ms.currentSequence !== si || !ms.currentSequenceState) { + ms.currentSequence = si; + ms.currentSequenceState = buildSequenceState(seqs[si]); + } + const seg = ms._seqTimeline[si]; + const localNow = seg.loopMs > 0 ? (pos - seg.start) % seg.loopMs : 0; + const seq = ms.currentSequenceState; + if (!seq) return; + if (seq.type === "parallel") { + for (const ts of seq.trackStates) this._selectNote(api, ms, ts, ts.track, localNow); + } else { + this._selectNote(api, ms, seq, seq.notes, localNow); + } + } + // Play a single note with full per-note timbre. Rests are silent but still + // occupy their slot in the timeline. + _playNote(api, ms, noteData, trackIndex) { + const note2 = noteData.note; + if (!note2 || note2 === "rest" || !api.sound?.synth) return; + const tone = this.noteToTone(note2, noteData.octave); + const sonicMs = (noteData.sonicDuration != null ? noteData.sonicDuration : noteData.duration || 1) * MELODY_BASE_MS; + const struck = !!noteData.struck; + const opts = { + type: noteData.waveType || "sine", + tone, + duration: sonicMs / 1e3 * 0.98, + attack: struck ? 5e-3 : 0.01, + decay: struck ? 0.9 : 0.99, + volume: noteData.volume != null ? noteData.volume : 0.8 + }; + if (typeof noteData.toneShift === "number" && noteData.toneShift !== 0) { + opts.toneShift = noteData.toneShift; + } + api.sound.synth(opts); + } + // Convert note letter to frequency + noteToTone(note2, octave = null) { + return noteToTone(note2, octave); + } + // CORE OPTIMIZATION HELPER: Check if expression contains a variable + containsVariable(expr, varName) { + if (typeof expr === "string") { + return expr === varName; + } + if (typeof expr === "number") { + return false; + } + if (Array.isArray(expr)) { + return expr.some((subExpr) => this.containsVariable(subExpr, varName)); + } + return false; + } + // Helper function to create a deep copy of a form + createFormCopy(templateForm, api) { + let geometryData = api.CUBEL; + const copy8 = new api.Form( + geometryData, + // Use the appropriate geometry data + { + color: templateForm.color ? [...templateForm.color] : void 0, + alpha: templateForm.alpha + }, + { + pos: [0, 0, 0], + // Start with clean transform state + rot: [0, 0, 0], + scale: [1, 1, 1] + } + ); + copy8.primitive = templateForm.primitive; + copy8.type = templateForm.type; + copy8.texture = templateForm.texture; + copy8.colorModifier = templateForm.colorModifier; + copy8.gradients = templateForm.gradients; + return copy8; + } + // Create global environment (cached for performance) + getGlobalEnv() { + if (this.globalEnvCache) { + return this.globalEnvCache; + } + const numArg = (api, env, raw, fallback) => { + if (raw === void 0 || raw === null) return fallback; + if (typeof raw === "number") return raw; + const v2 = this.fastEval(raw, api, env || this.localEnv); + const n2 = typeof v2 === "number" ? v2 : parseFloat(v2); + return Number.isFinite(n2) ? n2 : fallback; + }; + this.globalEnvCache = { + // once: (api, args) => { + // console.log("Oncing...", args); + // if (this.drawer) this.evaluate(this.drawer, api); + // }, + now: (api, args) => { + if (args.length === 2) { + const name = unquoteString(args[0]); + if (this.globalDef.hasOwnProperty(name)) { + this.globalDef[name] = args[1]; + } else { + console.warn("\u{1F6AB}\u{1F9E0} Not defined:", name); + } + return args[1]; + } + console.error("\u2757 Invalid `now`. Wrong number of arguments."); + }, + // Program Architecture + def: (api, args, env) => { + if (args.length === 2) { + const name = unquoteString(args[0]); + if (!validIdentifierRegex.test(name)) { + return; + } + if (this.localEnvLevel > 0) { + this.localEnv[name] = args[1]; + } else { + if (!Object.prototype.hasOwnProperty.call(this.globalDef, name)) { + this.globalDef[name] = args[1]; + } else { + } + } + return args[1]; + } + console.error("\u2757 Invalid `def`. Wrong number of arguments."); + }, + die: (api, args) => { + const name = unquoteString(args[0]); + const def = this.globalDef[name]; + if (def) { + delete this.globalDef[name]; + def.kill?.(); + } + }, + later: (api, args) => { + if (args.length >= 2) { + const name = args[0]; + let params = []; + let body = null; + args.slice(1).forEach((arg, index) => { + if (Array.isArray(arg) && body === null) { + body = args.slice(index + 1); + } else if (body === null) { + params.push(arg); + } + }); + if (body === null) { + console.error("No body found in arguments for 'later' function."); + return; + } + this.globalDef[name] = { body, params }; + if (VERBOSE) { + console.log( + `Latered '${name}' as:`, + this.globalDef[name], + "with parameters:", + params + ); + } + return body; + } + }, + net: { + handles: (api) => { + const iter = { iterable: true, data: [] }; + if (!this.networkCache.handles) { + this.networkCache.handles = "loading"; + fetch("/api/handles").then((response) => response.json()).then((data) => { + this.networkCache.handles = data.handles; + iter.data = data.handles; + api.needsPaint(); + }).catch((error) => console.warn(error)); + } else if (Array.isArray(this.networkCache.handles)) { + iter.data = this.networkCache.handles; + } + return iter; + } + }, + tap: (api, args) => { + this.tapper = args; + }, + draw: (api, args) => { + this.drawer = args; + }, + // 🫳 The other half of a press. `tap` fires when the hand lands; `lift` fires + // when it leaves. A button that can be pushed but never released is not a + // button — it's a switch that only goes one way. + lift: (api, args) => { + this.lifter = args; + }, + if: (api, args, env) => { + if (!args || args.length < 1) { + console.error("\u2757 Invalid `if`. Wrong number of arguments."); + return false; + } + const evaled = this.evaluate(args[0], api, env); + if (evaled) this.evaluate(args.slice(1), api, env); + }, + once: (api, args, env) => { + if (!args || args.length < 1) { + console.error("\u2757 Invalid `once`. Wrong number of arguments."); + return; + } + const onceKey = JSON.stringify(args); + if (!this.onceExecuted.has(onceKey)) { + this.onceExecuted.add(onceKey); + let result; + for (const arg of args) { + result = this.evaluate(arg, api, env); + } + return result; + } + return void 0; + }, + not: (api, args, env) => { + if (!args || args.length < 1) { + console.error("\u2757 Invalid `not`. Wrong number of arguments."); + return false; + } + const evaled = this.evaluate(args[0], api, env); + if (!evaled) { + args.slice(1).forEach((expr) => { + this.evaluate(expr, api, env); + }); + } + return !evaled; + }, + // Fill/Outline mode commands (Processing-style) + fill: (api) => { + this.fillMode = true; + return void 0; + }, + outline: (api) => { + this.fillMode = false; + return void 0; + }, + stroke: (api) => { + this.fillMode = false; + return void 0; + }, + nofill: (api) => { + this.fillMode = false; + return void 0; + }, + nostroke: (api) => { + this.fillMode = true; + return void 0; + }, + range: (api, args) => { + if (args.length === 3) { + const array = args[0].data; + const startIndex = max6(0, floor8(args[1])); + const endIndex = max6(0, floor8(args[2])); + if (Array.isArray(array) && typeof startIndex === "number" && typeof endIndex === "number") { + return { iterable: true, data: array.slice(startIndex, endIndex) }; + } else { + console.error( + "\u2757 Invalid arguments for `range`. Expected an array and two numbers." + ); + } + } else { + console.error("\u2757 Invalid `range`. Wrong number of arguments."); + } + }, + // 🧠 Logical Operators + ">": (api, args, env) => { + if (!args || args.length < 2) { + console.error("\u2757 Invalid `>`. Wrong number of arguments."); + return false; + } + const left = this.evaluate(args[0], api, env), right = this.evaluate(args[1], api, env); + if (left > right) { + return this.evaluate(args.slice(2), api, env); + } else { + return false; + } + }, + "<": (api, args, env) => { + if (!args || args.length < 2) { + console.error("\u2757 Invalid `<`. Wrong number of arguments."); + return false; + } + const left = this.evaluate(args[0], api, env), right = this.evaluate(args[1], api, env); + if (left < right) { + return this.evaluate(args.slice(2), api, env); + } else { + return false; + } + }, + "=": (api, args, env) => { + if (!args || args.length < 2) { + console.error("\u2757 Invalid `=`. Wrong number of arguments."); + return false; + } + const left = this.evaluate(args[0], api, env), right = this.evaluate(args[1], api, env); + if (left === right) { + return args.length > 2 ? this.evaluate(args.slice(2), api, env) : true; + } else { + return false; + } + }, + // ➗ Mathematical Operators + max: (api, args, env) => { + const nums = args.map((arg) => this.evaluate(arg, api, this.localEnv)).filter((value) => typeof value === "number" && !isNaN(value)); + return nums.length > 0 ? Math.max(...nums) : 0; + }, + min: (api, args, env) => { + const nums = args.map((arg) => this.evaluate(arg, api, this.localEnv)).filter((value) => typeof value === "number" && !isNaN(value)); + return nums.length > 0 ? Math.min(...nums) : 0; + }, + sin: (api, args = []) => { + if (args.length === 0) { + return Math.sin(this.frameCount * 0.01); + } + const value = this.evaluate(args[0], api, this.localEnv); + return typeof value === "number" ? Math.sin(value) : 0; + }, + cos: (api, args = []) => { + if (args.length === 0) { + return Math.cos(this.frameCount * 0.01); + } + const value = this.evaluate(args[0], api, this.localEnv); + return typeof value === "number" ? Math.cos(value) : 0; + }, + "+": (api, args, env) => { + const result = args.reduce((acc, arg) => { + const value = this.evaluate(arg, api, this.localEnv); + return acc + (typeof value === "number" ? value : 0); + }, 0); + return result; + }, + "-": (api, args, env) => { + if (args.length === 0) return 0; + if (args.length === 1) { + const value = this.evaluate(args[0], api, this.localEnv); + return -(typeof value === "number" ? value : 0); + } + const first = this.evaluate(args[0], api, this.localEnv); + const result = args.slice(1).reduce( + (acc, arg) => { + const value = this.evaluate(arg, api, this.localEnv); + return acc - (typeof value === "number" ? value : 0); + }, + typeof first === "number" ? first : 0 + ); + return result; + }, + "*": (api, args, env) => { + const result = args.reduce((acc, arg) => { + const value = this.evaluate(arg, api, this.localEnv); + return acc * (typeof value === "number" ? value : 0); + }, 1); + return result; + }, + mul: (api, args, env) => { + const result = args.reduce((acc, arg) => { + const value = this.evaluate(arg, api, this.localEnv); + return acc * (typeof value === "number" ? value : 0); + }, 1); + return result; + }, + "/": (api, args, env) => { + if (args.length === 0) return 0; + const first = this.evaluate(args[0], api, this.localEnv); + const result = args.slice(1).reduce( + (acc, arg) => { + const value = this.evaluate(arg, api, this.localEnv); + const divisor = typeof value === "number" ? value : 1; + return divisor !== 0 ? acc / divisor : acc; + }, + typeof first === "number" ? first : 0 + ); + return result; + }, + "%": (api, args, env) => { + if (args.length < 2) return 0; + const first = this.evaluate(args[0], api, this.localEnv); + const second = this.evaluate(args[1], api, this.localEnv); + const a2 = typeof first === "number" ? first : 0; + const b2 = typeof second === "number" ? second : 1; + return b2 !== 0 ? a2 % b2 : 0; + }, + mod: (api, args, env) => { + if (args.length < 2) return 0; + const first = this.evaluate(args[0], api, this.localEnv); + const second = this.evaluate(args[1], api, this.localEnv); + const a2 = typeof first === "number" ? first : 0; + const b2 = typeof second === "number" ? second : 1; + return b2 !== 0 ? a2 % b2 : 0; + }, + // Random number generation + random: (api, args) => { + if (args.length === 0) { + return Math.floor(this.seededRandom() * 256); + } else if (args.length === 1) { + const max9 = args[0]; + return Math.floor(this.seededRandom() * max9); + } else if (args.length >= 2) { + const min10 = args[0]; + const max9 = args[1]; + return Math.floor(this.seededRandom() * (max9 - min10 + 1)) + min10; + } + }, + // Check if an image URL is ready to be pasted + "ready?": (api, args) => { + if (args.length === 0) return false; + const url = unquoteString(args[0]?.toString() || ""); + if (!url) return false; + const paintings2 = api.paintings || {}; + return paintings2[url] && paintings2[url] !== "fetching"; + }, + // Paint API + resolution: (api, args) => { + if (args.length === 1 && typeof args[0] === "string") { + const fraction = args[0]; + let divisor; + let trackingFlag; + switch (fraction) { + case "half": + divisor = 2; + trackingFlag = "halfResolutionApplied"; + break; + case "third": + divisor = 3; + trackingFlag = "thirdResolutionApplied"; + break; + case "fourth": + divisor = 4; + trackingFlag = "fourthResolutionApplied"; + break; + default: + api.resolution?.(...args); + return; + } + if (!this[trackingFlag]) { + this[trackingFlag] = false; + } + if (!this[trackingFlag]) { + const currentWidth = api.screen?.width || 256; + const currentHeight = api.screen?.height || 256; + const newWidth = Math.floor(currentWidth / divisor); + const newHeight = Math.floor(currentHeight / divisor); + api.resolution?.(newWidth, newHeight); + this[trackingFlag] = true; + console.log( + `\u{1F504} ${fraction} resolution applied: ${currentWidth}x${currentHeight} \u2192 ${newWidth}x${newHeight}` + ); + } else { + console.log( + `\u{1F504} ${fraction} resolution already applied, skipping to prevent squashing` + ); + } + } else { + api.resolution?.(...args); + this.halfResolutionApplied = false; + this.thirdResolutionApplied = false; + this.fourthResolutionApplied = false; + } + }, + wipe: (api, args) => { + const processedArgs = processArgStringTypes(args); + const performWipe = () => { + api.wipe?.(processedArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performWipe); + return; + } + performWipe(); + }, + coat: (api, args, env) => { + if (args.length < 1) { + console.error("\u2757 coat requires at least a color argument"); + return; + } + let color3 = args[0]; + const alpha = args.length >= 2 ? args[1] : 128; + const previousInk = this.inkState; + const previousInkSet = this.inkStateSet; + if (typeof color3 === "string" && color3.startsWith("fade:")) { + let evaluatedColor = color3; + const fadeParts = color3.split(":"); + if (fadeParts.length >= 3) { + const fadePrefix = fadeParts[0]; + const colors = fadeParts[1]; + const direction = fadeParts[2]; + let evaluatedDirection = direction; + try { + const numericValue = parseFloat(direction); + if (!isNaN(numericValue)) { + evaluatedDirection = direction; + } else { + let evalResult; + const expanded = this.expandFastMathMacros(direction); + if (Array.isArray(expanded)) { + evalResult = this.fastEval(expanded, api, env); + } else { + const globalEnv = this.getGlobalEnv(); + if (globalEnv[direction] && typeof globalEnv[direction] === "function") { + evalResult = globalEnv[direction](api, []); + } else { + evalResult = this.evaluate(direction, api, env); + } + } + evaluatedDirection = String(evalResult); + } + } catch (error) { + console.warn("Failed to evaluate fade direction in coat:", direction, error); + evaluatedDirection = direction; + } + evaluatedColor = `${fadePrefix}:${colors}:${evaluatedDirection}`; + } + const fadeColorArray = [evaluatedColor, alpha]; + api.ink?.(fadeColorArray); + } else { + const processedColor = processArgStringTypes([color3]); + api.ink?.(...processedColor, alpha); + } + if (api.box) { + const screenWidth = api.width || api.screen?.width || 256; + const screenHeight = api.height || api.screen?.height || 256; + api.box(0, 0, screenWidth, screenHeight); + } + if (previousInkSet) { + this.inkState = previousInk; + this.inkStateSet = previousInkSet; + if (previousInk && previousInk.length > 0) { + api.ink?.(...previousInk); + } + } else { + this.clearInkState(); + } + }, + ink: (api, args) => { + if (this.performanceMode?.enabled && args && Array.isArray(args) && args.length >= 3 && typeof args[0] === "number" && typeof args[1] === "number" && typeof args[2] === "number") { + this.inkState = [args[0], args[1], args[2]]; + this.inkStateSet = true; + if (VERBOSE) { + const drawingTarget = api.screen; + let bakedIndex = -1; + if (this.bakedLayers && drawingTarget) { + bakedIndex = this.bakedLayers.findIndex((layer) => layer?.buffer === drawingTarget || layer?.buffer?.pixels === drawingTarget?.pixels); + } + console.log(`\u{1F58B}\uFE0F Ink (fast path) target info: width=${drawingTarget?.width} height=${drawingTarget?.height} bakedIndex=${bakedIndex}`); + } + api.ink?.(args[0], args[1], args[2]); + return; + } + if (!args || !Array.isArray(args)) { + args = args ? [args] : []; + } + const isEraseMode = args.length >= 1 && args[0] === "erase"; + if (isEraseMode && api.blend) { + if (kidlispInkLoggingEnabled()) { + } + api.blend("erase"); + if (this.currentBakeIndex >= 0 && this.bakes && api.setEraseTarget) { + const bake3 = this.bakes[this.currentBakeIndex]; + if (bake3 && !bake3.eraseMask) { + bake3.eraseMask = new Uint8Array(bake3.width * bake3.height); + } + if (bake3?.eraseMask) { + api.setEraseTarget(bake3.eraseMask, bake3.width); + } + } + } else if (!isEraseMode && api.blend) { + api.blend("blend"); + if (api.setEraseTarget) api.setEraseTarget(null, 0); + } + let processedArgs; + if (args.length === 1 && Array.isArray(args[0])) { + processedArgs = args[0]; + } else { + processedArgs = processArgStringTypes(args); + if (Array.isArray(processedArgs) && processedArgs.length > 1 && Array.isArray(processedArgs[0])) { + processedArgs = [...processedArgs[0], ...processedArgs.slice(1)]; + } + } + if (args.length === 0) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + this.inkState = randomColor; + this.inkStateSet = true; + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "ink", + func: api.ink, + args: randomColor + }); + return; + } + if (VERBOSE) { + const drawingTarget = api.screen; + let bakedIndex = -1; + if (this.bakedLayers && drawingTarget) { + bakedIndex = this.bakedLayers.findIndex((layer) => layer?.buffer === drawingTarget || layer?.buffer?.pixels === drawingTarget?.pixels); + } + console.log(`\u{1F58B}\uFE0F Ink (random) target info: width=${drawingTarget?.width} height=${drawingTarget?.height} bakedIndex=${bakedIndex}`); + } + api.ink?.(...randomColor); + return; + } else if (args.length === 1 && (args[0] === null || args[0] === void 0)) { + this.clearInkState(); + return void 0; + } else { + if (args.length === 1 && typeof args[0] === "string" && args[0].startsWith("fade:")) { + this.inkState = [args[0]]; + this.inkStateSet = true; + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "ink", + func: api.ink, + args: [args[0]] + }); + return; + } + api.ink?.(args[0]); + return; + } + this.inkState = Array.isArray(processedArgs) ? processedArgs : [processedArgs]; + this.inkStateSet = true; + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "ink", + func: api.ink, + args: Array.isArray(processedArgs) ? processedArgs : [processedArgs] + }); + return; + } + api.ink?.(...Array.isArray(processedArgs) ? processedArgs : [processedArgs]); + } + }, + // Fade string constructor - returns a fade string that can be used with ink + // Usage: (fade "red" "blue") → "fade:red-blue" + // Usage: (fade "red" "blue" "vertical") → "fade:red-blue:vertical" + // Usage: (fade "red" "yellow" "blue" "horizontal-reverse") → "fade:red-yellow-blue:horizontal-reverse" + fade: (api, args) => { + if (args.length < 2) { + return "fade:red-blue"; + } + const lastArg = args[args.length - 1]; + const validDirections = [ + "horizontal", + "horizontal-reverse", + "vertical", + "vertical-reverse", + "diagonal", + "diagonal-reverse" + ]; + let colors, direction; + if (typeof lastArg === "string" && validDirections.includes(lastArg)) { + colors = processArgStringTypes(args.slice(0, -1)).join("-"); + direction = lastArg; + return `fade:${colors}:${direction}`; + } else if (typeof lastArg === "number") { + colors = processArgStringTypes(args.slice(0, -1)).join("-"); + direction = lastArg.toString(); + return `fade:${colors}:${direction}`; + } else if (args.length >= 3) { + colors = processArgStringTypes(args.slice(0, -1)).join("-"); + try { + const evaluated = typeof lastArg === "string" ? parseFloat(lastArg) : lastArg; + if (!isNaN(evaluated)) { + return `fade:${colors}:${evaluated}`; + } else { + return `fade:${colors}:${JSON.stringify(lastArg)}`; + } + } catch (error) { + return `fade:${colors}:${JSON.stringify(lastArg)}`; + } + } else { + colors = processArgStringTypes(args).join("-"); + return `fade:${colors}`; + } + }, + // Dynamic timing helpers with intuitive names + hop: (api, args, env) => { + if (args.length >= 3) { + const seconds = this.evaluate(args[0], api, env); + const color1 = args[1]; + const color22 = args[2]; + const timingStr = `${seconds}s...`; + return this.evaluate([timingStr, color1, color22], api, env); + } + console.error("\u2757 Invalid `hop`. Expected (hop seconds color1 color2)."); + }, + delay: (api, args, env) => { + if (args.length >= 2) { + const seconds = this.evaluate(args[0], api, env); + const action = args[1]; + const timingStr = `${seconds}s`; + return this.evaluate([timingStr, action], api, env); + } + console.error("\u2757 Invalid `delay`. Expected (delay seconds action)."); + }, + line: (api, args = []) => { + if (!this.inkStateSet && kidlispInkLoggingEnabled()) { + console.log(`${kidlispInkLogPrefix()}\u{1F41B} line called without ink!`); + console.log(` Current expression:`, this.currentEvaluatingExpression); + console.log(` AST:`, JSON.stringify(this.ast).substring(0, 200)); + } + if (kidlispInkLoggingEnabled()) { + let activeInkRaw; + try { + activeInkRaw = cloneValueForInkLog(api?.inkrn?.()); + } catch (err) { + activeInkRaw = `inkrn-error:${err?.message || err}`; + } + const inkStateRaw = cloneValueForInkLog(this.getInkState()); + const argsRaw = Array.isArray(args) ? args.map((arg) => cloneValueForInkLog(arg)) : cloneValueForInkLog(args); + const canUseActiveInk = activeInkRaw !== void 0 && activeInkRaw !== null && typeof activeInkRaw !== "string"; + const hasInkState = inkStateRaw !== void 0 && inkStateRaw !== null; + const resolvedInkRaw = hasInkState ? inkStateRaw : canUseActiveInk ? activeInkRaw : inkStateRaw; + const resolvedInkSource = hasInkState ? "state" : canUseActiveInk ? "active" : "none"; + } + const useRandomInk = !this.inkStateSet; + if (useRandomInk) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + api.ink?.(...randomColor); + if (kidlispInkLoggingEnabled()) { + console.log(`${kidlispInkLogPrefix()}\u{1F3A8} line: Using random ink [${randomColor.join(", ")}]`); + } + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "line", + func: api.line, + args: [...args] + }); + return; + } + if (this.inkState && api.ink) { + api.ink(...this.inkState); + if (kidlispInkLoggingEnabled() && this.inkState[0] === "erase") { + const graphColor = api.color ? api.color() : null; + if (Array.isArray(graphColor)) { + } else if (graphColor) { + } else { + } + } + } + const currentPage = api.screen; + const isBakeBuffer = this.bakes?.some((b2) => b2.pixels === currentPage?.pixels); + api.line(...args); + }, + // Batch line drawing for performance + lines: (api, args = []) => { + if (args.length > 0 && Array.isArray(args[0])) { + args[0].forEach((lineArgs) => { + if (Array.isArray(lineArgs) && lineArgs.length >= 4) { + api.line(...lineArgs); + } + }); + } + }, + wiggle: (api, args = []) => { + const amount = args.length > 0 ? args[0] : 10; + return (this.seededRandom() - 0.5) * amount; + }, + box: (api, args = []) => { + if (!Array.isArray(args)) { + console.warn("\u26A0\uFE0F box function received non-array args, converting to array"); + args = Array.isArray(args) ? args : [args]; + } + if (args.length === 0) { + args = [void 0, void 0, void 0, void 0]; + } + const processedArgs = args.map((arg, index) => { + if (arg === void 0) { + switch (index) { + case 0: + return Math.floor(this.seededRandom() * (api.screen?.width || 256)); + case 1: + return Math.floor(this.seededRandom() * (api.screen?.height || 256)); + case 2: + return Math.floor(this.seededRandom() * ((api.screen?.width || 256) / 4)) + 10; + case 3: + return Math.floor(this.seededRandom() * ((api.screen?.height || 256) / 4)) + 10; + default: + return Math.floor(this.seededRandom() * 256); + } + } + return arg; + }); + if (processedArgs.length === 4 && processedArgs.every((a2) => typeof a2 === "number")) { + processedArgs.push(this.fillMode ? "fill" : "outline"); + } + if (!this.inkStateSet) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + api.ink?.(...randomColor); + } + const drawBox = () => { + api.box(...processedArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, drawBox); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "box", + func: api.box, + args: [...processedArgs] + }); + return; + } + drawBox(); + }, + circle: (api, args = []) => { + if (args.length === 0) { + args = [void 0, void 0, void 0]; + } + const processedCircleArgs = args.map((arg, index) => { + if (arg === void 0) { + switch (index) { + case 0: + return Math.floor(this.seededRandom() * (api.screen?.width || 256)); + case 1: + return Math.floor(this.seededRandom() * (api.screen?.height || 256)); + case 2: + return Math.floor(this.seededRandom() * (Math.min(api.screen?.width || 256, api.screen?.height || 256) / 8)) + 5; + default: + return Math.floor(this.seededRandom() * 256); + } + } + return arg; + }); + let circleArgs = [...processedCircleArgs]; + if (circleArgs.length === 3) { + circleArgs.push(this.fillMode); + } + if (!this.inkStateSet) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + api.ink?.(...randomColor); + } + const drawCircle = () => { + api.circle(...circleArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, drawCircle); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "circle", + func: api.circle, + args: [...circleArgs] + }); + return; + } + drawCircle(); + }, + oval: (api, args = []) => { + if (args.length === 0) args = [void 0, void 0, void 0, void 0]; + const processedOvalArgs = args.map((arg, index) => { + if (arg !== void 0) return arg; + const w = api.screen?.width || 256, h = api.screen?.height || 256; + switch (index) { + case 0: + return Math.floor(this.seededRandom() * w); + case 1: + return Math.floor(this.seededRandom() * h); + case 2: + return Math.floor(this.seededRandom() * (w / 8)) + 5; + case 3: + return Math.floor(this.seededRandom() * (h / 8)) + 5; + default: + return Math.floor(this.seededRandom() * 256); + } + }); + const [ox, oy, orx, ory] = processedOvalArgs; + const mode = processedOvalArgs[4]; + const filled = mode === void 0 ? this.fillMode : mode !== "outline"; + if (!this.inkStateSet) { + api.ink?.( + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ); + } + api.oval(ox, oy, orx, ory, filled); + }, + point: (api, args = []) => { + const x = args.length >= 1 ? args[0] : Math.floor(this.seededRandom() * (api.screen?.width || 128)); + const y = args.length >= 2 ? args[1] : Math.floor(this.seededRandom() * (api.screen?.height || 128)); + const drawPoint = () => { + api.point(x, y); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, drawPoint); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "point", + func: api.point, + args: [x, y] + }); + return; + } + drawPoint(); + }, + tri: (api, args = []) => { + if (args.length === 0) { + args = [void 0, void 0, void 0, void 0, void 0, void 0]; + } + const processedTriArgs = args.map((arg, index) => { + if (arg === void 0) { + if (index % 2 === 0) { + return Math.floor(this.seededRandom() * (api.screen?.width || 256)); + } else { + return Math.floor(this.seededRandom() * (api.screen?.height || 256)); + } + } + return arg; + }); + let triArgs = [...processedTriArgs]; + if (triArgs.length === 6 && triArgs.every((a2) => typeof a2 === "number")) { + triArgs.push(this.fillMode ? "fill" : "outline"); + } + if (!this.inkStateSet) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + api.ink?.(...randomColor); + } + const drawTri = () => { + api.tri(...triArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, drawTri); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "tri", + func: api.tri, + args: [...triArgs] + }); + return; + } + drawTri(); + }, + flood: (api, args = []) => { + if (args.length === 0) args = [void 0, void 0]; + if (args.length >= 2) { + let x = args[0]; + let y = args[1]; + if (x === void 0) { + x = Math.floor(this.seededRandom() * (api.screen?.width || 256)); + } + if (y === void 0) { + y = Math.floor(this.seededRandom() * (api.screen?.height || 256)); + } + const fillColor = args[2]; + const useRandomInk = !this.inkStateSet && fillColor === void 0; + if (useRandomInk) { + const randomColor = [ + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ]; + api.ink?.(...randomColor); + } + const performFlood = () => { + if (fillColor !== void 0) { + api.flood(x, y, processArgStringTypes(fillColor)); + } else { + api.flood(x, y); + } + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performFlood); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase) { + this.postEmbedCommands = this.postEmbedCommands || []; + this.postEmbedCommands.push({ + name: "flood", + func: () => { + performFlood(); + }, + args: [x, y, fillColor] + }); + return; + } + performFlood(); + } + }, + shape: (api, args = []) => { + if (args.length === 0 || args.length === 1 && typeof args[0] === "number") { + const n2 = args.length === 1 ? Math.max(3, Math.floor(args[0])) : 3 + Math.floor(this.seededRandom() * 4); + const w = api.screen?.width || 256, h = api.screen?.height || 256; + const pts = []; + for (let i2 = 0; i2 < n2; i2++) { + pts.push([Math.floor(this.seededRandom() * w), Math.floor(this.seededRandom() * h)]); + } + const cx = pts.reduce((s2, p) => s2 + p[0], 0) / n2; + const cy = pts.reduce((s2, p) => s2 + p[1], 0) / n2; + pts.sort((a2, b2) => Math.atan2(a2[1] - cy, a2[0] - cx) - Math.atan2(b2[1] - cy, b2[0] - cx)); + args = pts.flat(); + if (!this.inkStateSet) { + api.ink?.( + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256), + Math.floor(this.seededRandom() * 256) + ); + } + api.shape({ points: args, filled: this.fillMode !== false, thickness: 1 }); + return; + } + if (this.performanceMode?.enabled && args.length === 6 && args.every((arg) => typeof arg === "number")) { + api.shape({ points: args, filled: true, thickness: 1 }); + return; + } + let filled = true; + let thickness = 1; + let points = args; + const lastArg = args[args.length - 1]; + if (typeof lastArg === "string") { + const fillMode = unquoteString(lastArg); + if (fillMode === "outline" || fillMode === "unfilled" || fillMode === "false") { + filled = false; + points = args.slice(0, -1); + } + if (fillMode.startsWith("outline:")) { + filled = false; + thickness = parseInt(fillMode.split(":")[1]) || 1; + points = args.slice(0, -1); + } + } + api.shape({ points, filled, thickness }); + }, + // 🐢 Turtle Graphics + crawl: (api, args = []) => { + const steps = args.length > 0 ? args[0] : 1; + return api.crawl?.(steps); + }, + left: (api, args = []) => { + const degrees2 = args.length > 0 ? args[0] : 1; + return api.left?.(degrees2); + }, + right: (api, args = []) => { + const degrees2 = args.length > 0 ? args[0] : 1; + return api.right?.(degrees2); + }, + up: (api, args = []) => { + return api.up?.(); + }, + down: (api, args = []) => { + return api.down?.(); + }, + goto: (api, args = []) => { + if (args.length >= 2) { + return api.goto?.(args[0], args[1]); + } + return api.goto?.(); + }, + face: (api, args = []) => { + const angle3 = args.length > 0 ? args[0] : 0; + return api.face?.(angle3); + }, + scroll: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.suppressDrawingBeforeBake) { + if (VERBOSE) console.log("\u{1F35E} Scroll: SUPPRESSED (before bake point)"); + return; + } + let dx = 0, dy = 0; + if (!args || args.length === 0) { + if (!this.scrollFuzzDirection) { + const directions = [ + [1, 0], + // scroll right + [-1, 0], + // scroll left + [0, 1], + // scroll down + [0, -1] + // scroll up + ]; + this.scrollFuzzDirection = directions[Math.floor(this.seededRandom() * directions.length)]; + } + dx = this.scrollFuzzDirection[0]; + dy = this.scrollFuzzDirection[1]; + } else if (Array.isArray(args) && args.length === 1 && Array.isArray(args[0]) && args[0].length > 0 && typeof args[0][0] === "string" && args[0][0].endsWith("...")) { + const timingExpr = args[0]; + const result = this.evaluate([timingExpr], api); + if (Array.isArray(result) && result.length >= 2) { + dx = parseFloat(result[0]) || 0; + dy = parseFloat(result[1]) || 0; + } else if (typeof result === "number") { + dx = result; + dy = 0; + } else { + return; + } + } else if (Array.isArray(args) && args.length > 0 && Array.isArray(args[0])) { + const timingPhases = args; + let activePhase = null; + for (let i2 = 0; i2 < timingPhases.length; i2++) { + const phase = timingPhases[i2]; + if (Array.isArray(phase) && phase.length > 0) { + const timingToken = phase[0]; + if (typeof timingToken === "string" && /^\d*\.?\d+[s]\.\.\.?$/.test(timingToken)) { + if (this.evaluateTimingExpression && this.evaluateTimingExpression(api, timingToken)) { + activePhase = phase; + break; + } else { + console.log(`\u{1F5B1}\uFE0F SCROLL timing phase ${i2} is inactive`); + } + } + } + } + if (activePhase && activePhase.length > 1) { + const values2 = activePhase.slice(1); + if (values2.length >= 2) { + dx = parseFloat(values2[0]) || 0; + dy = parseFloat(values2[1]) || 0; + } else if (values2.length === 1) { + dx = parseFloat(values2[0]) || 0; + dy = 0; + } + console.log(`\u{1F5B1}\uFE0F SCROLL using active timing phase values: dx=${dx}, dy=${dy}`); + } else { + console.log(`\u{1F5B1}\uFE0F SCROLL no active timing phase found, using defaults`); + } + } else if (Array.isArray(args)) { + if (args.length === 1) { + dx = parseFloat(args[0]) || 0; + dy = 0; + } else if (args.length === 2) { + dx = parseFloat(args[0]) || 0; + dy = parseFloat(args[1]) || 0; + } else if (args.length > 2) { + dx = parseFloat(args[0]) || 0; + dy = parseFloat(args[1]) || 0; + } + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "scroll", + func: () => { + if (typeof api.scroll === "function") { + api.scroll(dx, dy); + } + }, + args: [dx, dy] + }); + return; + } + if (typeof api.scroll === "function") { + api.scroll(dx, dy); + } + if (this.burnedBuffer) { + this._scrollPersistentLayers(api, dx, dy); + } + }, + spin: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "spin", + func: () => api.spin(...args), + args + }); + return; + } + api.spin(...args); + if (this.burnedBuffer) { + this._spinPersistentLayers(api, args); + } + }, + resetSpin: (api, args = []) => { + api.resetSpin(); + }, + smoothspin: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "smoothspin", + func: () => api.smoothSpin(...args), + args + }); + return; + } + api.smoothSpin(...args); + if (this.burnedBuffer) { + this._spinPersistentLayers(api, args, true); + } + }, + sort: (api, args = []) => { + api.sort(...args); + }, + zoom: (api, args = []) => { + this.preserveLayer0NextFrame = true; + const performZoom = () => { + api.zoom(...args); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, () => { + performZoom(); + }); + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "zoom", + func: () => { + api.zoom(...args); + }, + args + }); + return; + } + performZoom(); + if (this.burnedBuffer) { + const layers = []; + if (this.layer0?.pixels) layers.push(this.layer0); + if (this.bakes) { + for (const b2 of this.bakes) { + if (b2?.pixels) layers.push(b2); + } + } + for (const buf of layers) { + api.page(buf); + api.zoom(...args); + } + api.page(this.burnedBuffer); + } + }, + flip: (api, args = []) => { + this.preserveLayer0NextFrame = true; + const performFlip = () => { + if (typeof api.flip === "function") { + api.flip(...args); + } + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performFlip); + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "flip", + func: performFlip, + args + }); + return; + } + performFlip(); + }, + suck: (api, args = []) => { + this.preserveLayer0NextFrame = true; + const performSuck = () => { + api.suck(...args); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performSuck); + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "suck", + func: () => { + api.suck(...args); + }, + args + }); + return; + } + performSuck(); + }, + blur: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.suppressDrawingBeforeBake) { + const routed = this.runWithBakedBuffer(api, () => { + api.blur(...args); + }); + if (routed) { + return; + } + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "blur", + func: () => api.blur(...args), + args + }); + return; + } + api.blur(...args); + }, + sharpen: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.suppressDrawingBeforeBake) { + const routed = this.runWithBakedBuffer(api, () => { + api.sharpen(...args); + }); + if (routed) { + return; + } + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "sharpen", + func: () => api.sharpen(...args), + args + }); + return; + } + api.sharpen(...args); + }, + invert: (api, args = []) => { + this.preserveLayer0NextFrame = true; + if (this.suppressDrawingBeforeBake) { + const routed = this.runWithBakedBuffer(api, () => { + api.invert(...args); + }); + if (routed) { + return; + } + return; + } + if (this.embeddedLayers?.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands.push({ + name: "invert", + func: () => api.invert(...args), + args + }); + return; + } + api.invert(...args); + }, + contrast: (api, args = []) => { + this.preserveLayer0NextFrame = true; + const performContrast = () => { + api.contrast(...args); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performContrast); + return; + } + if (this.embeddedLayers && this.embeddedLayers.length > 0 && !this.inEmbedPhase && !this.isEmbeddedContext) { + this.postCompositeCommands = this.postCompositeCommands || []; + this.postCompositeCommands.push({ + name: "contrast", + func: () => { + performContrast(); + }, + args + }); + return; + } + performContrast(); + }, + pan: (api, args = []) => { + api.pan(...args); + }, + unpan: (api, args = []) => { + api.unpan(); + }, + // 📷 Camera rotation functions + camrotx: (api, args = []) => { + const rotation = args[0] || 0; + if (api.system.defaultCam) { + api.system.defaultCam.rotX = rotation; + } + }, + camroty: (api, args = []) => { + const rotation = args[0] || 0; + if (api.system.defaultCam) { + api.system.defaultCam.rotY = rotation; + } + }, + camrotz: (api, args = []) => { + const rotation = args[0] || 0; + if (api.system.defaultCam) { + api.system.defaultCam.rotZ = rotation; + } + }, + camrot: (api, args = []) => { + const x = args[0] || 0; + const y = args[1] || 0; + const z = args[2] || 0; + if (api.system.defaultCam) { + api.system.defaultCam.rotX = x; + api.system.defaultCam.rotY = y; + api.system.defaultCam.rotZ = z; + } + }, + camspinx: (api, args = []) => { + const speed = args[0] || 0; + if (api.system.defaultCam) { + const frameCount = this.frameCount || 0; + api.system.defaultCam.rotX = speed * frameCount; + } + }, + camspiny: (api, args = []) => { + const speed = args[0] || 0; + if (api.system.defaultCam) { + const frameCount = this.frameCount || 0; + api.system.defaultCam.rotY = speed * frameCount; + } + }, + camspinz: (api, args = []) => { + const speed = args[0] || 0; + if (api.system.defaultCam) { + const frameCount = this.frameCount || 0; + api.system.defaultCam.rotZ = speed * frameCount; + } + }, + camspin: (api, args = []) => { + const xSpeed = args[0] || 0; + const ySpeed = args[1] || 0; + const zSpeed = args[2] || 0; + if (api.system.defaultCam) { + const frameCount = this.frameCount || 0; + api.system.defaultCam.rotX = xSpeed * frameCount; + api.system.defaultCam.rotY = ySpeed * frameCount; + api.system.defaultCam.rotZ = zSpeed * frameCount; + } + }, + mask: (api, args = []) => { + if (args.length >= 4) { + const box2 = { + x: args[0], + y: args[1], + width: args[2], + height: args[3] + }; + api.mask(box2); + } + }, + unmask: (api, args = []) => { + api.unmask(); + }, + steal: (api, args = []) => { + api.steal(...args); + }, + putback: (api, args = []) => { + api.putback(...args); + }, + // 🖼️ Image pasting and stamping + // Shorthand for pasting the current user's painting + painting: (api, args = []) => { + const processedArgs = [api.system?.painting, ...args]; + api.paste(...processedArgs); + }, + paste: (api, args = []) => { + const processedArgs = args.map((arg, index) => { + if (typeof arg === "string" && arg.startsWith('"') && arg.endsWith('"')) { + return arg.slice(1, -1); + } + if (typeof arg === "string" && arg === "painting") { + return api.system?.painting; + } + if (index === 0 && typeof arg === "string" && arg) { + if (arg.startsWith("http://") || arg.startsWith("https://") || arg.includes("/") || arg.includes("@") || arg.startsWith("#")) { + return arg; + } + } + return arg; + }); + const performPaste = () => { + api.paste(...processedArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performPaste); + return; + } + performPaste(); + }, + stamp: (api, args = []) => { + const processedArgs = args.map((arg, index) => { + if (typeof arg === "string" && arg.startsWith('"') && arg.endsWith('"')) { + return arg.slice(1, -1); + } + if (typeof arg === "string" && arg === "painting") { + return api.system?.painting; + } + if (index === 0 && typeof arg === "string" && arg) { + if (arg.startsWith("http://") || arg.startsWith("https://") || arg.includes("/") || arg.includes("@") || arg.startsWith("#")) { + return arg; + } + } + const result = this.evaluate(arg, api, this.localEnv); + if (index === 0 && typeof result === "function") { + return typeof arg === "string" ? arg : result; + } + return result; + }); + if (processedArgs.length > 0 && typeof processedArgs[0] !== "string" && (typeof processedArgs[0] !== "object" || !processedArgs[0]?.width)) { + console.warn("\u26A0\uFE0F stamp: invalid image source", processedArgs[0]); + return; + } + const performStamp = () => { + api.stamp(...processedArgs); + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performStamp); + return; + } + performStamp(); + }, + // Convert args to string and remove surrounding quotes for text commands + write: (api, args = []) => { + const content = unquoteString(args[0]?.toString() || ""); + let x = args[1]; + let y = args[2]; + if (typeof x === "string" && x.startsWith('"') && x.endsWith('"')) { + x = unquoteString(x); + } + if (typeof y === "string" && y.startsWith('"') && y.endsWith('"')) { + y = unquoteString(y); + } + const bg = args[3] !== void 0 ? processArgStringTypes(args[3]) : void 0; + const size = args[4]; + const bounds = args[5]; + const options = {}; + if (bg !== void 0) { + options.bg = bg; + } + if (bounds !== void 0) { + options.bounds = bounds; + } + const centerX = x === "center"; + const centerY = y === "center"; + const pos = {}; + if (centerX && centerY) { + pos.center = "xy"; + } else if (centerX) { + pos.center = "x"; + pos.y = y; + } else if (centerY) { + pos.center = "y"; + pos.x = x; + } else { + if (x !== void 0) pos.x = x; + if (y !== void 0) pos.y = y; + } + if (size !== void 0) { + pos.size = size; + } + const performWrite = () => { + if (Object.keys(options).length > 0) { + api.write(content, pos, options); + } else { + api.write(content, pos); + } + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performWrite); + return; + } + performWrite(); + }, + // � 3D Form functions + // 3D Objects - simple global forms + cube: (api, args, env, colon) => { + let cubeId; + if (colon) { + cubeId = colon; + } else if (args.length > 0) { + cubeId = String(this.evaluate(args[0], api, env)); + } else { + cubeId = "0"; + } + const templateKey = `cubeTemplate_${cubeId}`; + if (!api.system[templateKey]) { + api.system[templateKey] = new api.Form(api.CUBEL, { pos: [0, 0, 2], rot: [0, 0, 0], scale: 1 }); + } + return api.system[templateKey]; + }, + quad: (api) => { + if (!api.system.quadTemplate) { + api.system.quadTemplate = new api.Form(api.QUAD, { pos: [0, 0, -4], rot: [0, 0, 0], scale: 1 }); + } + return api.system.quadTemplate; + }, + // 🎭 Trans function - creates working copies and applies transformations + // Usage: (trans cube (move 1 2 3) (scale 2) (spin 0.1 0 0) (rotate 45 0 0)) + trans: (api, args, env) => { + if (args.length < 2) { + console.error("\u2757 trans requires at least 2 arguments: form and transformation(s)"); + return; + } + const templateForm = this.evaluate(args[0], api, env); + if (!templateForm || !templateForm.vertices) { + console.error("\u2757 trans: first argument must be a valid form, got:", templateForm); + return; + } + const workingForm = this.createFormCopy(templateForm, api); + for (let i2 = 1; i2 < args.length; i2++) { + const transformCmd = args[i2]; + if (Array.isArray(transformCmd) && transformCmd.length > 0) { + const [cmd, ...params] = transformCmd; + const evaluatedParams = params.map((param) => this.evaluate(param, api, env)); + switch (cmd) { + case "move": + case "pos": + if (evaluatedParams.length >= 3) { + workingForm.position[0] = evaluatedParams[0] || 0; + workingForm.position[1] = evaluatedParams[1] || 0; + workingForm.position[2] = evaluatedParams[2] || 0; + } + break; + case "scale": + if (evaluatedParams.length === 1) { + const s2 = evaluatedParams[0] || 1; + workingForm.scale[0] = s2; + workingForm.scale[1] = s2; + workingForm.scale[2] = s2; + } else if (evaluatedParams.length >= 3) { + workingForm.scale[0] = evaluatedParams[0] || 1; + workingForm.scale[1] = evaluatedParams[1] || 1; + workingForm.scale[2] = evaluatedParams[2] || 1; + } + break; + case "rotate": + if (evaluatedParams.length >= 3) { + workingForm.rotation[0] = evaluatedParams[0] || 0; + workingForm.rotation[1] = evaluatedParams[1] || 0; + workingForm.rotation[2] = evaluatedParams[2] || 0; + } + break; + case "spin": + if (evaluatedParams.length >= 3) { + const frameCount = this.frameCount || 0; + workingForm.rotation[0] = (evaluatedParams[0] || 0) * frameCount; + workingForm.rotation[1] = (evaluatedParams[1] || 0) * frameCount; + workingForm.rotation[2] = (evaluatedParams[2] || 0) * frameCount; + } + break; + default: + console.warn(`\u2757 Unknown transform command: ${cmd}`); + break; + } + } + } + workingForm.gpuTransformed = true; + return workingForm; + }, + // Render a form + form: (api, args = []) => { + const forms = args.filter((f2) => f2 !== void 0); + if (forms.length > 0) { + api.form(forms); + } + }, + // Simple transform function for cube + cubespin: (api, args = []) => { + const xSpeed = args[0] || 0; + const ySpeed = args[1] || 0; + const zSpeed = args[2] || 0; + const cube = api.system.cube; + if (cube) { + cube.rotation[0] += xSpeed; + cube.rotation[1] += ySpeed; + cube.rotation[2] += zSpeed; + cube.gpuTransformed = true; + } + return cube; + }, + // Alternative center-spinning cube function + cubespin2: (api, args = []) => { + const xSpeed = args[0] || 0; + const ySpeed = args[1] || 0; + const zSpeed = args[2] || 0; + const cube = api.system.cube; + if (cube && cube.vertices) { + const centerX = cube.position[0]; + const centerY = cube.position[1]; + const centerZ = cube.position[2]; + const rotY = ySpeed; + const cos5 = Math.cos(rotY); + const sin6 = Math.sin(rotY); + cube.vertices.forEach((vertex) => { + const x = vertex.pos[0] - centerX; + const z = vertex.pos[2] - centerZ; + const newX = x * cos5 - z * sin6; + const newZ = x * sin6 + z * cos5; + vertex.pos[0] = newX + centerX; + vertex.pos[2] = newZ + centerZ; + }); + cube.gpuTransformed = true; + } + return cube; + }, + // Move cube to position + cubepos: (api, args = []) => { + const x = args[0] || 0; + const y = args[1] || 0; + const z = args[2] || -4; + const cube = api.system.cube; + if (cube) { + cube.position[0] = x; + cube.position[1] = y; + cube.position[2] = z; + cube.gpuTransformed = true; + } + return cube; + }, + // Scale cube + cubescale: (api, args = []) => { + const scale7 = args[0] || 1; + const cube = api.system.cube; + if (cube) { + cube.scale[0] = scale7; + cube.scale[1] = scale7; + cube.scale[2] = scale7; + cube.gpuTransformed = true; + } + return cube; + }, + // Set cube rotation + cuberot: (api, args = []) => { + const x = args[0] || 0; + const y = args[1] || 0; + const z = args[2] || 0; + const cube = api.system.cube; + if (cube) { + cube.rotation[0] = x; + cube.rotation[1] = y; + cube.rotation[2] = z; + cube.gpuTransformed = true; + } + return cube; + }, + // Global 3D objects work directly + // Manipulate cube.position, cube.rotation, cube.scale directly from KidLisp + // �🏷️ HUD label + label: (api, args = []) => { + const text = args[0] ? unquoteString(args[0].toString()) : void 0; + const color3 = args[1]; + const offset = args[2]; + api.hud?.label?.(text, color3, offset); + return text; + }, + len: (api, args = []) => { + return args[0]?.toString().length; + }, + // (Getters / globals). + source: (api, args = [], env, colon) => { + let sourcedAST = []; + if (colon) { + if (!this.networkCache.sources[colon]) { + this.networkCache.sources[colon] = "loading"; + fetch(`/aesthetic.computer/disks/${colon}.lisp`).then((response) => response.text()).then((code2) => { + this.networkCache.sources[colon] = this.parse(code2); + api.needsPaint(); + }).catch((error) => console.warn(error)); + } else { + sourcedAST = this.networkCache.sources[colon]; + } + } else { + sourcedAST = this.ast; + } + return { iterable: true, data: sourcedAST }; + }, + width: (api) => { + return api.screen.width; + }, + w: (api) => { + return api.screen.width; + }, + height: (api) => { + return api.screen.height; + }, + h: (api) => { + return api.screen.height; + }, + frame: (api) => { + return api.paintCount || 0; + }, + f: (api) => { + return api.paintCount || 0; + }, + clock: (api, args = []) => { + if (args.length > 0) return this.runMelody(api, args); + this._syncClock(api); + return this._now(); + }, + // 🎯 Performance monitoring functions (singleton behavior) + perf: (api, args = []) => { + const perfKey = `perf_singleton_${args.join("_")}`; + if (this.onceExecuted.has(perfKey)) { + return this.perf.showHUD; + } + this.onceExecuted.add(perfKey); + if (args.length === 0) { + this.startPerformanceMonitoring(); + return true; + } else if (args[0] === "start" || args[0] === "on") { + this.startPerformanceMonitoring(); + return true; + } else if (args[0] === "stop" || args[0] === "off") { + this.stopPerformanceMonitoring(); + return false; + } else if (args[0] === "toggle") { + return this.togglePerformanceHUD(); + } + return this.perf.enabled; + }, + fps: (api, args) => { + if (this.forcedFps !== null) return this.forcedFps; + const isPackMode = typeof window !== "undefined" && window.acPACK_MODE || typeof globalThis !== "undefined" && globalThis.acPACK_MODE; + if (args.length > 0) { + const parsed = parseFloat(args[0]); + if (!isNaN(parsed)) { + if (parsed > 0) { + const targetFps = parsed; + this.targetFps = targetFps; + if (api && typeof api.fps === "function") { + api.fps(targetFps); + } + if (api && api.system) { + api.system.kidlispFps = targetFps; + } + if (typeof window !== "undefined") { + window.currentKidlispFps = targetFps; + if (!window.kidlispFpsTimeline) { + window.kidlispFpsTimeline = []; + } + const timestamp2 = performance.now(); + window.kidlispFpsTimeline.push({ + timestamp: timestamp2, + fps: targetFps + }); + if (window.currentRecordingOptions) { + window.currentRecordingOptions.kidlispFps = targetFps; + window.currentRecordingOptions.kidlispFpsTimeline = window.kidlispFpsTimeline; + if (!isPackMode) { + console.log(`\u{1F3AC} Updated recording options with KidLisp FPS: ${targetFps} at ${timestamp2.toFixed(2)}ms`); + } + } + } + if (this._lastLoggedFps !== targetFps && !isPackMode) { + console.log(`\u{1F3AC} KidLisp FPS set to: ${targetFps} at ${(typeof window !== "undefined" ? performance.now() : 0).toFixed(2)}ms`); + this._lastLoggedFps = targetFps; + } + return targetFps; + } + this.targetFps = null; + if (api && typeof api.fps === "function") { + api.fps(null); + } + if (api && api.system) { + api.system.kidlispFps = null; + } + if (typeof window !== "undefined") { + window.currentKidlispFps = null; + if (!window.kidlispFpsTimeline) { + window.kidlispFpsTimeline = []; + } + const timestamp2 = performance.now(); + window.kidlispFpsTimeline.push({ + timestamp: timestamp2, + fps: null + }); + if (window.currentRecordingOptions) { + window.currentRecordingOptions.kidlispFps = null; + window.currentRecordingOptions.kidlispFpsTimeline = window.kidlispFpsTimeline; + if (!isPackMode) { + console.log(`\u{1F3AC} Reset recording KidLisp FPS override at ${timestamp2.toFixed(2)}ms`); + } + } + } + if (this._lastLoggedFps !== null && !isPackMode) { + console.log(`\u{1F3AC} KidLisp FPS reset to default at ${(typeof window !== "undefined" ? performance.now() : 0).toFixed(2)}ms`); + this._lastLoggedFps = null; + } + return 60; + } + } + return this.targetFps || 60; + }, + // 🎯 Auto-density: automatic pixel density scaling based on FPS + // Usage: (auto-density) - enable with defaults + // (auto-density on) - enable + // (auto-density off) - disable + // (auto-density 30 55) - enable with min/max FPS targets + // (auto-density status) - get current status + "auto-density": (api, args) => { + if (args.length === 0 || args[0] === "on" || args[0] === true) { + this.enableAutoDensity(); + return true; + } else if (args[0] === "off" || args[0] === false) { + this.disableAutoDensity(); + return false; + } else if (args[0] === "status") { + return this.getAutoDensityStatus(); + } else if (typeof args[0] === "number") { + const options = { + targetFpsMin: args[0] + }; + if (args.length > 1 && typeof args[1] === "number") { + options.targetFpsMax = args[1]; + } + this.enableAutoDensity(options); + return true; + } + return this.autoDensity.enabled; + }, + // Alias for auto-density + "scale": (api, args) => { + return this.globalEnv["auto-density"](api, args); + }, + // 🔄 Repeat function (highly optimized) + repeat: (api, args, env) => { + perfStart("repeat-setup"); + if (args.length < 2) { + console.error( + "\u2757 repeat requires at least 2 arguments: count and expression(s)" + ); + return void 0; + } + const countValue = this.evaluate(args[0], api, env); + const count = Number(countValue); + if (isNaN(count) || count < 0) { + console.error( + "\u2757 repeat count must be a non-negative number, got:", + countValue + ); + return void 0; + } + if (count > 1e4) { + console.error( + `\u2757 repeat count ${count} exceeds safety limit of 10,000. Use smaller counts or timing expressions.` + ); + return void 0; + } + perfEnd("repeat-setup"); + let result; + if (args.length >= 3 && typeof args[1] === "string") { + perfStart("repeat-with-iterator"); + const iteratorVar = args[1]; + const expressions = args.slice(2); + if (expressions.length === 2 && Array.isArray(expressions[0]) && expressions[0][0] === "ink" && Array.isArray(expressions[1]) && expressions[1][0] === "box") { + perfStart("fast-draw-loop"); + const inkExpr = expressions[0]; + const boxExpr = expressions[1]; + let paletteColors2 = null; + let sequenceKey = null; + let resolvedColors = null; + if (inkExpr.length === 2 && Array.isArray(inkExpr[1]) && inkExpr[1][0] === "...") { + paletteColors2 = inkExpr[1].slice(1); + sequenceKey = JSON.stringify(paletteColors2); + resolvedColors = paletteColors2.map((colorName) => { + if (api.help?.color) { + return api.help.color(colorName); + } + return colorName; + }); + if (!this.sequenceCounters) { + this.sequenceCounters = /* @__PURE__ */ new Map(); + } + if (!this.sequenceCounters.has(sequenceKey)) { + this.sequenceCounters.set(sequenceKey, 0); + } + } + const boxArgs = boxExpr.slice(1); + const analyzedBoxArgs = boxArgs.map((arg) => { + if (Array.isArray(arg) && arg.length === 3 && arg[0] === "*" && arg[1] === iteratorVar) { + return { type: "iterator_multiply", multiplier: arg[2] }; + } else if (this.containsVariable(arg, iteratorVar)) { + return { type: "variable", expr: arg }; + } else { + return { + type: "constant", + value: this.evaluate(arg, api, { ...this.localEnv, ...env }) + }; + } + }); + const prevLocalEnv2 = this.localEnv; + this.localEnvLevel += 1; + if (!this.localEnvStore[this.localEnvLevel]) { + this.localEnvStore[this.localEnvLevel] = /* @__PURE__ */ Object.create(null); + } + const loopEnv2 = this.localEnvStore[this.localEnvLevel]; + for (const key in this.localEnv) { + loopEnv2[key] = this.localEnv[key]; + } + for (const key in env) { + loopEnv2[key] = env[key]; + } + this.localEnv = loopEnv2; + for (let i2 = 0; i2 < count; i2++) { + loopEnv2[iteratorVar] = i2; + if (resolvedColors) { + const currentIndex = this.sequenceCounters.get(sequenceKey); + const nextIndex = (currentIndex + 1) % resolvedColors.length; + this.sequenceCounters.set(sequenceKey, nextIndex); + const color3 = resolvedColors[currentIndex]; + if (typeof color3 === "object" && color3.r !== void 0) { + api.ink(color3.r, color3.g, color3.b, color3.a || 255); + } else { + api.ink(color3); + } + } else { + this.evaluate(inkExpr, api, this.localEnv); + } + const evaluatedBoxArgs = analyzedBoxArgs.map((arg) => { + switch (arg.type) { + case "iterator_multiply": + return i2 * arg.multiplier; + // Direct multiplication, no evaluation + case "constant": + return arg.value; + // Pre-computed constant + case "variable": + return this.evaluate(arg.expr, api, this.localEnv); + default: + return 0; + } + }); + api.box(...evaluatedBoxArgs); + } + this.localEnvLevel -= 1; + this.localEnv = prevLocalEnv2; + perfEnd("fast-draw-loop"); + perfEnd("repeat-with-iterator"); + return result; + } + if (expressions.length === 2 && Array.isArray(expressions[0]) && expressions[0][0] === "ink" && Array.isArray(expressions[1]) && expressions[1][0] === "write") { + perfStart("fast-text-loop"); + const inkExpr = expressions[0]; + const writeExpr = expressions[1]; + let colorChoices = null; + if (inkExpr.length === 2 && Array.isArray(inkExpr[1]) && inkExpr[1][0] === "choose") { + colorChoices = inkExpr[1].slice(1); + } + let textChoices = null; + if (writeExpr.length >= 2 && Array.isArray(writeExpr[1]) && writeExpr[1][0] === "choose") { + textChoices = writeExpr[1].slice(1); + } + const prevLocalEnv2 = this.localEnv; + this.localEnvLevel += 1; + if (!this.localEnvStore[this.localEnvLevel]) { + this.localEnvStore[this.localEnvLevel] = /* @__PURE__ */ Object.create(null); + } + const loopEnv2 = this.localEnvStore[this.localEnvLevel]; + for (const key in this.localEnv) { + loopEnv2[key] = this.localEnv[key]; + } + for (const key in env) { + loopEnv2[key] = env[key]; + } + this.localEnv = loopEnv2; + for (let i2 = 0; i2 < count; i2++) { + loopEnv2[iteratorVar] = i2; + if (colorChoices) { + const colorIndex = Math.floor(this.seededRandom() * colorChoices.length); + api.ink?.(colorChoices[colorIndex]); + } else { + this.evaluate(inkExpr, api, this.localEnv); + } + let text, x, y; + if (textChoices) { + const textIndex = Math.floor(this.seededRandom() * textChoices.length); + text = textChoices[textIndex]; + } else { + text = this.fastEval(writeExpr[1], api, this.localEnv); + } + if (writeExpr.length >= 4) { + x = this.fastEval(writeExpr[2], api, this.localEnv); + y = this.fastEval(writeExpr[3], api, this.localEnv); + } else { + x = 0; + y = 0; + } + api.write?.(text, x, y); + } + this.localEnvLevel -= 1; + this.localEnv = prevLocalEnv2; + perfEnd("fast-text-loop"); + perfEnd("repeat-with-iterator"); + return result; + } + const baseEnv = { ...this.localEnv, ...env }; + const prevLocalEnv = this.localEnv; + const expressionAnalysis = expressions.map((expr) => { + const containsIterator = this.containsVariable ? this.containsVariable(expr, iteratorVar) : true; + return { expr, containsIterator }; + }); + const invariantResults = /* @__PURE__ */ new Map(); + expressionAnalysis.forEach((analysis, index) => { + if (!analysis.containsIterator) { + invariantResults.set( + index, + this.evaluate(analysis.expr, api, baseEnv) + ); + } + }); + this.localEnvLevel += 1; + if (!this.localEnvStore[this.localEnvLevel]) { + this.localEnvStore[this.localEnvLevel] = /* @__PURE__ */ Object.create(null); + } + const loopEnv = this.localEnvStore[this.localEnvLevel]; + for (const key in loopEnv) { + delete loopEnv[key]; + } + for (const key in baseEnv) { + loopEnv[key] = baseEnv[key]; + } + this.localEnv = loopEnv; + for (let i2 = 0; i2 < count; i2++) { + loopEnv[iteratorVar] = i2; + expressionAnalysis.forEach((analysis, index) => { + if (invariantResults.has(index)) { + result = invariantResults.get(index); + } else { + result = this.evaluate(analysis.expr, api, this.localEnv); + } + }); + } + this.localEnvLevel -= 1; + this.localEnv = prevLocalEnv; + perfEnd("repeat-with-iterator"); + } else { + const expressions = args.slice(1); + for (let i2 = 0; i2 < count; i2++) { + for (const expr of expressions) { + result = this.evaluate(expr, api, env); + } + } + } + return result; + }, + // Abbreviation for repeat + rep: (api, args, env) => { + return this.getGlobalEnv().repeat(api, args, env); + }, + // Alias for repeat - "bunching together" code clods + bunch: (api, args, env) => { + return this.getGlobalEnv().repeat(api, args, env); + }, + // 🎲 Random selection (optimized with caching for performance) + choose: (api, args = []) => { + if (args.length === 0) return void 0; + if (args.length <= 8) { + const cacheKey = JSON.stringify(args); + if (!this.choiceCache) this.choiceCache = /* @__PURE__ */ new Map(); + let cachedArgs = this.choiceCache.get(cacheKey); + if (!cachedArgs) { + cachedArgs = [...args]; + this.choiceCache.set(cacheKey, cachedArgs); + if (this.choiceCache.size > 100) { + const firstKey = this.choiceCache.keys().next().value; + this.choiceCache.delete(firstKey); + } + } + const randomIndex2 = Math.floor(this.seededRandom() * cachedArgs.length); + return cachedArgs[randomIndex2]; + } + if (api.help?.choose) { + return api.help.choose(...args); + } + const randomIndex = Math.floor(this.seededRandom() * args.length); + return args[randomIndex]; + }, + // 🎲 Random selection (alias) + "?": (api, args = []) => { + if (args.length > 0) { + if (api.help?.choose) { + return api.help.choose(...args); + } + const randomIndex = Math.floor(this.seededRandom() * args.length); + return args[randomIndex]; + } + console.log("\u{1F3B2} ? called with no arguments, returning undefined"); + return void 0; + }, + // 🔄 Sequential selection (cycles through arguments in order) + "...": (api, args = [], env) => { + if (args.length === 0) return void 0; + const sequenceKey = JSON.stringify(args); + if (!this.sequenceCounters) { + this.sequenceCounters = /* @__PURE__ */ new Map(); + } + if (!this.sequenceCounters.has(sequenceKey)) { + this.sequenceCounters.set(sequenceKey, 0); + } + const currentIndex = this.sequenceCounters.get(sequenceKey); + const nextIndex = (currentIndex + 1) % args.length; + this.sequenceCounters.set(sequenceKey, nextIndex); + return args[currentIndex]; + }, + // 🔄 Sequential selection (alias with two dots) + "..": (api, args = [], env) => { + return this.getGlobalEnv()["..."](api, args, env); + }, + // 🔈 Voices — the same physical models the JS pads use (lib/pads.mjs), said + // in Lisp. `overtone` below is a bare square wave; these have bodies. + // + // (pluck c4) a struck string + // (bell e5 0.3) shimmering, long tail — second arg is volume + // (sub a1) warm low end + // (flute g4) breathy waveguide lead + // (hat) a closed hi-hat tick + // (voice harp c4 0.4 -0.5) any type, volume, pan + // + // A note is what you'd say out loud — c4, e5, a1 — because a language you + // can't say out loud is a language nobody writes music in. + // Sound is a boundary: `synth` isn't there on the first frames, and a voice + // that throws takes the whole evaluation down with it. A pad with no audio + // yet should draw silently, not die. + // + // The NOTE arrives unevaluated (these heads are in the no-eval list), because + // `c4` is ALREADY a color in KidLisp — it evaluates to [0,255,0], and a synth + // handed a green triple says "Note not found in the list". In a voice call a + // note is a note; nothing else gets to claim that name. Every OTHER argument + // is evaluated by hand, so volume and pan can still be expressions. + pluck: (api, args = [], env) => api.sound?.synth && voices.pluck(api.sound.synth, note(args[0], "c4"), { + volume: numArg(api, env, args[1], 0.5), + pan: numArg(api, env, args[2], 0) + }), + bell: (api, args = [], env) => api.sound?.synth && voices.bell(api.sound.synth, note(args[0], "e5"), { + volume: numArg(api, env, args[1], 0.4), + pan: numArg(api, env, args[2], 0) + }), + sub: (api, args = [], env) => api.sound?.synth && voices.sub(api.sound.synth, note(args[0], "a1"), { + volume: numArg(api, env, args[1], 0.5), + pan: numArg(api, env, args[2], 0) + }), + flute: (api, args = [], env) => api.sound?.synth && voices.flute(api.sound.synth, note(args[0], "g4"), { + volume: numArg(api, env, args[1], 0.3), + pan: numArg(api, env, args[2], 0) + }), + hat: (api, args = [], env) => api.sound?.synth && voices.hat(api.sound.synth, { + volume: numArg(api, env, args[0], 0.14), + pan: numArg(api, env, args[1], 0) + }), + // The whole synth, for when a named voice isn't the one you hear. + // (voice sine c4 0.4 0 1.2) → type, note, volume, pan, beats + voice: (api, args = [], env) => { + const synth = api.sound?.synth; + if (!synth) return; + return synth({ + type: unquoteString(String(args[0] ?? "sine")), + tone: note(args[1], "c4"), + volume: numArg(api, env, args[2], 0.4), + pan: numArg(api, env, args[3], 0), + beats: numArg(api, env, args[4], 0.6), + attack: numArg(api, env, args[5], 0.01), + decay: numArg(api, env, args[6], 0.6) + }); + }, + // 🔈 Sound + overtone: (api, args = []) => { + let tone; + if (args[0] === void 0) { + tone = 440; + } else { + tone = args[0] * args[1] / args[2]; + } + return api.sound.synth({ + type: "square", + tone, + // beats: 0.1, + duration: Infinity, + attack: 0.01, + decay: 0.5, + volume: 0.15 + }); + }, + rainbow: (api) => { + return "rainbow"; + }, + zebra: (api) => { + return "zebra"; + }, + // 🎨 Noise generation + noise: (api, args = []) => { + const variant = args.length > 0 ? unquoteString(args[0]) : null; + if (variant === "digitpain") { + api.noise16DIGITPAIN?.(); + } else if (variant === "aesthetic") { + api.noise16Aesthetic?.(); + } else if (variant === "sotce") { + api.noise16Sotce?.(); + } else { + api.noise16?.(); + } + }, + // 🔧 Debug function + debug: (api, args = []) => { + if (isKidlispConsoleEnabled()) { + postKidlispConsole("log", `\u{1F527} DEBUG: ${formatConsoleArgs(args)}`, { kind: "debug" }); + } + console.log("\u{1F527} DEBUG:", args); + return "debug called"; + }, + log: (api, args = []) => { + if (isKidlispConsoleEnabled()) { + postKidlispConsole("log", `\u{1F4DD} LOG: ${formatConsoleArgs(args)}`, { kind: "log" }); + } + console.log("\u{1F4DD} LOG:", ...args); + return args[0]; + }, + // 💾 Cache function - loads cached KidLisp code using nanoid + // Usage: (cache abc123XY) or (cache $abc123XY) loads cached code + cache: (api, args = []) => { + if (args.length === 0) { + console.warn("\u2757 cache function requires a nanoid argument"); + return void 0; + } + let cacheId = unquoteString(args[0].toString()); + if (cacheId.startsWith("$")) { + cacheId = cacheId.slice(1); + } + if (!cacheId || !/^[0-9A-Za-z]+$/.test(cacheId)) { + console.warn("\u2757 Invalid cache code:", cacheId); + return cacheId; + } + return getCachedCodeMultiLevel(cacheId).then((source) => { + if (source) { + const parsed = this.parse(source); + return this.evaluate(parsed, api, this.localEnv); + } else { + return void 0; + } + }).catch((error) => { + console.error("\u274C Error loading cached code:", cacheId, error); + return void 0; + }); + }, + // 🚫 Disable wrapper - ignores wrapped expressions + no: (api, args = []) => { + return void 0; + }, + // ✅ Enable wrapper - passthrough that evaluates wrapped expressions + yes: (api, args = []) => { + if (args.length === 0) return void 0; + return this.evaluate(args[0], api, this.localEnv); + }, + // 🎤 Microphone (function version for backward compatibility and explicit control) + mic: (api, args = []) => { + this.requestMicrophoneConnection(api); + return this.globalDef.mic; + }, + // 🔊 Real-time audio amplitude from speakers/system audio + amplitude: (api, args = []) => { + let amplitudeValue = 0; + if (api.sound?.speaker?.amplitudes?.left !== void 0) { + amplitudeValue = api.sound.speaker.amplitudes.left; + } else if (api.sound?.speaker?.amplitudes?.right !== void 0) { + amplitudeValue = api.sound.speaker.amplitudes.right; + } + return amplitudeValue; + }, + // 🎵 Melody - plays a sequence of notes in a loop + melody: (api, args = []) => this.runMelody(api, args), + // 🔊 Speaker - returns whether sound is enabled + speaker: (api) => { + return api.sound?.enabled?.() || false; + if (!this.speakerCache) this.speakerCache = { result: null, timestamp: 0 }; + const now = performance.now(); + if (this.speakerCache.result !== null && now - this.speakerCache.timestamp < 2e3) { + return this.speakerCache.result; + } + const soundEnabled = api.sound?.enabled?.() || false; + this.speakerCache.result = soundEnabled; + this.speakerCache.timestamp = now; + return soundEnabled; + }, + // 🎨 Backdrop - shorthand for (once (wipe color)) to set background once + backdrop: (api, args = []) => { + const performBackdrop = () => { + if (args.length === 0) { + console.error("\u2757 Invalid `backdrop`. Requires at least one color argument."); + return; + } + const backdropKey = "backdrop_" + JSON.stringify(args); + if (!this.onceExecuted.has(backdropKey)) { + this.onceExecuted.add(backdropKey); + if (api.wipe) { + if (args.length === 1) { + return api.wipe(this.evaluate(args[0], api, this.localEnv)); + } else if (args.length === 3) { + return api.wipe( + this.evaluate(args[0], api, this.localEnv), + this.evaluate(args[1], api, this.localEnv), + this.evaluate(args[2], api, this.localEnv) + ); + } else if (args.length === 4) { + return api.wipe( + this.evaluate(args[0], api, this.localEnv), + this.evaluate(args[1], api, this.localEnv), + this.evaluate(args[2], api, this.localEnv), + this.evaluate(args[3], api, this.localEnv) + ); + } + } + } + return void 0; + }; + if (this.suppressDrawingBeforeBake) { + this.executePreBakeDraw(api, performBackdrop); + return; + } + return performBackdrop(); + }, + // Programmatically add all CSS color constants to the global environment. + ...Object.keys(cssColors2).reduce((acc, colorName) => { + acc[colorName] = () => cssColors2[colorName]; + return acc; + }, {}), + // Add static color codes (c0, c1, c2, etc.) using standardized mapping + ...Object.keys(staticColorMap).reduce((acc, index) => { + acc[`c${index}`] = () => staticColorMap[index]; + return acc; + }, {}), + // Add palette codes (p0, p1, etc.) + p0: () => "rainbow", + p1: () => "zebra", + // 🍞 Bake function - creates a new painting buffer for subsequent drawing + // Like: bakes[index] || painting(screen.width, screen.height, (api) => {...}) + bake: (api, args = []) => { + if (api.setEraseTarget) api.setEraseTarget(null, 0); + if (this.isEmbeddedContext) { + return 0; + } + if (!this.bakes) { + this.bakes = []; + this.currentBakeIndex = -1; + } + this.currentBakeIndex++; + const width2 = this.displayBuffer?.width || api.screen?.width || 256; + const height2 = this.displayBuffer?.height || api.screen?.height || 256; + if (this.bakes[this.currentBakeIndex]) { + const existing2 = this.bakes[this.currentBakeIndex]; + if (existing2.width === width2 && existing2.height === height2) { + existing2.burned = false; + api.page(existing2); + if (!api.screen) { + api.screen = { width: existing2.width, height: existing2.height, pixels: existing2.pixels }; + } else { + api.screen.width = existing2.width; + api.screen.height = existing2.height; + api.screen.pixels = existing2.pixels; + } + return this.currentBakeIndex; + } else { + const oldLayer = this.bakes[this.currentBakeIndex]; + const oldPixels = oldLayer.pixels; + const oldWidth = oldLayer.width; + const oldHeight = oldLayer.height; + const resizedLayer = this.createOrReuseBuffer(width2, height2); + resizedLayer.burned = false; + if (oldPixels && oldPixels.length > 0 && !(oldPixels.buffer && oldPixels.buffer.detached)) { + const copyWidth = Math.min(oldWidth, width2); + const copyHeight = Math.min(oldHeight, height2); + for (let y = 0; y < copyHeight; y++) { + const srcOffset = y * oldWidth * 4; + const destOffset = y * width2 * 4; + const rowLength = copyWidth * 4; + if (srcOffset + rowLength <= oldPixels.length && destOffset + rowLength <= resizedLayer.pixels.length) { + resizedLayer.pixels.set(oldPixels.subarray(srcOffset, srcOffset + rowLength), destOffset); + } + } + } + this.returnBufferToPool(oldLayer, oldWidth, oldHeight); + this.bakes[this.currentBakeIndex] = resizedLayer; + api.page(resizedLayer); + if (!api.screen) { + api.screen = { width: resizedLayer.width, height: resizedLayer.height, pixels: resizedLayer.pixels }; + } else { + api.screen.width = resizedLayer.width; + api.screen.height = resizedLayer.height; + api.screen.pixels = resizedLayer.pixels; + } + return this.currentBakeIndex; + } + } + const bakeBuffer = this.createOrReuseBuffer(width2, height2); + bakeBuffer.burned = false; + this.bakes[this.currentBakeIndex] = bakeBuffer; + api.page(bakeBuffer); + if (!api.screen) { + api.screen = { width: bakeBuffer.width, height: bakeBuffer.height, pixels: bakeBuffer.pixels }; + } else { + api.screen.width = bakeBuffer.width; + api.screen.height = bakeBuffer.height; + api.screen.pixels = bakeBuffer.pixels; + } + return this.currentBakeIndex; + }, + // 🔥 Burn function - composites all visible layers into a single frozen buffer + // (burn zoom 1.5) - zooms all bake layers by 1.5x + // This lets you apply transformations to the entire bake stack at once + burn: (api, args = []) => { + if (api.setEraseTarget) api.setEraseTarget(null, 0); + if (this.isEmbeddedContext) { + return 0; + } + const width2 = this.displayBuffer?.width || api.screen?.width || 256; + const height2 = this.displayBuffer?.height || api.screen?.height || 256; + if (this.burnedBuffer && this.burnedBuffer.width === width2 && this.burnedBuffer.height === height2) { + this.burnedBuffer.pixels.fill(0); + } else { + if (this.burnedBuffer) { + this.returnBufferToPool(this.burnedBuffer, this.burnedBuffer.width, this.burnedBuffer.height); + } + this.burnedBuffer = this.createOrReuseBuffer(width2, height2); + } + api.page(this.burnedBuffer); + if (this.layer0 && this.layer0.pixels) { + if (this.layer0.width === width2 && this.layer0.height === height2) { + try { + api.paste(this.layer0, 0, 0); + } catch (error) { + console.warn(`\u26A0\uFE0F Burn: Error pasting layer0:`, error.message); + } + } else { + console.warn(`\u26A0\uFE0F Burn: Skipping layer0 paste due to dimension mismatch: layer0=${this.layer0.width}x${this.layer0.height}, burn=${width2}x${height2}`); + } + } + if (this.bakes) { + const bakesLength = this.bakes.length; + for (let i2 = 0; i2 < bakesLength; i2++) { + const bakeLayer = this.bakes[i2]; + if (bakeLayer && bakeLayer.pixels && !bakeLayer.burned) { + if (bakeLayer.width === width2 && bakeLayer.height === height2) { + try { + api.paste(bakeLayer, 0, 0); + } catch (error) { + console.warn(`\u26A0\uFE0F Burn: Error pasting bake layer ${i2}:`, error.message); + } + if (bakeLayer.eraseMask) { + const dst = this.burnedBuffer.pixels; + const mask2 = bakeLayer.eraseMask; + const totalPixels = bakeLayer.width * bakeLayer.height; + for (let p = 0; p < totalPixels; p++) { + if (mask2[p] > 0) { + const di = p * 4; + const normalAlpha = 1 - mask2[p] / 255; + dst[di + 3] = dst[di + 3] * normalAlpha + 0.5 | 0; + if (dst[di + 3] === 0) { + dst[di] = 32; + dst[di + 1] = 32; + dst[di + 2] = 32; + } + } + } + } + } else { + console.warn(`\u26A0\uFE0F Burn: Skipping bake layer ${i2} paste due to dimension mismatch: bake=${bakeLayer.width}x${bakeLayer.height}, burn=${width2}x${height2}`); + } + } + } + for (let i2 = 0; i2 < bakesLength; i2++) { + const bakeLayer = this.bakes[i2]; + if (bakeLayer) { + bakeLayer.burned = true; + } + } + } + return 0; + }, + // � Tape function - loads and plays tape recordings as embedded video + // Usage: (tape !CODE x y w h) - paste tape frame at position with dimensions + // (tape !CODE x y w h speed) - with playback speed (1.0 = normal) + // (tape !CODE x y) - paste at position with original dimensions + // (tape !CODE) - paste fullscreen + tape: (api, args = []) => { + if (args.length === 0) { + console.warn("\u2757 tape function requires a tape code argument (e.g., !abc123)"); + return void 0; + } + let tapeCode = unquoteString(args[0].toString()); + if (tapeCode.startsWith("!")) { + tapeCode = tapeCode.slice(1); + } + if (!tapeCode || !/^[0-9A-Za-z]+$/.test(tapeCode)) { + console.warn("\u2757 Invalid tape code:", tapeCode); + return void 0; + } + const screenWidth = api.screen?.width || 256; + const screenHeight = api.screen?.height || 256; + let x = 0, y = 0, w = screenWidth, h = screenHeight, speed = 1; + if (args.length >= 3) { + x = this.evaluate(args[1], api, this.localEnv) || 0; + y = this.evaluate(args[2], api, this.localEnv) || 0; + } + if (args.length >= 5) { + w = this.evaluate(args[3], api, this.localEnv) || screenWidth; + h = this.evaluate(args[4], api, this.localEnv) || screenHeight; + } + if (args.length >= 6) { + speed = this.evaluate(args[5], api, this.localEnv) || 1; + } + let tapeEmbed = this.tapeEmbeds.get(tapeCode); + if (!tapeEmbed) { + tapeEmbed = { + frames: [], + frameIndex: 0, + isLoading: true, + timing: null, + width: 0, + height: 0, + lastFrameTime: performance.now(), + frameDuration: 1e3 / 30, + // Default 30fps + loadError: null + }; + this.tapeEmbeds.set(tapeCode, tapeEmbed); + console.log(`\u{1F4FC} KidLisp: Loading tape !${tapeCode}...`); + if (api.send) { + const zipUrl = `${typeof location !== "undefined" ? location.origin : ""}/media/tapes/${tapeCode}`; + api.send({ + type: "tape:preload", + content: { + tapeId: `kidlisp-${tapeCode}`, + code: tapeCode, + zipUrl, + requestFrames: true + // Request frames to be sent back + } + }); + } else { + console.warn("\u2757 Cannot load tape - api.send not available"); + tapeEmbed.isLoading = false; + tapeEmbed.loadError = "No send API"; + } + return void 0; + } + if (tapeEmbed.isLoading) { + return void 0; + } + if (tapeEmbed.loadError) { + console.warn(`\u2757 Tape !${tapeCode} failed to load:`, tapeEmbed.loadError); + return void 0; + } + if (!tapeEmbed.frames || tapeEmbed.frames.length === 0) { + return void 0; + } + const now = performance.now(); + const elapsed = now - tapeEmbed.lastFrameTime; + const adjustedDuration = tapeEmbed.frameDuration / speed; + if (elapsed >= adjustedDuration) { + tapeEmbed.frameIndex = (tapeEmbed.frameIndex + 1) % tapeEmbed.frames.length; + tapeEmbed.lastFrameTime = now; + } + const frame = tapeEmbed.frames[tapeEmbed.frameIndex]; + if (!frame) return void 0; + if (api.paste && frame) { + try { + const transform = { width: w, height: h }; + api.paste(frame, x, y, transform); + } catch (e2) { + console.warn(`\u{1F4FC} Tape frame paste error:`, e2.message); + } + } + return tapeEmbed.frameIndex; + }, + // �🖼️ Embed function - loads cached KidLisp code and creates persistent animated layers + // Usage: (embed $pie) - loads cached code in default 256x256 layer (fixed size for cache efficiency) + // (embed $pie 128 128) - loads cached code in 128x128 layer + // (embed $pie 0 0 60 40) - loads cached code in 60x40 layer at position (0,0) + // (embed $pie 0 0 60 40 128) - loads cached code in 60x40 layer with alpha 128 (0-255, or 0.0-1.0) + embed: (api, args = []) => { + if (args.length === 0) { + console.warn("\u2757 embed function requires a cached code argument"); + return void 0; + } + let cacheId = unquoteString(args[0].toString()); + if (cacheId.startsWith("$")) { + cacheId = cacheId.slice(1); + } + if (!cacheId || !/^[0-9A-Za-z]+$/.test(cacheId)) { + console.warn("\u2757 Invalid cache code:", cacheId); + return void 0; + } + const screenWidth = api.screen?.width || 512; + const screenHeight = api.screen?.height || 512; + let width2 = screenWidth, height2 = screenHeight, x = 0, y = 0, alpha = 255; + let usesScreenDimensions = true; + if (args.length >= 2) { + if (args.length === 2) { + const evalX = this.evaluate(args[1], api, this.localEnv); + x = evalX !== void 0 && evalX !== null ? evalX : 0; + } else if (args.length === 3) { + const evalX = this.evaluate(args[1], api, this.localEnv); + const evalY = this.evaluate(args[2], api, this.localEnv); + x = evalX !== void 0 && evalX !== null ? evalX : 0; + y = evalY !== void 0 && evalY !== null ? evalY : 0; + } else if (args.length === 4) { + const evalX = this.evaluate(args[1], api, this.localEnv); + const evalY = this.evaluate(args[2], api, this.localEnv); + x = evalX !== void 0 && evalX !== null ? evalX : 0; + y = evalY !== void 0 && evalY !== null ? evalY : 0; + const sizeValue = this.evaluate(args[3], api, this.localEnv); + if (!sizeValue) { + usesScreenDimensions = true; + width2 = screenWidth; + height2 = screenWidth; + } else { + width2 = sizeValue; + height2 = sizeValue; + } + } else if (args.length === 5) { + const widthArg = args[3]; + const heightArg = args[4]; + if (typeof widthArg === "string" && widthArg === "w" || typeof heightArg === "string" && heightArg === "h") { + usesScreenDimensions = true; + } + const evalX = this.evaluate(args[1], api, this.localEnv); + const evalY = this.evaluate(args[2], api, this.localEnv); + x = evalX !== void 0 && evalX !== null ? evalX : 0; + y = evalY !== void 0 && evalY !== null ? evalY : 0; + const widthValue = this.evaluate(args[3], api, this.localEnv); + const heightValue = this.evaluate(args[4], api, this.localEnv); + if (!widthValue || !heightValue) { + usesScreenDimensions = true; + } + width2 = widthValue || screenWidth; + height2 = heightValue || screenHeight; + } else if (args.length >= 6) { + const widthArg = args[3]; + const heightArg = args[4]; + if (typeof widthArg === "string" && widthArg === "w" || typeof heightArg === "string" && heightArg === "h") { + usesScreenDimensions = true; + } + const evalX = this.evaluate(args[1], api, this.localEnv); + const evalY = this.evaluate(args[2], api, this.localEnv); + x = evalX !== void 0 && evalX !== null ? evalX : 0; + y = evalY !== void 0 && evalY !== null ? evalY : 0; + const widthValue = this.evaluate(args[3], api, this.localEnv); + const heightValue = this.evaluate(args[4], api, this.localEnv); + if (!widthValue || !heightValue) { + usesScreenDimensions = true; + } + width2 = widthValue || screenWidth; + height2 = heightValue || screenHeight; + alpha = this.evaluate(args[5], api, this.localEnv); + if (alpha !== void 0 && alpha !== null && alpha <= 1 && alpha >= 0) { + alpha = Math.floor(alpha * 255); + } + if (alpha === void 0 || alpha === null) { + alpha = 255; + } + } + } + width2 = Math.max(1, Math.floor(width2)); + height2 = Math.max(1, Math.floor(height2)); + x = Math.floor(x); + y = Math.floor(y); + alpha = Math.max(0, Math.min(255, Math.floor(alpha))); + const rightEdge = x + width2; + const bottomEdge = y + height2; + const halfWidth = Math.ceil(screenWidth / 2); + const halfHeight = Math.ceil(screenHeight / 2); + if (rightEdge > 0 && rightEdge < screenWidth && screenWidth - rightEdge <= 1) { + width2 = screenWidth - x; + } + if (bottomEdge > 0 && bottomEdge < screenHeight && screenHeight - bottomEdge <= 1) { + height2 = screenHeight - y; + } + if (x === 0 && rightEdge > 0 && rightEdge < halfWidth && halfWidth - rightEdge <= 1) { + width2 = halfWidth; + } + if (y === 0 && bottomEdge > 0 && bottomEdge < halfHeight && halfHeight - bottomEdge <= 1) { + height2 = halfHeight; + } + let normalizedWidth, normalizedHeight; + if (usesScreenDimensions) { + normalizedWidth = width2; + normalizedHeight = height2; + } else { + normalizedWidth = width2 <= 128 ? 128 : width2 <= 256 ? 256 : width2 <= 512 ? 512 : width2; + normalizedHeight = height2 <= 128 ? 128 : height2 <= 256 ? 256 : height2 <= 512 ? 512 : height2; + } + const screenSuffix = usesScreenDimensions ? `_screen${screenWidth}x${screenHeight}` : ""; + const layerKey = `${cacheId}_${normalizedWidth}x${normalizedHeight}_${x},${y}_${alpha}${screenSuffix}`; + if (this.embeddedLayerCache && usesScreenDimensions) { + const currentScreenKey = `_screen${screenWidth}x${screenHeight}`; + const entriesToDelete = []; + const layerIndicesToRemove = []; + for (const [key, layer] of this.embeddedLayerCache.entries()) { + if (key.includes("_screen") && !key.includes(currentScreenKey)) { + entriesToDelete.push(key); + if (layer && layer.buffer) { + this.returnBufferToPool(layer.buffer, layer.width, layer.height); + } + if (this.embeddedLayers) { + const layerIndex = this.embeddedLayers.findIndex((l2) => l2.cacheId === key); + if (layerIndex !== -1) { + layerIndicesToRemove.push(layerIndex); + } + } + } + } + if (entriesToDelete.length > 0) { + entriesToDelete.forEach((key) => { + this.embeddedLayerCache.delete(key); + }); + if (layerIndicesToRemove.length > 0 && this.embeddedLayers) { + layerIndicesToRemove.sort((a2, b2) => b2 - a2); + layerIndicesToRemove.forEach((idx) => { + this.embeddedLayers.splice(idx, 1); + }); + console.log(`\u2728 Cleared ${entriesToDelete.length} outdated responsive cache entries and ${layerIndicesToRemove.length} embedded layers`); + } + } + } + if (this.embeddedLayerCache && this.embeddedLayerCache.has(layerKey)) { + const existingLayer = this.embeddedLayerCache.get(layerKey); + if (existingLayer.disabled) { + return void 0; + } + existingLayer.x = x; + existingLayer.y = y; + existingLayer.alpha = alpha; + if (existingLayer.width !== width2 || existingLayer.height !== height2) { + } + if (this.currentTimingContext) { + const timingCtx = this.currentTimingContext; + const selectedArg = timingCtx.selectedArg; + let isCurrentlySelected = false; + if (Array.isArray(selectedArg) && selectedArg.length > 0) { + isCurrentlySelected = selectedArg[0] === `$${cacheId}`; + } else if (typeof selectedArg === "string") { + isCurrentlySelected = selectedArg === `$${cacheId}`; + } + if (!isCurrentlySelected) { + console.log(`\u23F8\uFE0F SKIPPING EMBED ${cacheId} - not selected. Current:`, JSON.stringify(selectedArg)); + return void 0; + } else { + console.log(`\u2705 ALLOWING EMBED ${cacheId} - is selected`); + } + } + const shouldExecute = true; + const shouldRender = this.updateEmbeddedLayer(api, existingLayer); + if (existingLayer.buffer && api.paste) { + } else { + } + return existingLayer; + } + if (!this.embeddedLayerCache) { + this.embeddedLayerCache = /* @__PURE__ */ new Map(); + } + this.loadingEmbeddedLayers.add(cacheId); + if (this.embeddedSourceCache.has(cacheId)) { + const cachedSource = this.embeddedSourceCache.get(cacheId); + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + return this.createEmbeddedLayerFromSource(cachedSource, cacheId, layerKey, width2, height2, x, y, alpha, api); + } + const globalScope = (function() { + if (typeof window !== "undefined") return window; + if (typeof globalThis !== "undefined") return globalThis; + if (typeof global !== "undefined") return global; + if (typeof self !== "undefined") return self; + return {}; + })(); + if (globalScope.objktKidlispCodes && globalScope.objktKidlispCodes[cacheId]) { + const teiaSource = globalScope.objktKidlispCodes[cacheId]; + console.log(`\u{1F3AF} Using OBJKT cached code for embedded layer: ${cacheId}`); + this.embeddedSourceCache.set(cacheId, teiaSource); + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + return this.createEmbeddedLayerFromSource(teiaSource, cacheId, layerKey, width2, height2, x, y, alpha, api); + } + const fetchKey = `${cacheId}_fetching_source`; + if (this.embeddedLayerCache.has(fetchKey)) { + return this.embeddedLayerCache.get(fetchKey); + } + const placeholderSource = `(fps 24) +(wipe 32 32 32 128) +(ink 200 200 200) +(write (+ "Loading " ${JSON.stringify(cacheId)} "...") 4 4)`; + const placeholderLayer = this.createEmbeddedLayerFromSource( + placeholderSource, + cacheId, + layerKey, + width2, + height2, + x, + y, + alpha, + api + ); + console.log(`\u2705 Placeholder layer created for ${cacheId}:`, placeholderLayer ? "success" : "failed"); + const backgroundFetch = Promise.race([ + getCachedCodeMultiLevel(cacheId), + new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 1e4); + }) + ]).then((source) => { + this.embeddedLayerCache.delete(fetchKey); + this.loadingEmbeddedLayers.delete(cacheId); + this.loadedEmbeddedLayers.add(cacheId); + if (!source) { + source = `(fps 24) +(wipe red) +(ink yellow) +(line 0 64 128 64) +(ink green) +(line 64 0 64 128)`; + } + this.embeddedSourceCache.set(cacheId, source); + if (placeholderLayer && this.embeddedLayerCache.has(layerKey)) { + const existingLayer = this.embeddedLayerCache.get(layerKey); + if (existingLayer) { + if (existingLayer.buffer && existingLayer.buffer.pixels) { + existingLayer.buffer.pixels.fill(0); + } + existingLayer.source = source; + existingLayer.sourceCode = source; + existingLayer.kidlispInstance.source = source; + existingLayer.parsedCode = existingLayer.kidlispInstance.parse(source); + existingLayer.kidlispInstance.firstLineColor = null; + existingLayer.kidlispInstance.ast = JSON.parse( + JSON.stringify(existingLayer.parsedCode) + ); + existingLayer.kidlispInstance.detectFirstLineColor(); + existingLayer.firstLineColorApplied = false; + existingLayer.localFrameCount = 0; + existingLayer.timingPattern = this.extractTimingPattern(source); + console.log(`\u{1F504} Updated embedded layer ${cacheId} with real source code`); + } + } + return placeholderLayer; + }).catch((error) => { + console.error("\u274C Error fetching embedded layer source:", cacheId, error); + this.embeddedLayerCache.delete(fetchKey); + return placeholderLayer; + }); + this.embeddedLayerCache.set(fetchKey, backgroundFetch); + return placeholderLayer; + }, + jump: (api, args = [], env) => { + if (args.length === 0) { + console.warn("\u2757 jump function requires a destination argument"); + return; + } + let destination = args[0]; + if (typeof destination === "string" && destination.startsWith("$")) { + destination = destination; + } else if (typeof destination === "string") { + destination = unquoteString(destination); + } else { + destination = this.evaluate(destination, api, env); + if (typeof destination === "object" && destination !== null) { + if (destination.toString && typeof destination.toString === "function") { + destination = destination.toString(); + } else { + console.warn("\u2757 Invalid jump destination type:", typeof destination); + return; + } + } else { + destination = String(destination); + } + } + if (typeof destination === "string" && destination.startsWith("$")) { + const cacheId = destination.slice(1); + if (globalCodeCache.has(cacheId)) { + console.log(`\u{1F680} KidLisp fast-jumping to cached code: ${destination} (no network request needed)`); + } else { + console.log(`\u{1F680} KidLisp jumping to ${destination} (will check IndexedDB and network if needed)`); + } + } + const ahistorical = args.length > 1 ? args[1] : false; + const alias = args.length > 2 ? args[2] : false; + if (api.jump) { + console.log("\u{1F680} KidLisp jumping to:", destination); + api.jump(destination, ahistorical, alias); + } else { + console.warn("\u2757 Jump API function not available"); + } + } + }; + return this.globalEnvCache; + } + // Context-aware randomization for ? tokens + contextAwareRandom(functionName, argIndex, api) { + const width2 = api.screen?.width || 256; + const height2 = api.screen?.height || 256; + const contextRanges = { + box: [ + { min: 0, max: width2 }, + // x position (arg 0) + { min: 0, max: height2 }, + // y position (arg 1) + { min: 10, max: 100 }, + // width (arg 2) + { min: 10, max: 100 } + // height (arg 3) + ], + line: [ + { min: 0, max: width2 }, + // x1 (arg 0) + { min: 0, max: height2 }, + // y1 (arg 1) + { min: 0, max: width2 }, + // x2 (arg 2) + { min: 0, max: height2 } + // y2 (arg 3) + ], + circle: [ + { min: 0, max: width2 }, + // x position (arg 0) + { min: 0, max: height2 }, + // y position (arg 1) + { min: 5, max: 50 } + // radius (arg 2) + ], + tri: [ + { min: 0, max: width2 }, + // x1 position (arg 0) + { min: 0, max: height2 }, + // y1 position (arg 1) + { min: 0, max: width2 }, + // x2 position (arg 2) + { min: 0, max: height2 }, + // y2 position (arg 3) + { min: 0, max: width2 }, + // x3 position (arg 4) + { min: 0, max: height2 } + // y3 position (arg 5) + ], + write: [ + { min: 0, max: width2 }, + // x position (arg 1, since arg 0 is text) + { min: 0, max: height2 } + // y position (arg 2) + ], + ink: [ + { min: 0, max: 255 }, + // red (arg 0) + { min: 0, max: 255 }, + // green (arg 1) + { min: 0, max: 255 }, + // blue (arg 2) + { min: 0, max: 255 } + // alpha (arg 3) + ], + flood: [ + { min: 0, max: width2 }, + // x position (arg 0) + { min: 0, max: height2 } + // y position (arg 1) + ], + embed: [ + // arg 0 is the cache code (string), so no range for that + { min: 32, max: 512 }, + // width (arg 1) + { min: 32, max: 512 }, + // height (arg 2) + { min: 0, max: width2 }, + // x position (arg 3 in 5-arg version) + { min: 0, max: height2 }, + // y position (arg 4 in 5-arg version) + { min: 32, max: 512 }, + // width (arg 5 in 5-arg version) + { min: 32, max: 512 } + // height (arg 6 in 5-arg version) + ] + }; + const functionRanges = contextRanges[functionName]; + if (functionRanges && functionRanges[argIndex]) { + const range = functionRanges[argIndex]; + return Math.floor(this.seededRandom() * (range.max - range.min + 1)) + range.min; + } + return Math.floor(this.seededRandom() * 256); + } + // Fast evaluation for common expressions to avoid full recursive evaluation + fastEval(expr, api, env) { + if (typeof expr === "number") return expr; + if (typeof expr === "string") { + if (expr === "?") { + return Math.floor(this.seededRandom() * 256); + } + if (expr.startsWith("-") && expr.length > 1) { + const baseIdentifier = expr.substring(1); + const baseValue = this.fastEval(baseIdentifier, api, env); + if (typeof baseValue === "number") { + return -baseValue; + } + } + let value; + if (Object.prototype.hasOwnProperty.call(this.localEnv, expr)) { + value = this.localEnv[expr]; + } else if (env && Object.prototype.hasOwnProperty.call(env, expr)) { + value = env[expr]; + } else if (Object.prototype.hasOwnProperty.call(this.globalDef, expr)) { + value = this.globalDef[expr]; + if (expr === "mic") { + console.log("\u{1F3A4} Mic global accessed! Current value:", this.globalDef.mic.toFixed(3)); + this.requestMicrophoneConnection(api); + } + if (expr === "amp" && this.frameCount % 60 === 0) { + console.log("\u{1F50A} KL amp resolved:", value, "globalDef.amp:", this.globalDef.amp); + } + } + if (value !== void 0) { + return value; + } + const globalEnv = this.getGlobalEnv(); + value = globalEnv[expr]; + if (typeof value === "function") { + if (expr.startsWith("fade:")) { + return expr; + } + value = value(api, []); + return value; + } + if (expr.startsWith("$") && expr.length > 1) { + const cacheId = expr.slice(1); + if (/^[a-zA-Z0-9]{1,12}$/.test(cacheId)) { + console.log(`\u{1F3AF} Processing dollar code: ${expr} -> cacheId: ${cacheId}`); + const embedFunc = globalEnv.embed; + if (embedFunc) { + console.log(`\u{1F527} Calling embed function for ${expr}`); + return embedFunc(api, [cacheId]); + } else { + console.warn("\u274C embed function not found in global environment"); + return expr; + } + } + } + if (value === void 0 && NOTE_LITERAL.test(expr)) return expr; + if (value === void 0 && validIdentifierRegex.test(expr) && !this.unknownWordsLogged?.has(expr) && expr !== "fps" && expr !== "s" && expr.length > 1) { + this.unknownWordsLogged?.add(expr); + const sourcePrefix = this.embeddedSourceId ? `[$${this.embeddedSourceId}] ` : ""; + const msg = `\u274C ${sourcePrefix}Unknown KidLisp word: ${expr}`; + let loc = null; + if (this.currentSource && typeof this.currentSource === "string") { + const regex = new RegExp(`\\b${expr}\\b`); + const match = this.currentSource.match(regex); + if (match) { + const offset = this.currentSource.indexOf(match[0]); + if (offset >= 0) { + loc = kidlispOffsetToLineCol(this.currentSource, offset); + } + } + } + if (isKidlispConsoleEnabled()) { + postKidlispConsole("error", msg, { kind: "unknown-word", loc, embeddedSource: this.embeddedSourceId }); + } + console.error(msg); + } + const result = value !== void 0 ? value : expr; + return result; + } + if (Array.isArray(expr) && expr.length === 3) { + const [op, left, right] = expr; + if (op === "+" || op === "-" || op === "*" || op === "/" || op === "%") { + const leftVal = this.fastEval(left, api, env); + const rightVal = this.fastEval(right, api, env); + if (typeof leftVal === "number" && typeof rightVal === "number") { + switch (op) { + case "+": + return leftVal + rightVal; + case "-": + return leftVal - rightVal; + case "*": + return leftVal * rightVal; + case "/": + return rightVal !== 0 ? leftVal / rightVal : 0; + // Prevent division by zero + case "%": + return rightVal !== 0 ? leftVal % rightVal : 0; + } + } + } + } + if (Array.isArray(expr) && expr.length === 3 && expr[0] === "mod") { + const [, innerExpr, modulus] = expr; + if (Array.isArray(innerExpr) && innerExpr.length === 3 && innerExpr[0] === "+") { + const [, leftTerm, rightTerm] = innerExpr; + if (Array.isArray(leftTerm) && leftTerm.length === 3 && leftTerm[0] === "*" && Array.isArray(rightTerm) && rightTerm.length === 3 && rightTerm[0] === "*") { + const leftResult = this.fastEval(leftTerm[1], api, env) * this.fastEval(leftTerm[2], api, env); + const rightResult = this.fastEval(rightTerm[1], api, env) * this.fastEval(rightTerm[2], api, env); + const sum = leftResult + rightResult; + const modValue2 = this.fastEval(modulus, api, env); + if (typeof sum === "number" && typeof modValue2 === "number" && modValue2 !== 0) { + return sum % modValue2; + } + } + } + const innerValue = this.fastEval(innerExpr, api, env); + const modValue = this.fastEval(modulus, api, env); + if (typeof innerValue === "number" && typeof modValue === "number" && modValue !== 0) { + return innerValue % modValue; + } + } + if (Array.isArray(expr) && expr.length === 3) { + const [op, left, right] = expr; + if ((op === "+" || op === "-") && Array.isArray(left) && left.length === 3) { + const [innerOp, innerLeft, innerRight] = left; + if (innerOp === "/" && typeof innerLeft === "string" && (innerLeft === "width" || innerLeft === "height") && typeof innerRight === "number" && innerRight === 2) { + const dimension = this.fastEval(innerLeft, api, env); + const offset = this.fastEval(right, api, env); + if (typeof dimension === "number" && typeof offset === "number") { + const halfDimension = dimension / 2; + return op === "+" ? halfDimension + offset : halfDimension - offset; + } + } + } + } + return this.evaluate(expr, api, env); + } + // Optimized function resolution + resolveFunction(head, api, env) { + if (!this.globalFunctionCache) { + this.globalFunctionCache = /* @__PURE__ */ new Map(); + const globalEnv = this.getGlobalEnv(); + const commonFunctions = ["ink", "box", "line", "circle", "write", "repeat", "choose", "blur", "shape", "tri", "wipe"]; + for (const funcName of commonFunctions) { + if (globalEnv[funcName]) { + this.globalFunctionCache.set(funcName, { type: "global", value: globalEnv[funcName] }); + } + } + } + if (this.globalFunctionCache.has(head)) { + return this.globalFunctionCache.get(head); + } + const cacheKey = `${head}_${this.localEnvLevel}`; + if (this.functionCache.has(cacheKey)) { + return this.functionCache.get(cacheKey); + } + let result = null; + if (typeof head === "string" && head.startsWith("$") && head.length > 1) { + const cacheId = head.slice(1); + if (/^[0-9A-Za-z]{3,12}$/.test(cacheId)) { + const cacheFunction = (api2, args = []) => { + const globalEnv = this.getGlobalEnv(); + const embedFunc = globalEnv.embed; + if (embedFunc) { + const embedArgs = [cacheId, ...args]; + return embedFunc(api2, embedArgs); + } else { + console.warn("\u274C embed function not found in global environment"); + return void 0; + } + }; + result = { + type: "cache", + value: cacheFunction + }; + } + } + if (!result) { + if (head === "blur" && existing(this.getGlobalEnv()[head])) { + result = { type: "global", value: this.getGlobalEnv()[head] }; + } else if (this.isNestedInstance && api && typeof api[head] === "function" && (head === "scroll" || head === "flip" || head === "spin" || head === "zoom" || head === "suck" || head === "contrast" || head === "shear" || head === "brightness" || head === "line")) { + result = { type: "api", value: api[head] }; + } else if (existing(this.localEnv[head])) { + result = { type: "local", value: this.localEnv[head] }; + } else if (existing(env?.[head])) { + result = { type: "env", value: env[head] }; + } else if (existing(this.getGlobalEnv()[head])) { + result = { type: "global", value: this.getGlobalEnv()[head] }; + } else if (existing(this.globalDef[head])) { + result = { type: "globalDef", value: this.globalDef[head] }; + } else if (existing(api[head]) && typeof api[head] === "function") { + result = { type: "api", value: api[head] }; + } + } + if (result && result.type !== "local" && result.type !== "cache") { + this.functionCache.set(cacheKey, result); + } + return result; + } + evaluate(parsed, api = {}, env, inArgs, isTopLevel = false, expressionIndex = 0) { + if (parsed === void 0 || parsed === null || parsed === "" || Array.isArray(parsed) && parsed.length === 0) { + if (api.wipe && api.ink && api.box) { + api.wipe(240, 240, 240); + api.ink(60, 60, 60); + const size = 32; + const cols = Math.ceil((api.screen?.width || 256) / size); + const rows = Math.ceil((api.screen?.height || 256) / size); + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + if ((x + y) % 2 === 0) { + api.box(x * size, y * size, size, size); + } + } + } + } + return void 0; + } + if (Array.isArray(parsed) && parsed.length === 1 && Array.isArray(parsed[0]) && parsed[0][0] === "__chaos__") { + return this.evaluateChaos(parsed[0][1], api); + } + const tracing = isKidlispTraceEnabled(); + if (tracing) { + traceEnter(parsed); + } + const prevExpression = this.currentEvaluatingExpression; + if (!this.inkStateSet && kidlispInkLoggingEnabled()) { + this.currentEvaluatingExpression = Array.isArray(parsed) ? `(${parsed[0]} ...)` : String(parsed).substring(0, 30); + } + const evalStart = this.startTiming("evaluate"); + perfStart("evaluate-total"); + if (VERBOSE) console.log("\u2797 Evaluating:", parsed); + const shouldLog = false; + let perfTimer; + if (shouldLog) { + const exprName = Array.isArray(parsed) ? `${parsed[0]}${parsed.length > 1 ? "(...)" : ""}` : String(parsed).substring(0, 20); + perfTimer = performance.now(); + } + if (shouldLog) { + } + if (!this.evalDepth) this.evalDepth = 0; + this.evalDepth++; + const needsCaching = Array.isArray(parsed) && parsed.length > 0; + let cacheKey = null; + if (needsCaching && this.perf.enabled) { + const head = parsed[0]; + if (typeof head === "string" && (head === "sin" || head === "cos" || head === "mod" || head === "+" || head === "-" || head === "*" || head === "/" || head === "min" || head === "max" || head === "abs")) { + const exprString = JSON.stringify(parsed); + const hasTimeVars = exprString.includes("frame") || exprString.includes("amplitude") || exprString.includes("clock") || exprString.includes("speaker") || exprString.includes('"i"') || exprString.includes('"j"') || exprString.includes('"n"') || exprString.includes('"x"') || exprString.includes('"y"') || exprString.includes("random"); + if (!hasTimeVars) { + cacheKey = exprString + "_" + this.frameCount; + if (!this.frameCache) this.frameCache = /* @__PURE__ */ new Map(); + if (this.frameCache.has(cacheKey)) { + const cachedResult = this.frameCache.get(cacheKey); + if (tracing) traceExit(parsed, cachedResult); + this.evalDepth--; + this.endTiming("evaluate", evalStart); + perfEnd("evaluate-total"); + return cachedResult; + } + } + } + } + if (api.setKidLispContext) { + api.setKidLispContext(this, api, env); + } + let body; + perfStart("get-global-env"); + const globalEnv = this.getGlobalEnv(); + perfEnd("get-global-env"); + if (api.screen) { + globalEnv.screen = api.screen; + } + if (parsed.body) { + const newLocalEnv = {}; + body = parsed.body; + parsed.params.forEach((param, i2) => { + console.log( + "\u{1F609} Binding param:", + param, + "to value:", + inArgs?.[i2], + "at index:", + i2 + ); + if (i2 < inArgs.length) { + newLocalEnv[param] = inArgs[i2]; + } else { + console.warn( + `Parameter ${param} at index ${i2} has no corresponding argument` + ); + newLocalEnv[param] = void 0; + } + }); + this.localEnvLevel += 1; + if (!this.localEnvStore[this.localEnvLevel]) { + this.localEnvStore[this.localEnvLevel] = {}; + } + this.localEnvStore[this.localEnvLevel] = { + ...this.localEnv, + ...newLocalEnv + }; + this.localEnv = this.localEnvStore[this.localEnvLevel]; + console.log( + "\u{1F7E0} Local env level:", + this.localEnvLevel, + "Environment:", + this.localEnv + ); + if (VERBOSE) + console.log("Running:", body, "with environment:", this.localEnv); + } else { + if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { + body = [parsed]; + } else { + body = Array.isArray(parsed) ? parsed : [parsed]; + } + } + if (VERBOSE) console.log("\u{1F3C3} Body:", body); + if (body.length > 0 && !parsed.body) { + const firstItem = body[0]; + let colorName = null; + if (body.length >= 3 && typeof body[0] === "number" && typeof body[1] === "number" && typeof body[2] === "number") { + const r2 = body[0]; + const g = body[1]; + const b2 = body[2]; + const hasAlpha = body.length >= 4 && typeof body[3] === "number"; + const a2 = hasAlpha ? body[3] : 255; + if (r2 >= 0 && r2 <= 255 && g >= 0 && g <= 255 && b2 >= 0 && b2 <= 255 && a2 >= 0 && a2 <= 255) { + body = body.slice(hasAlpha ? 4 : 3); + if (body.length === 0) { + return void 0; + } + } + } + if (typeof firstItem === "string") { + colorName = firstItem; + } else if (Array.isArray(firstItem) && firstItem.length === 1 && typeof firstItem[0] === "string") { + colorName = firstItem[0]; + } + if (colorName) { + const globalEnv2 = this.getGlobalEnv(); + let isValidFirstLineColor = false; + if (isValidRGBString(colorName)) { + const rgbValues = parseRGBString(colorName); + if (rgbValues) { + isValidFirstLineColor = true; + body = body.slice(1); + } + } else if (cssColors2 && cssColors2[colorName]) { + isValidFirstLineColor = true; + } else if (colorName.match(/^c\d+$/)) { + const colorIndex = parseInt(colorName.substring(1)); + if (staticColorMap[colorIndex]) { + isValidFirstLineColor = true; + } + } else if (colorName.match(/^p\d+$/)) { + const patternIndex = parseInt(colorName.substring(1)); + if (patternIndex === 0 || patternIndex === 1) { + isValidFirstLineColor = true; + } + } else if (colorName.startsWith("fade:")) { + const fadeColors2 = this.parseFadeString(colorName); + if (fadeColors2 && fadeColors2.length >= 2) { + isValidFirstLineColor = true; + } + } + if (isValidFirstLineColor) { + body = body.slice(1); + } + } + } + const evaluateTimingArg = (arg, api2, env2) => { + if (typeof arg === "number" || typeof arg === "boolean") { + return arg; + } else if (typeof arg === "string" && /^".*"$/.test(arg)) { + return arg.slice(1, -1); + } else if (Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg)) { + return this.fastEval(arg, api2, env2); + } else { + return arg; + } + }; + let result; + if (this.pendingTimingTriggers && this.pendingTimingTriggers.size > 0) { + const toTrigger = []; + const toKeep = /* @__PURE__ */ new Map(); + for (const [timingKey, pending] of this.pendingTimingTriggers) { + pending.frameDelay--; + if (pending.frameDelay <= 0) { + toTrigger.push(pending); + } else { + toKeep.set(timingKey, pending); + } + } + this.pendingTimingTriggers = toKeep; + for (const pending of toTrigger) { + this.markTimingTriggered(pending.head); + } + } + if (this.perf.enabled && body.length > 0) { + if (!this.perf.bodyProcessCount) this.perf.bodyProcessCount = 0; + this.perf.bodyProcessCount++; + if (this.perf.bodyProcessCount % 500 === 0) { + console.warn(`\u{1F525} BODY PROCESSING ${this.perf.bodyProcessCount} TIMES! Body length: ${body.length}`); + } + if (this.perf.bodyProcessCount > 5e3) { + console.error(`\u{1F6A8} PATHOLOGICAL BODY PROCESSING: ${this.perf.bodyProcessCount} calls! Likely infinite recursion.`); + this.perf.bodyProcessCount = 0; + this.perf.evalCallCount = 0; + this.evalDepth = 0; + return 0; + } + } + for (let bodyIndex = 0; bodyIndex < body.length; bodyIndex++) { + const item = body[bodyIndex]; + if (item && typeof item === "object" && item.optimized) { + perfStart("optimized-execution"); + const optFuncStart = performance.now(); + result = item.func(api); + const optFuncEnd = performance.now(); + this.trackFunction(`opt:${item.name || "anonymous"}`, optFuncEnd - optFuncStart); + perfEnd("optimized-execution"); + continue; + } + if (Array.isArray(item)) { + let [head, ...args] = item; + if (args.length >= 2) { + const reglued = []; + for (let i2 = 0; i2 < args.length; i2++) { + const a2 = args[i2]; + const next = args[i2 + 1]; + if (typeof a2 === "string" && a2.startsWith("fade:") && a2.endsWith(":") && next !== void 0) { + let angleValue; + try { + if (Array.isArray(next)) { + angleValue = this.fastEval(next, api, env); + } else if (typeof next === "number") { + angleValue = next; + } else if (typeof next === "string") { + const num = parseFloat(next); + if (!isNaN(num) && /^-?\d+(?:\.\d+)?$/.test(next)) { + angleValue = num; + } else { + const expanded = this.expandFastMathMacros(next); + angleValue = Array.isArray(expanded) ? this.fastEval(expanded, api, env) : this.evaluate(next, api, env); + } + } else { + angleValue = next; + } + } catch (e2) { + console.warn("Failed to evaluate fade angle expression:", next, e2); + angleValue = 0; + } + reglued.push(a2 + String(angleValue)); + i2++; + continue; + } + reglued.push(a2); + } + args = reglued; + } + args = args.map((arg) => { + if (typeof arg === "string" && arg.startsWith("fade:")) { + const fadeParts = arg.split(":"); + if (fadeParts.length >= 3) { + const fadePrefix = fadeParts[0]; + const colors = fadeParts[1]; + const direction = fadeParts[2]; + let evaluatedDirection = direction; + try { + const numericValue = parseFloat(direction); + if (!isNaN(numericValue)) { + evaluatedDirection = direction; + } else { + let evalResult; + if (direction === "frame") { + evalResult = this.frameCount; + } else { + const expanded = this.expandFastMathMacros(direction); + if (Array.isArray(expanded)) { + evalResult = this.fastEval(expanded, api, env); + } else { + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2[direction] && typeof globalEnv2[direction] === "function") { + evalResult = globalEnv2[direction](api, []); + } else { + evalResult = this.evaluate(direction, api, env); + } + } + } + evaluatedDirection = String(evalResult); + } + } catch (error) { + console.warn("Failed to evaluate fade direction:", direction, error); + evaluatedDirection = direction; + } + return `${fadePrefix}:${colors}:${evaluatedDirection}`; + } + } + return arg; + }); + if (typeof head === "number" && Number.isInteger(head)) { + const frameDivisor = head + 1; + if (this.frameCount % frameDivisor === 0) { + this.markTimingTriggered(head.toString()); + let timingResult; + for (const arg of args) { + timingResult = evaluateTimingArg(arg, api, env); + } + result = timingResult; + } + continue; + } else if (typeof head === "string" && /^\d*\.?\d+[s]!?$/.test(head)) { + const hasInstantTrigger = head.endsWith("!"); + const timeString = hasInstantTrigger ? head.slice(0, -1) : head; + const seconds = parseFloat(timeString.slice(0, -1)); + if (seconds === 0) { + this.markTimingTriggered(head); + this.markDelayTimerActive(head); + let timingResult; + for (const arg of args) { + timingResult = evaluateTimingArg(arg, api, env); + } + result = timingResult; + } else if (seconds < 0.016) { + const minInterval = 0.016; + const clockResult = api.clock.time(); + if (!clockResult) continue; + const currentTimeMs = clockResult.getTime ? clockResult.getTime() : Date.now(); + const currentTime = currentTimeMs / 1e3; + const timingKey = head + "_" + args.length; + if (!this.lastSecondExecutions.hasOwnProperty(timingKey)) { + this.lastSecondExecutions[timingKey] = currentTime; + } else { + const lastExecution = this.lastSecondExecutions[timingKey]; + const diff = currentTime - lastExecution; + if (diff >= minInterval) { + this.lastSecondExecutions[timingKey] = currentTime; + this.markTimingTriggered(head); + this.markDelayTimerActive(head); + let timingResult; + for (const arg of args) { + timingResult = evaluateTimingArg(arg, api, env); + } + result = timingResult; + } + } + } else { + const clockResult = api.clock.time(); + if (!clockResult) continue; + const currentTimeMs = clockResult.getTime ? clockResult.getTime() : Date.now(); + const currentTime = currentTimeMs / 1e3; + const timingKey = head + "_" + args.length; + if (!this.lastSecondExecutions.hasOwnProperty(timingKey)) { + this.lastSecondExecutions[timingKey] = currentTime; + if (bodyIndex === 0) { + this.markTimingTriggered(head); + this.markDelayTimerActive(head); + const wasInEmbedPhase = this.inEmbedPhase; + this.inEmbedPhase = true; + let timingResult; + for (const arg of args) { + timingResult = this.fastEval(arg, api, env); + } + this.inEmbedPhase = wasInEmbedPhase; + result = timingResult; + continue; + } + } + { + const lastExecution = this.lastSecondExecutions[timingKey]; + const diff = currentTime - lastExecution; + const tolerance = 5e-3; + const adjustedSeconds = Math.max(seconds, tolerance); + const crossedBoundary = Math.floor(currentTime / adjustedSeconds) !== Math.floor(lastExecution / adjustedSeconds); + if (crossedBoundary) { + this.lastSecondExecutions[timingKey] = currentTime; + this.markTimingTriggered(head); + this.markDelayTimerActive(head); + const wasInEmbedPhase = this.inEmbedPhase; + this.inEmbedPhase = true; + let timingResult; + for (const arg of args) { + timingResult = this.fastEval(arg, api, env); + } + this.inEmbedPhase = wasInEmbedPhase; + result = timingResult; + } + } + } + continue; + } else if (typeof head === "string" && /^\d*\.?\d+[s]\.\.\.?$/.test(head)) { + this._syncClock(api); + const _meta = this.parseTimingTokenMeta(head); + const seconds = _meta ? _meta.intervalMs / 1e3 : parseFloat(head); + const currentTimeMs = this.getSimulationTime ? this.getSimulationTime() : this._now(); + const currentTime = currentTimeMs / 1e3; + const timingKey = head + "_" + args.length; + if (!this.lastSecondExecutions.hasOwnProperty(timingKey)) { + this.lastSecondExecutions[timingKey] = currentTime; + if (!this.sequenceCounters) { + this.sequenceCounters = /* @__PURE__ */ new Map(); + } + this.sequenceCounters.set(timingKey, 0); + if (bodyIndex === 0) { + if (!this.pendingTimingTriggers) { + this.pendingTimingTriggers = /* @__PURE__ */ new Map(); + } + this.pendingTimingTriggers.set(timingKey, { + frameDelay: 2, + // Wait 2 frames before triggering + head + }); + } + } + const lastExecution = this.lastSecondExecutions[timingKey]; + const diff = currentTime - lastExecution; + const intervalMs = seconds * 1e3; + const absIndex = intervalMs > 0 && args.length > 0 ? (Math.floor(currentTimeMs / intervalMs) % args.length + args.length) % args.length : 0; + if (this.sequenceCounters.get(timingKey) !== absIndex) { + this.markTimingTriggered(head); + this.sequenceCounters.set(timingKey, absIndex); + this.lastSecondExecutions[timingKey] = currentTime; + } + if (args.length > 0) { + const currentIndex = absIndex; + if (head === "0.1s" && args.some( + (arg) => Array.isArray(arg) && arg[0] === "zoom" || typeof arg === "string" && arg.includes("zoom") + )) { + console.log(`\u23F0 DEBUG TIMER [${head}]: executing arg ${currentIndex} of ${args.length}`, { + timingKey, + currentIndex, + selectedArg: args[currentIndex], + allArgs: args, + timeDiff: diff, + secondsRequired: seconds, + shouldAdvance: diff >= seconds + }); + } + this.activeTimingExpressions.set(timingKey, { + currentIndex, + totalArgs: args.length, + timingToken: head, + args + }); + for (let i2 = 0; i2 < args.length; i2++) { + const color3 = i2 === currentIndex ? "olive" : "255,255,255,0"; + this.signalSyntaxHighlight(args[i2], color3); + } + this.currentTimingContext = { + timingKey, + currentIndex, + totalArgs: args.length, + args, + selectedArg: args[currentIndex] + // Add the currently selected argument for easy comparison + }; + const selectedArg = args[currentIndex]; + let result2; + if (typeof selectedArg === "string") { + if (selectedArg.startsWith('"') && selectedArg.endsWith('"')) { + result2 = selectedArg.slice(1, -1); + } else if (selectedArg.startsWith("fade:")) { + const fadeParts = selectedArg.split(":"); + if (fadeParts.length >= 3) { + const fadePrefix = fadeParts[0]; + const colors = fadeParts[1]; + const direction = fadeParts[2]; + let evaluatedDirection = direction; + try { + const numericValue = parseFloat(direction); + if (!isNaN(numericValue)) { + evaluatedDirection = direction; + } else { + let evalResult; + const expanded = this.expandFastMathMacros(direction); + if (Array.isArray(expanded)) { + evalResult = this.fastEval(expanded, api, env); + } else { + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2[direction] && typeof globalEnv2[direction] === "function") { + evalResult = globalEnv2[direction](api, []); + } else { + evalResult = this.evaluate(direction, api, env); + } + } + evaluatedDirection = String(evalResult); + } + } catch (error) { + console.warn("Failed to evaluate fade direction:", direction, error); + evaluatedDirection = direction; + } + const evaluatedFadeString = `${fadePrefix}:${colors}:${evaluatedDirection}`; + if (api.ink && typeof api.ink === "function") { + api.ink(evaluatedFadeString); + result2 = evaluatedFadeString; + } else { + result2 = evaluatedFadeString; + } + } else { + if (api.ink && typeof api.ink === "function") { + api.ink(selectedArg); + result2 = selectedArg; + } else { + result2 = selectedArg; + } + } + } else { + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2[selectedArg] && typeof globalEnv2[selectedArg] === "function") { + result2 = globalEnv2[selectedArg](api, []); + } else { + result2 = this.evaluate(selectedArg, api, env); + } + } + } else if (typeof selectedArg === "number") { + result2 = selectedArg; + } else { + result2 = this.evaluate(selectedArg, api, env); + if (result2 === void 0 || result2 === null) { + result2 = selectedArg; + } + } + this.currentTimingContext = null; + if (body.length === 1) { + return result2; + } + } + continue; + } + if (!existing(head)) { + return this.evalNotFound(head); + } + if (Array.isArray(head)) { + const evaledHead = this.evaluate([head], api, env); + const newEval = [evaledHead, ...args]; + result = this.evaluate([newEval], api, env); + continue; + } + if (typeof head !== "string" && head?.iterable) { + result = this.iterate(head, api, args, env); + continue; + } + if (typeof head !== "string") { + head = String(head); + } + let splitHead = []; + let colon = null; + if (head === "..." || head === "..") { + colon = head.includes(":") ? head.split(":")[1] : null; + head = head.split(":")[0]; + } else { + splitHead = head.split("."); + head = splitHead[0]; + const colonSplit = head.split(":"); + head = colonSplit[0]; + colon = colonSplit[1]; + } + if (head === "now" || head === "def" || head === "die") + args[0] = `"${args[0]}"`; + if (typeof head === "string" && head.startsWith("#") && /^#[0-9A-Za-z]{1,8}$/.test(head)) { + args = [head, ...args]; + head = "stamp"; + } + const resolved = this.resolveFunction(head, api, env); + if (head === "zoom") { + } + if (head === "-1" || head === "1") { + console.log(`\u{1F6A8} CRITICAL: Trying to resolve number "${head}" as function!`); + console.trace("Stack trace for number resolution:"); + } + if (resolved) { + const { type, value } = resolved; + if (head === "zoom") { + } + if (head === "scroll") { + } + switch (type) { + case "local": + perfStart(`local-${head}`); + if (VERBOSE) + console.log("\u{1F62B} Local definition found!", head, value); + if (value.iterable) { + result = this.iterate(value, api, args, env); + } else { + result = value; + } + perfEnd(`local-${head}`); + break; + case "env": + perfStart(`env-${head}`); + if (value.iterable) { + result = this.iterate(value, api, args, env); + } else { + result = value; + } + perfEnd(`env-${head}`); + break; + case "global": + if (typeof value === "function" || typeof value === "object") { + let processedArgs; + if ( + // The voices: their first argument is a NOTE and must arrive + // unevaluated, because `c4` is also a color. They evaluate their + // own numeric args (see numArg above). + head === "pluck" || head === "bell" || head === "sub" || head === "flute" || head === "hat" || head === "voice" || head === "later" || head === "tap" || head === "draw" || head === "lift" || head === "if" || head === "not" || head === ">" || head === "<" || head === "=" || head === "net" || head === "source" || head === "choose" || head === "?" || head === "repeat" || head === "once" || head === "hop" || head === "delay" || head === "trans" || head === "jump" + ) { + processedArgs = args; + } else { + processedArgs = args.map((arg, index) => { + if (arg === "?") { + return this.contextAwareRandom(head, index, api); + } + if (Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg)) { + if (Array.isArray(arg) && arg.length > 0 && typeof arg[0] === "string" && /^\d*\.?\d+[s]\.\.\.?$/.test(arg[0])) { + const result2 = this.evaluate([arg], api, this.localEnv); + return result2; + } else { + const result2 = this.fastEval(arg, api, this.localEnv); + return result2; + } + } else { + return arg; + } + }); + } + if (splitHead[1]) { + result = getNestedValue(value, item[0])(api, processedArgs); + } else { + if (head === "zoom") { + } + if (head === "suck") { + } + const funcStart = performance.now(); + result = value(api, processedArgs, env, colon); + const funcEnd = performance.now(); + this.trackFunction(head, funcEnd - funcStart); + if (result?.iterable) { + result = this.iterate(result, api, args, env); + continue; + } + } + } else { + result = value; + } + break; + case "globalDef": + const evaluatedArgs = args.map( + (arg) => Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg) ? this.fastEval(arg, api, this.localEnv) : arg + ); + const userFuncStart = performance.now(); + result = Array.isArray(value) || value.body ? this.evaluate(value, api, this.localEnv, evaluatedArgs) : value; + const userFuncEnd = performance.now(); + this.trackFunction(`user:${head}`, userFuncEnd - userFuncStart); + break; + case "api": + const apiArgs = args.map( + (arg) => Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg) ? this.fastEval(arg, api, this.localEnv) : arg + ); + if (head === "zoom") { + } + if (head === "scroll") { + } + const apiFuncStart = performance.now(); + result = value(...apiArgs); + const apiFuncEnd = performance.now(); + this.trackFunction(`api:${head}`, apiFuncEnd - apiFuncStart); + break; + case "cache": + const cacheArgs = args.map( + (arg) => Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg) ? this.fastEval(arg, api, this.localEnv) : arg + ); + result = value(api, cacheArgs); + break; + } + } else { + if (typeof head === "string" && validIdentifierRegex.test(head) && head !== "fps" && !this.unknownWordsLogged?.has(head)) { + this.unknownWordsLogged?.add(head); + const sourcePrefix = this.embeddedSourceId ? `[$${this.embeddedSourceId}] ` : ""; + const msg = `\u274C ${sourcePrefix}Unknown KidLisp word: ${head}`; + let loc = null; + if (this.currentSource && typeof this.currentSource === "string") { + const regex = new RegExp(`\\b${head}\\b`); + const match = this.currentSource.match(regex); + if (match) { + const offset = this.currentSource.indexOf(match[0]); + if (offset >= 0) { + loc = kidlispOffsetToLineCol(this.currentSource, offset); + } + } + } + if (isKidlispConsoleEnabled()) { + postKidlispConsole("error", msg, { kind: "unknown-word", loc, embeddedSource: this.embeddedSourceId }); + } + console.error(msg); + } + if (Array.isArray(head)) { + if (VERBOSE) console.log("Environment:", this.localEnv); + result = this.evaluate(head, api, this.localEnv); + } else { + result = this.evalNotFound(head, api, this.localEnv); + } + } + } else { + let root, tail; + if (typeof item === "string") { + [root, tail] = item.split("."); + } + if (!Array.isArray(env) && existing(env?.[root])) { + result = getNestedValue(env, item); + } else { + result = this.fastEval(item, api, env); + if (result === item) { + if (Array.isArray(item)) { + result = this.evaluate(item, api, env); + } else if (typeof item === "string" && item.includes(":")) { + const colonSplit = item.split(":"); + const head = colonSplit[0]; + const colon = colonSplit[1]; + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2[head] && typeof globalEnv2[head] === "function") { + result = globalEnv2[head](api, [], env, colon); + } else { + result = this.evalNotFound(item, api, env); + } + } else { + result = this.evalNotFound(item, api, env); + } + } + } + } + } + if (parsed.body) { + this.localEnvLevel -= 1; + this.localEnv = this.localEnvStore[this.localEnvLevel] || {}; + console.log( + "\u{1F519} Restored env level:", + this.localEnvLevel, + "Environment:", + this.localEnv + ); + } + if (api.clearKidLispContext) { + api.clearKidLispContext(); + } + this.currentTimingContext = null; + if (cacheKey && this.frameCache && result !== void 0) { + this.frameCache.set(cacheKey, result); + } + if (tracing) traceExit(parsed, result); + this.evalDepth--; + const evalTime = performance.now() - evalStart.time; + if (shouldLog && evalTime > 1) { + } + if (shouldLog && perfTimer) { + const totalTime = performance.now() - perfTimer; + if (totalTime > 5) { + const exprName = Array.isArray(parsed) ? `${parsed[0]}${parsed.length > 1 ? "(...)" : ""}` : String(parsed).substring(0, 20); + } + } + perfEnd("evaluate-total"); + this.endTiming("evaluate", evalStart); + return result; + } + evalNotFound(expression, api, env) { + if (typeof expression !== "string") { + return expression; + } else { + if (expression === "fps") { + return "fps"; + } + } + if (typeof expression === "string" && expression.startsWith("#")) { + if (/^#[0-9A-Za-z]{1,8}$/.test(expression)) { + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2 && typeof globalEnv2.stamp === "function") { + try { + return globalEnv2.stamp(api, [expression], env); + } catch (err) { + console.warn("\u26A0\uFE0F auto-stamp failed for", expression, err); + } + } + } + } + if (/^\d*\.?\d+[s]\.\.\.?$/.test(expression)) { + return this.evaluate([expression], api, env); + } + if (validIdentifierRegex.test(expression)) { + const globalEnv2 = this.getGlobalEnv(); + if (globalEnv2[expression] && typeof globalEnv2[expression] === "function") { + return globalEnv2[expression](api, [], env); + } + if (api[expression] && typeof api[expression] === "function") { + return api[expression](); + } + } + const identifiers = expression.match(identifierRegex) || []; + const globalEnv = this.getGlobalEnv(); + identifiers.forEach((id) => { + if (id === "s" && /\d+\.?\d*s/.test(expression)) { + return; + } + let value = this.fastEval(id, api, env); + if (value === id) { + value = 0; + } + expression = expression.replace(new RegExp(`\\b${id}\\b`, "g"), value); + }); + expression = expression.replace(/(\d+\.?\d*)s/g, "$1"); + try { + const compute = new Function(`return ${expression};`); + const result = compute(); + return result; + } catch (error) { + console.warn("\u2757 Failed to evaluate expression:", expression, error.message); + return 0; + } + } + // Loop over an iterable. + iterate(iterable, api, args, outerEnv) { + iterable.data.forEach((item, index) => { + const env = { ...outerEnv }; + env.index = index; + env.item = item; + if (Array.isArray(item)) item = { iterable: true, data: item.slice() }; + env.data = item; + args.forEach((arg) => { + if (typeof arg === "number" || typeof arg === "boolean") { + return arg; + } else if (typeof arg === "string" && /^".*"$/.test(arg)) { + return arg.slice(1, -1); + } else if (Array.isArray(arg) || typeof arg === "string" && !/^".*"$/.test(arg)) { + return this.fastEval(arg, api, env); + } else { + return arg; + } + }); + }); + return iterable; + } + // Syntax highlighting methods for HUD integration + // Initialize syntax highlighting for a kidlisp source + initializeSyntaxHighlighting(source) { + this.syntaxHighlightSource = source; + this.expressionPositions = this.mapExpressionsToPositions(source); + this.executionHistory = []; + this.currentlyHighlighted.clear(); + } + // Map parsed expressions to their positions in the source code + mapExpressionsToPositions(source) { + const positions = []; + let charIndex = 0; + let i2 = 0; + while (i2 < source.length) { + if (source[i2] === "(") { + const exprStart = charIndex + i2; + let depth = 1; + let j = i2 + 1; + while (j < source.length && depth > 0) { + if (source[j] === "(") depth++; + else if (source[j] === ")") depth--; + j++; + } + if (depth === 0) { + const exprEnd = charIndex + j; + const exprText = source.substring(i2, j); + const isNested = positions.some( + (pos) => exprStart > pos.start && exprStart < pos.end + ); + if (!isNested) { + positions.push({ + start: exprStart, + end: exprEnd, + text: exprText, + isExpression: true + }); + } + i2 = j; + } else { + i2++; + } + } else { + i2++; + } + } + return positions; + } + // Mark an expression as currently executing + markExpressionExecuting(expr) { + const now = performance.now(); + this.currentExecutingExpression = expr; + this.lastExecutionTime = now; + this.executionHistory.push({ + expression: expr, + timestamp: now, + type: "execution" + }); + if (this.executionHistory.length > 50) { + this.executionHistory = this.executionHistory.slice(-25); + } + } + // Generate colored syntax highlighting string for HUD using proper tokenization + buildColoredKidlispString() { + if (!this.syntaxHighlightSource) return ""; + try { + const tokens = tokenize(this.syntaxHighlightSource); + if (tokens.length === 0) { + return `\\white\\${this.syntaxHighlightSource}`; + } + let result = ""; + let sourceIndex = 0; + let lastColor = null; + for (let i2 = 0; i2 < tokens.length; i2++) { + let token = tokens[i2]; + const fastMathMatch = token.match(/^(\w+)\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)$/); + if (fastMathMatch) { + const [, left, op, right] = fastMathMatch; + const tokenIndex2 = this.syntaxHighlightSource.indexOf(token, sourceIndex); + if (tokenIndex2 !== -1) { + const whitespace = this.syntaxHighlightSource.substring(sourceIndex, tokenIndex2); + result += whitespace; + const leftColor = this.getTokenColor(left, [left, op, right], 0); + const opColor = this.getTokenColor(op, [left, op, right], 1); + const rightColor = this.getTokenColor(right, [left, op, right], 2); + result += `\\${leftColor}\\${left}`; + result += `\\${opColor}\\${op}`; + result += `\\${rightColor}\\${right}`; + lastColor = rightColor; + sourceIndex = tokenIndex2 + token.length; + } + continue; + } + const color3 = this.getTokenColor(token, tokens, i2); + if (!color3) { + console.warn(`\u26A0\uFE0F getTokenColor returned undefined for token:`, token, `at index:`, i2, "surrounding tokens:", tokens[i2 - 1] || "START", "->", token, "->", tokens[i2 + 1] || "END"); + continue; + } + const tokenIndex = this.syntaxHighlightSource.indexOf(token, sourceIndex); + if (tokenIndex !== -1) { + const whitespace = this.syntaxHighlightSource.substring(sourceIndex, tokenIndex); + result += whitespace; + const NOTEISH = /^[A-Za-z0-9#.,'^+\-{}\[\]>_~|]+$/; + const isQuoted = token.length >= 2 && (token[0] === '"' || token[0] === "'") && token[token.length - 1] === token[0]; + const looksLikeNotes = /[a-gA-G]/.test(token) && NOTEISH.test(token) && !/^-?\d*\.?\d+$/.test(token); + const melodyHeadBefore = () => { + let j = i2 - 1; + while (j >= 0) { + const t2 = tokens[j]; + if (t2 === "melody" || t2 === "clock") return true; + if (/[a-gA-G]/.test(t2) && NOTEISH.test(t2) && !/^-?\d*\.?\d+$/.test(t2)) { + j--; + continue; + } + return false; + } + return false; + }; + const isMelodyArg = (isQuoted || looksLikeNotes) && melodyHeadBefore(); + if (isMelodyArg) { + const inner = isQuoted ? token.slice(1, -1) : token; + const quoteColor = color3 || "yellow"; + const state2 = this.melodyByString?.get(inner) || null; + const coloredInner = buildColoredMelodyString(inner, state2, { + now: this._now(), + timingHasStarted: !!(state2 && state2.isPlaying), + recentlyMutated: this.melodyRecentlyMutated || { noteIndex: -1, trackIndex: -1 }, + mutationFlash: { active: false }, + specialCharFlash: false, + triggeredAsteriskPositions: [], + getStampleStatus: () => null + }); + if (isQuoted) { + const q = token[0]; + result += `\\${quoteColor}\\${q}${coloredInner}\\${quoteColor}\\${q}`; + } else { + result += coloredInner; + } + lastColor = null; + sourceIndex = tokenIndex + token.length; + continue; + } + if (color3.startsWith("COMPOUND:")) { + const parts = color3.split(":"); + const prefixColor = parts[1]; + const identifierColor = parts[2]; + const prefixChar = token.charAt(0); + result += `\\${prefixColor}\\${prefixChar}`; + result += `\\${identifierColor}\\${token.substring(1)}`; + lastColor = identifierColor; + } else if (color3 === "RAINBOW" && token === "rainbow") { + const rainbowColors2 = ["red", "orange", "yellow", "lime", "blue", "purple", "magenta"]; + for (let charIndex = 0; charIndex < token.length; charIndex++) { + const charColor = rainbowColors2[charIndex % rainbowColors2.length]; + result += `\\${charColor}\\${token[charIndex]}`; + } + lastColor = null; + } else if (color3 === "ZEBRA" && token === "zebra") { + const zebraColors2 = ["black", "white"]; + for (let charIndex = 0; charIndex < token.length; charIndex++) { + const charColor = zebraColors2[charIndex % zebraColors2.length]; + result += `\\${charColor}\\${token[charIndex]}`; + } + lastColor = null; + } else if (token.startsWith("fade:") && color3 === "mediumseagreen") { + const fadeResult = this.colorFadeExpression(token); + result += fadeResult; + lastColor = null; + } else { + if (color3 !== lastColor) { + result += `\\${color3}\\`; + lastColor = color3; + } + result += token; + } + sourceIndex = tokenIndex + token.length; + } + } + if (sourceIndex < this.syntaxHighlightSource.length) { + result += this.syntaxHighlightSource.substring(sourceIndex); + } + return result; + } catch (error) { + console.warn("Error building colored kidlisp string:", error); + return `\\white\\${this.syntaxHighlightSource}`; + } + } + // Check if a token is part of a timing expression and get its state + getTimingTokenState(token, tokens, index) { + if (/^\d*\.?\d+[sf]\.\.\.?$/.test(token)) { + for (const [timingKey, data] of this.activeTimingExpressions) { + if (timingKey.startsWith(token + "_")) { + return { + timingKey, + currentIndex: data.currentIndex, + totalArgs: data.totalArgs, + isBlinking: this.isTimingBlinking(token), + isInActiveArg: true + // The timing token itself is always "active" + }; + } + } + for (const [timingKey, currentIndex] of this.sequenceCounters) { + if (timingKey.startsWith(token + "_")) { + const argCount = parseInt(timingKey.split("_")[1]) || 0; + return { + timingKey, + currentIndex, + totalArgs: argCount, + isBlinking: this.isTimingBlinking(token), + isInActiveArg: true + }; + } + } + } else if (/^\d*\.?\d+[sf]!?$/.test(token)) { + return { + timingKey: token, + currentIndex: 0, + totalArgs: 1, + isBlinking: this.isTimingBlinking(token), + isActive: this.isDelayTimerActive(token), + // Track active display period + isInActiveArg: true, + isDelayTimer: true + // Mark as delay timer for special handling + }; + } + for (let i2 = 0; i2 < index; i2++) { + const prevToken = tokens[i2]; + if (/^\d*\.?\d+[sf]\.\.\.?$/.test(prevToken)) { + for (const [timingKey, data] of this.activeTimingExpressions) { + if (timingKey.startsWith(prevToken + "_")) { + const argInfo = this.getTokenArgPosition(tokens, i2, index); + if (argInfo && argInfo.argIndex < data.totalArgs) { + const isActive = argInfo.argIndex === data.currentIndex; + return { + timingKey, + currentIndex: data.currentIndex, + totalArgs: data.totalArgs, + isBlinking: this.isTimingBlinking(prevToken), + isInActiveArg: isActive + }; + } + } + } + for (const [timingKey, currentIndex] of this.sequenceCounters) { + if (timingKey.startsWith(prevToken + "_")) { + const argCount = parseInt(timingKey.split("_")[1]) || 0; + const argInfo = this.getTokenArgPosition(tokens, i2, index); + if (argInfo && argInfo.argIndex < argCount) { + return { + timingKey, + currentIndex, + totalArgs: argCount, + isBlinking: this.isTimingBlinking(prevToken), + isInActiveArg: argInfo.argIndex === currentIndex + }; + } + } + } + } else if (/^\d*\.?\d+[sf]!?$/.test(prevToken)) { + const isBlinking = this.isTimingBlinking(prevToken); + const isActive = this.isDelayTimerActive(prevToken); + const argInfo = this.getTokenArgPosition(tokens, i2, index); + if (argInfo !== null) { + return { + timingKey: prevToken, + currentIndex: 0, + totalArgs: 1, + isBlinking, + isActive, + isInActiveArg: true, + // For delay timers, all arguments are controlled by timer state + isDelayTimer: true + }; + } + } + } + return null; + } + // Get which argument position a token is in relative to a timing token + getTokenArgPosition(tokens, timingIndex, tokenIndex) { + let argIndex = -1; + let parenDepth = 0; + let currentTokenPos = timingIndex + 1; + while (currentTokenPos < tokens.length && currentTokenPos <= tokenIndex) { + const token = tokens[currentTokenPos]; + if (token === "(") { + parenDepth++; + if (parenDepth === 1) { + argIndex++; + } + } else if (token === ")") { + parenDepth--; + if (parenDepth < 0) { + break; + } + } else if (parenDepth === 0) { + argIndex++; + } + if (currentTokenPos === tokenIndex) { + return { argIndex, tokenDepth: parenDepth }; + } + currentTokenPos++; + } + return null; + } + // Get the arguments count for a timing expression (for key generation) + getTimingArgsCount(tokens, timingIndex) { + let argCount = 0; + let parenDepth = 0; + let currentTokenPos = timingIndex + 1; + while (currentTokenPos < tokens.length) { + const token = tokens[currentTokenPos]; + if (token === "(") { + parenDepth++; + if (parenDepth === 1) { + argCount++; + } + } else if (token === ")") { + parenDepth--; + if (parenDepth < 0) { + break; + } + } else if (parenDepth === 0 && token !== timingIndex) { + argCount++; + } + currentTokenPos++; + } + return argCount; + } + // Get the arguments for a timing expression + getTimingArgs(tokens, timingIndex) { + const args = []; + let parenDepth = 0; + let currentArg = []; + let foundArgs = false; + for (let i2 = timingIndex + 1; i2 < tokens.length; i2++) { + const token = tokens[i2]; + if (token === "(") { + parenDepth++; + currentArg.push(token); + if (!foundArgs) { + foundArgs = true; + } + } else if (token === ")") { + parenDepth--; + currentArg.push(token); + if (parenDepth === 0) { + if (foundArgs && currentArg.length > 0) { + args.push(currentArg); + currentArg = []; + } + if (foundArgs) break; + } + } else if (parenDepth === 0 && foundArgs) { + break; + } else if (parenDepth > 0) { + currentArg.push(token); + } + } + return args; + } + // Check if a token is inside an active timing expression argument + getTimingExpressionState(token, tokens, index) { + for (const [timingKey, timingData] of this.activeTimingExpressions) { + const tokenPosition = this.findTokenInTimingArgs(token, index, tokens, timingData); + if (tokenPosition.found) { + const isInActiveArg = tokenPosition.argIndex === timingData.currentIndex; + if ((token === "line" || token === "box" || token === "(" || token === ")") && this.frameCount % 30 === 0) { + } + return { + isInActiveArg, + currentArgIndex: tokenPosition.argIndex, + currentIndex: timingData.currentIndex, + timingKey, + timingData + }; + } + } + return null; + } + findTokenInTimingArgs(token, tokenIndex, tokens, timingData) { + const timingExpressions = this.findTimingExpressionsInAST(this.ast, timingData.timingToken); + for (const timingExpr of timingExpressions) { + const argIndex = this.findTokenInTimingExpressionArgs(token, tokenIndex, tokens, timingExpr); + if (argIndex !== -1) { + return { found: true, argIndex }; + } + if (token === "(" || token === ")") { + const parenArgIndex = this.findParenthesesInTimingArgs(tokenIndex, tokens, timingExpr); + if (parenArgIndex !== -1) { + return { found: true, argIndex: parenArgIndex }; + } + } + } + return { found: false }; + } + // Find timing expressions in the AST that match the given timing token + findTimingExpressionsInAST(ast, timingToken) { + const results = []; + if (Array.isArray(ast)) { + if (ast.length > 1 && ast[0] === timingToken) { + results.push(ast); + } + for (const item of ast) { + results.push(...this.findTimingExpressionsInAST(item, timingToken)); + } + } + return results; + } + // Find which timing argument a parenthesis belongs to by analyzing token positions + findParenthesesInTimingArgs(parenIndex, tokens, timingExpr) { + const timingToken = timingExpr[0]; + const timingStartIndex = tokens.indexOf(timingToken); + if (timingStartIndex === -1) return -1; + let parenCount = 0; + let timingEndIndex = -1; + for (let i2 = timingStartIndex - 1; i2 < tokens.length; i2++) { + if (tokens[i2] === "(") { + parenCount++; + if (parenCount === 1 && i2 < timingStartIndex) { + continue; + } + } else if (tokens[i2] === ")") { + parenCount--; + if (parenCount === 0) { + timingEndIndex = i2; + break; + } + } + } + if (timingEndIndex === -1) return -1; + if (parenIndex <= timingStartIndex || parenIndex >= timingEndIndex) { + return -1; + } + const args = timingExpr.slice(1); + let currentArgIndex = 0; + let currentTokenPos = timingStartIndex + 1; + for (let argIndex = 0; argIndex < args.length; argIndex++) { + const arg = args[argIndex]; + const argTokenCount = this.countTokensInExpression(arg); + const argEndPos = currentTokenPos + argTokenCount - 1; + if (parenIndex >= currentTokenPos && parenIndex <= argEndPos) { + return argIndex; + } + currentTokenPos = argEndPos + 1; + } + return -1; + } + // Count how many tokens an AST expression would consume in the token stream + countTokensInExpression(expr) { + if (typeof expr === "string" || typeof expr === "number") { + return 1; + } + if (Array.isArray(expr)) { + let count = 2; + for (const item of expr) { + count += this.countTokensInExpression(item); + } + return count; + } + return 1; + } + // Find which argument of a timing expression contains a specific token + findTokenInTimingExpressionArgs(token, tokenIndex, tokens, timingExpr) { + const args = timingExpr.slice(1); + for (let argIndex = 0; argIndex < args.length; argIndex++) { + if (this.doesExpressionContainToken(args[argIndex], token)) { + if ((token === "line" || token === "box") && this.frameCount % 60 === 0) { + } + return argIndex; + } + } + return -1; + } + // Check if an AST expression contains a specific token + doesExpressionContainToken(expr, token) { + if (typeof expr === "string") { + return expr === token; + } + if (typeof expr === "number") { + return expr.toString() === token; + } + if (Array.isArray(expr)) { + return expr.some((item) => this.doesExpressionContainToken(item, token)); + } + return false; + } + // Determine the color for a specific token based on its type and context + getTokenColor(token, tokens, index) { + if (this.lastValidationErrors && this.lastValidationErrors.length > 0) { + const errors = this.lastValidationErrors.join(" "); + if (errors.includes("closing parenthes")) { + if (token === "(") { + let depth = 1; + let isClosed = false; + for (let i2 = index + 1; i2 < tokens.length; i2++) { + if (tokens[i2] === "(") { + depth++; + } else if (tokens[i2] === ")") { + depth--; + if (depth === 0) { + isClosed = true; + break; + } + } + } + if (!isClosed) { + console.log(`\u{1F534} Highlighting unclosed paren at token index ${index}`); + return "red"; + } + } + } + if (errors.includes("Too many closing")) { + if (token === ")") { + let balance = 0; + for (let i2 = 0; i2 <= index; i2++) { + if (tokens[i2] === "(") balance++; + if (tokens[i2] === ")") balance--; + if (balance < 0 && i2 === index) return "red"; + } + } + } + if (errors.includes("Unmatched") && errors.includes("quote")) { + if (token.startsWith('"') || token.startsWith("'")) { + const quoteChar = token[0]; + let quoteCount = 0; + for (let i2 = 0; i2 <= index; i2++) { + const t2 = tokens[i2]; + if (t2.startsWith(quoteChar)) { + quoteCount++; + } + } + if (quoteCount % 2 !== 0) { + let hasMatchingQuote = false; + for (let i2 = index + 1; i2 < tokens.length; i2++) { + if (tokens[i2].startsWith(quoteChar)) { + hasMatchingQuote = true; + break; + } + } + if (!hasMatchingQuote) return "red"; + } + } + } + } + const timingExprState = this.getTimingExpressionState(token, tokens, index); + if (/^\d*\.?\d+[sf]\.\.\.?$/.test(token)) { + const timingState = this.getTimingTokenState(token, tokens, index); + if (this.isEditMode) { + const blinkState = this.getTimingEditBlinkState(token); + if (blinkState.isBlinking) { + return "lime"; + } + return "yellow"; + } + if (timingState && timingState.isBlinking) { + return "red"; + } + if (this.isTimingBlinking(token)) { + return "lime"; + } + if (timingState && timingState.currentIndex !== void 0) { + return "yellow"; + } + return "yellow"; + } + if (/^\d*\.?\d+[sf]!?$/.test(token)) { + const timingState = this.getTimingTokenState(token, tokens, index); + if (this.isEditMode) { + const blinkState = this.getTimingEditBlinkState(token); + if (blinkState.isBlinking) { + return "red"; + } + return "yellow"; + } + if (timingState && timingState.isBlinking) { + return "red"; + } + if (timingState && timingState.isActive) { + return "cyan"; + } + return "yellow"; + } + if (timingExprState) { + if (token === "(" || token === ")") { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + return normalColor2 || "192,192,192"; + } + if (this.isEditMode) { + const timingToken = timingExprState.timingData.timingToken; + const now = performance.now(); + const totalArgs = timingExprState.timingData.totalArgs; + const tokenArgIndex = timingExprState.currentArgIndex; + const cyclePosition = this.getTimingCyclePosition(timingToken, totalArgs, now); + const isInBlinkPhase = this.getTimingEditBlinkState(timingToken, now).isBlinking; + if (tokenArgIndex === cyclePosition) { + if (isInBlinkPhase) { + return "lime"; + } + } else { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + if (normalColor2) { + const rgb = this.parseColorToRGB(normalColor2); + if (rgb) { + const desaturated = this.desaturateColor(rgb); + if (desaturated) { + return desaturated.join(","); + } + } + } + return "128,128,128"; + } + } else { + if (timingExprState.isInActiveArg) { + const isTimingBlinking = this.isTimingBlinking(timingExprState.timingData.timingToken); + if (isTimingBlinking) { + return "lime"; + } + } else { + return "255,255,255,0"; + } + } + } + if (this.isEditMode && !timingExprState) { + for (let i2 = index - 1; i2 >= 0; i2--) { + const prevToken = tokens[i2]; + if (/^\d*\.?\d+[sf]\.\.\.?$/.test(prevToken)) { + const argInfo = this.getTokenArgPosition(tokens, i2, index); + if (argInfo !== null && argInfo.argIndex >= 0) { + const now = performance.now(); + const totalArgs = this.getTimingArgsCount(tokens, i2); + if (totalArgs === 0) { + break; + } + const cyclePosition = this.getTimingCyclePosition(prevToken, totalArgs, now); + const isInBlinkPhase = this.getTimingEditBlinkState(prevToken, now).isBlinking; + if (token === "(" || token === ")") { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + return normalColor2 || "192,192,192"; + } + if (argInfo.argIndex === cyclePosition) { + if (isInBlinkPhase) { + return "lime"; + } + } else { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + if (normalColor2) { + const rgb = this.parseColorToRGB(normalColor2); + if (rgb) { + const desaturated = this.desaturateColor(rgb); + if (desaturated) { + return desaturated.join(","); + } + } + } + return "128,128,128"; + } + break; + } + } + if (prevToken === "(" && i2 < index - 1) { + if (i2 + 1 < tokens.length && !/^\d*\.?\d+[sf]\.\.\.?$/.test(tokens[i2 + 1])) { + break; + } + } + } + } + if (this.isEditMode && token !== "(" && token !== ")") { + for (let i2 = index - 1; i2 >= 0; i2--) { + const prevToken = tokens[i2]; + if ((prevToken === "?" || prevToken === "choose") && i2 > 0 && tokens[i2 - 1] === "(") { + const argInfo = this.getTokenArgPosition(tokens, i2, index); + if (argInfo !== null && argInfo.argIndex >= 0) { + const totalArgs = this.getTimingArgsCount(tokens, i2); + if (totalArgs > 1) { + const now = performance.now(); + const holdDuration = 1500; + const offset = i2 * 577 % holdDuration; + const cyclePosition = Math.floor((now + offset) / holdDuration) % totalArgs; + const blinkDuration = 150; + const cyclePhase = (now + offset) % holdDuration; + const isInBlinkPhase = cyclePhase < blinkDuration; + if (argInfo.argIndex === cyclePosition) { + if (isInBlinkPhase) { + return "lime"; + } + } else { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + if (normalColor2) { + const rgb = this.parseColorToRGB(normalColor2); + if (rgb) { + const desaturated = this.desaturateColor(rgb); + if (desaturated) { + return desaturated.join(","); + } + } + } + return "128,128,128"; + } + } + } + break; + } + if (prevToken === "(") { + break; + } + } + } + const delayTimerState = this.getTimingTokenState(token, tokens, index); + if (delayTimerState && delayTimerState.isDelayTimer) { + if (token === "(" || token === ")") { + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + return normalColor2 || "192,192,192"; + } + if (this.isEditMode) { + const timingToken = delayTimerState.timingKey; + const now = performance.now(); + if (this.getTimingEditBlinkState(timingToken, now).isBlinking) { + return "red"; + } + const normalColor2 = this.getNormalTokenColor(token, tokens, index); + if (normalColor2) { + const rgb = this.parseColorToRGB(normalColor2); + if (rgb) { + const desaturated = this.desaturateColor(rgb); + if (desaturated) { + return desaturated.join(","); + } + } + } + return "128,128,128"; + } + if (delayTimerState.isBlinking) { + return "red"; + } + if (delayTimerState.isActive) { + } else { + return "255,255,255,0"; + } + } + const normalColor = this.getNormalTokenColor(token, tokens, index); + if (!normalColor) { + console.warn(`\u26A0\uFE0F getNormalTokenColor returned undefined for token:`, token); + return "orange"; + } + return normalColor; + } + // Parse color name to RGB array [r, g, b] + parseColorToRGB(colorName) { + if (colorName.includes(",")) { + const parts = colorName.split(",").map((v2) => parseInt(v2.trim())); + if (parts.length >= 3 && parts.every((v2) => !isNaN(v2))) { + return parts.slice(0, 3); + } + } + const colorMap = { + "white": [255, 255, 255], + "black": [0, 0, 0], + "red": [255, 0, 0], + "green": [0, 255, 0], + "blue": [0, 0, 255], + "yellow": [255, 255, 0], + "cyan": [0, 255, 255], + "magenta": [255, 0, 255], + "orange": [255, 165, 0], + "purple": [128, 0, 128], + "lime": [0, 255, 0], + "pink": [255, 192, 203], + "gray": [128, 128, 128], + "darkgray": [64, 64, 64], + "lightgray": [192, 192, 192], + "darkred": [139, 0, 0], + "darkgreen": [0, 100, 0], + "darkblue": [0, 0, 139] + }; + return colorMap[colorName.toLowerCase()] || null; + } + // Desaturate a color (convert to grayscale while preserving brightness) + desaturateColor(rgb, amount = 0.75) { + if (!rgb || rgb.length < 3) return null; + const gray = Math.floor(0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]); + const r2 = Math.floor(rgb[0] * (1 - amount) + gray * amount); + const g = Math.floor(rgb[1] * (1 - amount) + gray * amount); + const b2 = Math.floor(rgb[2] * (1 - amount) + gray * amount); + return [r2, g, b2]; + } + // Get the normal color for a token (the original getTokenColor logic) + getNormalTokenColor(token, tokens, index) { + if (token.startsWith(";")) { + return "gray"; + } + if (token.startsWith('"') && token.endsWith('"')) { + return "yellow"; + } + if (token.startsWith("$") && token.length > 1 && /^[$][0-9A-Za-z]+$/.test(token)) { + const cacheId = token.substring(1); + if (this.loadingEmbeddedLayers.has(cacheId)) { + const now = performance.now(); + const blinkInterval = 200; + const isBlinking = Math.floor(now / blinkInterval) % 2 === 0; + const mainColor = isBlinking ? "red" : "darkred"; + return `COMPOUND:${mainColor}:${mainColor}`; + } else if (this.loadedEmbeddedLayers.has(cacheId)) { + const now = performance.now(); + const magicBlinkInterval = 150; + const frame = Math.floor(now / magicBlinkInterval) % 3; + const dollarColor = frame === 0 ? "yellow" : frame === 1 ? "hotpink" : "limegreen"; + return `COMPOUND:${dollarColor}:lime`; + } else { + return "COMPOUND:limegreen:lime"; + } + } + if (token.startsWith("#") && token.length > 1 && /^[#][0-9A-Za-z]{1,8}$/.test(token)) { + return `COMPOUND:magenta:orange`; + } + if (token.startsWith("!") && token.length > 1 && /^[!][0-9A-Za-z]+$/.test(token)) { + const tapeCode = token.substring(1); + const tapeEmbed = this.tapeEmbeds?.get(tapeCode); + if (tapeEmbed?.isLoading) { + const now = performance.now(); + const blinkInterval = 200; + const isBlinking = Math.floor(now / blinkInterval) % 2 === 0; + const mainColor = isBlinking ? "red" : "darkred"; + return `COMPOUND:${mainColor}:${mainColor}`; + } else if (tapeEmbed?.frames?.length > 0) { + const now = performance.now(); + const magicBlinkInterval = 150; + const frame = Math.floor(now / magicBlinkInterval) % 3; + const bangColor = frame === 0 ? "cyan" : frame === 1 ? "magenta" : "yellow"; + return `COMPOUND:${bangColor}:cyan`; + } else if (tapeEmbed?.loadError) { + return `COMPOUND:red:darkred`; + } else { + return `COMPOUND:cyan:teal`; + } + } + if (/^-?\d+(\.\d+)?$/.test(token)) { + const prevToken = index > 0 ? tokens[index - 1] : null; + const nextToken = index < tokens.length - 1 ? tokens[index + 1] : null; + const next2Token = index < tokens.length - 2 ? tokens[index + 2] : null; + const isNumeric = (t2) => t2 && /^-?\d+(\.\d+)?$/.test(t2); + if (isNumeric(nextToken) && isNumeric(next2Token)) { + const value = Math.max(0, Math.min(255, parseFloat(token))); + return `${value},0,0`; + } else if (isNumeric(prevToken) && isNumeric(nextToken)) { + const value = Math.max(0, Math.min(255, parseFloat(token))); + return `0,${value},0`; + } else if (isNumeric(prevToken)) { + const prev2Token = index > 1 ? tokens[index - 2] : null; + if (isNumeric(prev2Token)) { + const value = Math.max(0, Math.min(255, parseFloat(token))); + const greenComponent = Math.round(value * 0.75); + return `0,${greenComponent},${value}`; + } + } + return "pink"; + } + if (/^\d*\.?\d+[sf]!?$/.test(token)) { + if (this.isTimingBlinking(token)) { + return "red"; + } + return "yellow"; + } + if (token === "(" || token === ")") { + return this.getParenthesesColor(tokens, index); + } + if (token === ",") { + return this.getParenthesesColor(tokens, index); + } + if (token === "rainbow") { + return "RAINBOW"; + } + if (token === "zebra") { + return "ZEBRA"; + } + if (token.match(/^c\d+$/)) { + const colorIndex = parseInt(token.substring(1)); + if (staticColorMap[colorIndex]) { + const colorValue = staticColorMap[colorIndex]; + if (Array.isArray(colorValue) && colorValue.length >= 3) { + const rgbColor = `${colorValue[0]},${colorValue[1]},${colorValue[2]}`; + return rgbColor; + } + } + } + if (token.match(/^p\d+$/)) { + const patternIndex = parseInt(token.substring(1)); + if (patternIndex === 0) { + return "RAINBOW"; + } else if (patternIndex === 1) { + return "ZEBRA"; + } + } + if (cssColors2 && cssColors2[token]) { + const colorValue = cssColors2[token]; + if (Array.isArray(colorValue) && colorValue.length >= 3) { + const rgbColor = `${colorValue[0]},${colorValue[1]},${colorValue[2]}`; + return rgbColor; + } + } + if (index > 0 && tokens[index - 1] === "(") { + return this.getFunctionColor(token); + } + if (token.startsWith("fade:")) { + return "mediumseagreen"; + } + if (this.isBareFunction(token, tokens, index)) { + return this.getFunctionColor(token); + } + const knownFunctions = [ + "wipe", + "ink", + "line", + "box", + "flood", + "circle", + "write", + "paste", + "stamp", + "point", + "poly", + "embed", + "print", + "debug", + "random", + "sin", + "cos", + "tan", + "floor", + "ceil", + "round", + "noise", + "choose", + "?", + "...", + "..", + "overtone", + "mic", + "amplitude", + "melody", + "speaker", + "resolution", + "lines", + "wiggle", + "shape", + "scroll", + "flip", + "spin", + "resetSpin", + "smoothspin", + "sort", + "zoom", + "blur", + "contrast", + "pan", + "unpan", + "mask", + "unmask", + "steal", + "putback", + "label", + "len", + "now", + "die", + "tap", + "draw", + "not", + "range", + "mul", + "log", + "no", + "yes", + "fade", + "jump" + ]; + if (token === "fade") { + return "mediumseagreen"; + } + if (["+", "-", "*", "/", "%", "mod", "=", ">", "<", ">=", "<=", "abs", "sqrt", "min", "max"].includes(token)) { + return "lime"; + } + if (knownFunctions.includes(token)) { + return "cyan"; + } + return "orange"; + } + // Helper method to determine if a token is a bare function call + isBareFunction(token, tokens, index) { + if (index > 0) { + const prevToken = tokens[index - 1]; + if (prevToken === "(") { + return false; + } + if (prevToken !== ")") { + return false; + } + } + if (!/^[a-zA-Z_]\w*$/.test(token)) { + return false; + } + const knownFunctions = [ + "wipe", + "ink", + "line", + "box", + "flood", + "circle", + "write", + "paste", + "stamp", + "point", + "poly", + "embed", + "print", + "debug", + "random", + "sin", + "cos", + "tan", + "floor", + "ceil", + "round", + "noise", + "choose", + "?", + "...", + "..", + "overtone", + "rainbow", + "mic", + "amplitude", + "melody", + "speaker", + "resolution", + "lines", + "wiggle", + "shape", + "scroll", + "flip", + "spin", + "resetSpin", + "smoothspin", + "sort", + "zoom", + "blur", + "contrast", + "pan", + "unpan", + "mask", + "unmask", + "steal", + "putback", + "label", + "len", + "now", + "die", + "tap", + "draw", + "not", + "range", + "mul", + "log", + "no", + "yes", + "bake", + "jump" + ]; + return knownFunctions.includes(token) || cssColors2 && cssColors2[token]; + } + // Helper method to get the appropriate color for a function + getFunctionColor(token) { + if (token.startsWith("$") && token.length > 1 && /^[$][0-9A-Za-z]+$/.test(token)) { + const cacheId = token.substring(1); + if (this.loadingEmbeddedLayers.has(cacheId)) { + const now = performance.now(); + const blinkInterval = 200; + const isBlinking = Math.floor(now / blinkInterval) % 2 === 0; + const mainColor = isBlinking ? "red" : "darkred"; + return `COMPOUND:${mainColor}:${mainColor}`; + } else if (this.loadedEmbeddedLayers.has(cacheId)) { + return "COMPOUND:lime:cyan"; + } else { + return "COMPOUND:green:teal"; + } + } + if (token.startsWith("!") && token.length > 1 && /^[!][0-9A-Za-z]+$/.test(token)) { + const tapeCode = token.substring(1); + const tapeEmbed = this.tapeEmbeds?.get(tapeCode); + if (tapeEmbed?.isLoading) { + const now = performance.now(); + const blinkInterval = 200; + const isBlinking = Math.floor(now / blinkInterval) % 2 === 0; + const mainColor = isBlinking ? "red" : "darkred"; + return `COMPOUND:${mainColor}:${mainColor}`; + } else if (tapeEmbed?.frames?.length > 0) { + return "COMPOUND:cyan:teal"; + } else if (tapeEmbed?.loadError) { + return `COMPOUND:red:darkred`; + } else { + return `COMPOUND:cyan:teal`; + } + } + if (["+", "-", "*", "/", "%", "mod", "=", ">", "<", ">=", "<=", "abs", "sqrt", "min", "max"].includes(token)) { + return "lime"; + } + if (["def", "if", "cond", "later", "once", "lambda", "let", "do"].includes(token)) { + return "purple"; + } + if (token === "repeat") { + return "magenta"; + } + return "cyan"; + } + // Get color for parentheses based on nesting depth (rainbow pattern) + getParenthesesColor(tokens, index) { + let depth = 0; + for (let i2 = 0; i2 < index; i2++) { + if (tokens[i2] === "(") { + depth++; + } else if (tokens[i2] === ")") { + depth--; + } + } + if (tokens[index] === ")") { + depth--; + } + depth = Math.max(0, depth); + const parenColors = [ + "192,192,192", + // Light gray (depth 0) + "255,215,0", + // Gold (depth 1) + "255,165,0", + // Orange (depth 2) + "255,105,180", + // Hot pink (depth 3) + "138,43,226", + // Blue violet (depth 4) + "0,191,255", + // Deep sky blue (depth 5) + "50,205,50" + // Lime green (depth 6) + ]; + const colorIndex = depth % parenColors.length; + const color3 = parenColors[colorIndex]; + if (!color3) { + console.warn(`\u26A0\uFE0F getParenthesesColor returning undefined. depth=${depth}, colorIndex=${colorIndex}, token=${tokens[index]}`); + return "192,192,192"; + } + return color3; + } + // Check if an expression matches the currently executing one + isExpressionMatch(posText, executingExpr) { + if (typeof executingExpr === "string") { + return posText.includes(executingExpr); + } else if (Array.isArray(executingExpr)) { + const exprStr = JSON.stringify(executingExpr); + return posText.includes(executingExpr[0]); + } + return false; + } + // Determine the type of expression for syntax coloring + getExpressionType(exprText) { + const trimmed = exprText.trim(); + if (trimmed.startsWith("(")) { + const match = trimmed.match(/^\(\s*([^\s)]+)/); + if (match) { + const funcName = match[1]; + if (["def", "if", "cond", "repeat", "later", "once", "lambda", "let", "do"].includes(funcName)) { + return "special"; + } + if (["ink", "wipe", "line", "box", "circle", "write", "paste", "stamp", "point", "poly", "embed"].includes( + funcName + )) { + return "function"; + } + if (["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "mod", "abs", "sqrt"].includes(funcName)) { + return "function"; + } + if (["print", "debug", "random", "sin", "cos", "tan", "floor", "ceil", "round"].includes(funcName)) { + return "function"; + } + return "function"; + } + } else if (/^-?\d+(\.\d+)?$/.test(trimmed)) { + return "number"; + } else if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return "string"; + } else if (trimmed.startsWith(";")) { + return "comment"; + } else { + return "variable"; + } + return "unknown"; + } + // Update HUD with syntax highlighted kidlisp + async updateHUDWithSyntaxHighlighting(api) { + if (!api.hud || !api.hud.label || !this.syntaxHighlightSource) return; + const coloredString = this.buildColoredKidlispString(); + if (coloredString) { + let attributionText = ""; + if (api.cachedKidlispOwner) { + if (this.cachedOwnerSub === api.cachedKidlispOwner && this.cachedOwnerHandle) { + attributionText = ` \\gray\\by @${this.cachedOwnerHandle}`; + } else { + this.getHandleFromSub(api.cachedKidlispOwner); + } + } + if (!this.isLispFile) { + api.hud.supportsInlineColor = true; + api.hud.disablePieceNameColoring = true; + api.hud.frameHash = performance.now(); + api.hud.label(coloredString + attributionText, void 0, 0); + } else { + api.hud.supportsInlineColor = false; + api.hud.disablePieceNameColoring = false; + } + } + } + // Fetch handle from user sub using the /handle API endpoint (with caching) + async getHandleFromSub(userSub) { + if (this.cachedOwnerSub === userSub && this.cachedOwnerHandle) { + return this.cachedOwnerHandle; + } + try { + const response = await fetch(`/handle?for=${userSub}`); + if (response.status === 200) { + const data = await response.json(); + this.cachedOwnerHandle = data.handle; + this.cachedOwnerSub = userSub; + return data.handle; + } + } catch (error) { + console.warn("Error fetching handle:", error); + } + this.cachedOwnerHandle = null; + this.cachedOwnerSub = null; + return null; + } + // Helper method to strip color codes (copied from clock.mjs pattern) + stripColorCodes(str7) { + return str7.replace(/\\[a-zA-Z0-9]+\\/g, ""); + } + // Resize a pixel buffer while preserving overlapping content + resizePixelBufferPreserve(bufferObj, targetWidth, targetHeight, fillAlpha = 0) { + const width2 = Math.max(0, Math.floor(targetWidth || 0)); + const height2 = Math.max(0, Math.floor(targetHeight || 0)); + const pixelCount = width2 * height2 * 4; + const newPixels = new Uint8ClampedArray(Math.max(0, pixelCount)); + if (fillAlpha !== 0 && pixelCount > 0) { + for (let i2 = 3; i2 < pixelCount; i2 += 4) { + newPixels[i2] = fillAlpha; + } + } + if (bufferObj?.pixels && bufferObj.width && bufferObj.height) { + const oldPixels = bufferObj.pixels; + const copyWidth = Math.min(bufferObj.width, width2); + const copyHeight = Math.min(bufferObj.height, height2); + if (copyWidth > 0 && copyHeight > 0) { + for (let y = 0; y < copyHeight; y++) { + const oldRowStart = y * bufferObj.width * 4; + const newRowStart = y * width2 * 4; + newPixels.set( + oldPixels.subarray(oldRowStart, oldRowStart + copyWidth * 4), + newRowStart + ); + } + } + } + return { + width: width2, + height: height2, + pixels: newPixels + }; + } + ensureBakedLayerSize(bakedLayer, targetWidth, targetHeight) { + if (!bakedLayer || !bakedLayer.buffer) return; + if (bakedLayer.width === targetWidth && bakedLayer.height === targetHeight && bakedLayer.buffer.width === targetWidth && bakedLayer.buffer.height === targetHeight) { + return; + } + const resized = this.resizePixelBufferPreserve( + bakedLayer.buffer, + targetWidth, + targetHeight, + 0 + ); + bakedLayer.buffer = resized; + bakedLayer.width = resized.width; + bakedLayer.height = resized.height; + } + runWithBakedBuffer(api, callback) { + if (!this.bakedLayers || this.bakedLayers.length === 0) { + return false; + } + const bakedLayer = this.bakedLayers[0]; + if (!bakedLayer?.buffer) { + return false; + } + if (api.screen?.width && api.screen?.height) { + this.ensureBakedLayerSize(bakedLayer, api.screen.width, api.screen.height); + } + const originalScreen = api.screen; + api.page(bakedLayer.buffer); + try { + callback(bakedLayer); + } finally { + api.page(originalScreen); + } + return true; + } + beginBakedFrameRouting(api) { + if (!this.hasBakedContent || !this.bakedLayers || this.bakedLayers.length === 0) { + this.frameRoutingContext = null; + return () => { + }; + } + const bakedLayer = this.bakedLayers[0]; + if (!bakedLayer?.buffer) { + this.frameRoutingContext = null; + return () => { + }; + } + if (api.screen?.width && api.screen?.height) { + this.ensureBakedLayerSize(bakedLayer, api.screen.width, api.screen.height); + } + const originalScreen = api.screen; + this.frameRoutingContext = { + originalScreen, + bakedLayer, + currentTarget: "baked" + }; + if (VERBOSE) { + const sampleIdx = 0; + const sample = Array.from(bakedLayer.buffer.pixels.slice(sampleIdx, sampleIdx + 4)); + console.log(`\u{1F35E} beginBakedFrameRouting: Routing to baked layer, first pixel before routing:`, sample); + } + api.page(bakedLayer.buffer); + if (VERBOSE) { + const sampleIdx = 0; + const sample = Array.from(api.screen.pixels.slice(sampleIdx, sampleIdx + 4)); + console.log(`\u{1F35E} beginBakedFrameRouting: After routing, api.screen first pixel:`, sample); + } + return () => { + this.endBakedFrameRouting(api); + }; + } + switchToPostBakeRouting(api) { + if (!this.postBakeLayer) { + return false; + } + if (api.screen?.width && api.screen?.height && (this.postBakeLayer.width !== api.screen.width || this.postBakeLayer.height !== api.screen.height)) { + this.postBakeLayer = this.resizePixelBufferPreserve( + this.postBakeLayer, + api.screen.width, + api.screen.height, + 0 + ); + } + if (!this.frameRoutingContext) { + this.frameRoutingContext = { + originalScreen: api.screen, + currentTarget: "postBake" + }; + } + api.page(this.postBakeLayer); + this.frameRoutingContext.currentTarget = "postBake"; + console.log("\u{1F35E} Switched to post-bake overlay buffer - api.screen now points to overlay"); + console.log("\u{1F35E} api.screen.width:", api.screen?.width, "overlay.width:", this.postBakeLayer.width); + console.log("\u{1F35E} postBakeLayer.pixels identity:", this.postBakeLayer.pixels.slice(0, 4)); + console.log("\u{1F35E} api.screen has pixels?", !!api.screen.pixels); + if (VERBOSE) console.log("\u{1F35E} Switched to post-bake overlay buffer"); + return true; + } + endBakedFrameRouting(api) { + if (!this.frameRoutingContext) { + return; + } + if (VERBOSE) { + const sampleIdx = 0; + const sample = Array.from(api.screen.pixels.slice(sampleIdx, sampleIdx + 4)); + console.log(`\u{1F35E} endBakedFrameRouting: Before restoring, api.screen first pixel:`, sample); + } + const { originalScreen, bakedLayer } = this.frameRoutingContext; + if (VERBOSE && bakedLayer?.buffer?.pixels) { + const sampleIdx = 0; + const sample = Array.from(bakedLayer.buffer.pixels.slice(sampleIdx, sampleIdx + 4)); + console.log(`\u{1F35E} endBakedFrameRouting: Baked layer first pixel:`, sample); + } + if (originalScreen) { + api.page(originalScreen); + } + if (VERBOSE) { + const sampleIdx = 0; + const sample = Array.from(api.screen.pixels.slice(sampleIdx, sampleIdx + 4)); + console.log(`\u{1F35E} endBakedFrameRouting: After restoring, api.screen first pixel:`, sample); + } + this.frameRoutingContext = null; + } + executePreBakeDraw(api, drawFn) { + if (!this.suppressDrawingBeforeBake) { + drawFn(); + return true; + } + if (this.frameRoutingContext?.currentTarget === "baked") { + drawFn(); + return true; + } + return this.runWithBakedBuffer(api, () => { + drawFn(); + }); + } + // Render baked layers - execute pre-bake code and composite the result + renderBakedLayers(api) { + if (!this.bakedLayers || this.bakedLayers.length === 0) { + if (VERBOSE && this.hasBakedContent) { + console.log("\u{1F35E} renderBakedLayers: No baked layers to render despite hasBakedContent=true"); + } + return; + } + const screenWidth = api.screen?.width; + const screenHeight = api.screen?.height; + this.bakedLayers.forEach((bakedLayer, index) => { + if (!bakedLayer || !bakedLayer.buffer) { + if (VERBOSE) console.log(`\u{1F35E} renderBakedLayers: Layer ${index} has no buffer`); + return; + } + if (VERBOSE) { + const sampleIdx = 0; + const sample = bakedLayer.buffer.pixels?.slice(sampleIdx, sampleIdx + 4); + console.log(`\u{1F35E} renderBakedLayers: Layer ${index} first pixel:`, sample); + } + if (screenWidth && screenHeight) { + this.ensureBakedLayerSize(bakedLayer, screenWidth, screenHeight); + } + if (bakedLayer.buffer.width !== bakedLayer.width || bakedLayer.buffer.height !== bakedLayer.height) { + console.warn(`\u{1F35E} Buffer size mismatch on layer ${index}: buffer=${bakedLayer.buffer.width}x${bakedLayer.buffer.height} metadata=${bakedLayer.width}x${bakedLayer.height}`); + } + this.compositeBakedLayer(api, bakedLayer.buffer); + }); + } + // Manual pixel compositing for baked layers + compositeBakedLayer(api, bakedLayer) { + if (!api.screen?.pixels || !bakedLayer.pixels) { + return; + } + const currentPixels = api.screen.pixels; + const bakedPixels = bakedLayer.pixels; + const currentWidth = api.screen.width; + const currentHeight = api.screen.height; + const bakedWidth = bakedLayer.width; + const bakedHeight = bakedLayer.height; + const width2 = Math.min(currentWidth, bakedWidth); + const height2 = Math.min(currentHeight, bakedHeight); + for (let y = 0; y < height2; y++) { + for (let x = 0; x < width2; x++) { + const currentIndex = (y * currentWidth + x) * 4; + const bakedIndex = (y * bakedWidth + x) * 4; + const currentR = currentPixels[currentIndex]; + const currentG = currentPixels[currentIndex + 1]; + const currentB = currentPixels[currentIndex + 2]; + const currentA = currentPixels[currentIndex + 3] / 255; + const bakedR = bakedPixels[bakedIndex]; + const bakedG = bakedPixels[bakedIndex + 1]; + const bakedB = bakedPixels[bakedIndex + 2]; + const bakedA = bakedPixels[bakedIndex + 3] / 255; + const outA = currentA + bakedA * (1 - currentA); + if (outA > 0) { + const outR = (currentR * currentA + bakedR * bakedA * (1 - currentA)) / outA; + const outG = (currentG * currentA + bakedG * bakedA * (1 - currentA)) / outA; + const outB = (currentB * currentA + bakedB * bakedA * (1 - currentA)) / outA; + currentPixels[currentIndex] = Math.round(outR); + currentPixels[currentIndex + 1] = Math.round(outG); + currentPixels[currentIndex + 2] = Math.round(outB); + currentPixels[currentIndex + 3] = Math.round(outA * 255); + } + } + } + } + // Composite the post-bake overlay on top of the baked background + compositePostBakeLayer(api, overlayLayer) { + if (!api.screen?.pixels || !overlayLayer.pixels) { + console.log("\u{1F35E} compositePostBakeLayer: Missing buffers!"); + return; + } + let nonTransparentCount = 0; + for (let i2 = 3; i2 < overlayLayer.pixels.length; i2 += 4) { + if (overlayLayer.pixels[i2] > 0) nonTransparentCount++; + } + console.log(`\u{1F35E} compositePostBakeLayer: Overlay has ${nonTransparentCount} non-transparent pixels`); + const currentPixels = api.screen.pixels; + const overlayPixels = overlayLayer.pixels; + const currentWidth = api.screen.width; + const currentHeight = api.screen.height; + const overlayWidth = overlayLayer.width; + const overlayHeight = overlayLayer.height; + const width2 = Math.min(currentWidth, overlayWidth); + const height2 = Math.min(currentHeight, overlayHeight); + for (let y = 0; y < height2; y++) { + for (let x = 0; x < width2; x++) { + const currentIndex = (y * currentWidth + x) * 4; + const overlayIndex = (y * overlayWidth + x) * 4; + const overlayR = overlayPixels[overlayIndex]; + const overlayG = overlayPixels[overlayIndex + 1]; + const overlayB = overlayPixels[overlayIndex + 2]; + const overlayA = overlayPixels[overlayIndex + 3] / 255; + if (overlayA === 0) continue; + const currentR = currentPixels[currentIndex]; + const currentG = currentPixels[currentIndex + 1]; + const currentB = currentPixels[currentIndex + 2]; + const currentA = currentPixels[currentIndex + 3] / 255; + const outA = overlayA + currentA * (1 - overlayA); + if (outA > 0) { + const outR = (overlayR * overlayA + currentR * currentA * (1 - overlayA)) / outA; + const outG = (overlayG * overlayA + currentG * currentA * (1 - overlayA)) / outA; + const outB = (overlayB * overlayA + currentB * currentA * (1 - overlayA)) / outA; + currentPixels[currentIndex] = Math.round(outR); + currentPixels[currentIndex + 1] = Math.round(outG); + currentPixels[currentIndex + 2] = Math.round(outB); + currentPixels[currentIndex + 3] = Math.round(outA * 255); + } + } + } + } + // Helper method to create embedded layer from source code + createEmbeddedLayerFromSource(source, cacheId, layerKey, width2, height2, x, y, alpha, api) { + const existingLayer = this.embeddedLayerCache.get(layerKey); + if (existingLayer) { + if (existingLayer.source !== source) { + if (existingLayer.buffer && existingLayer.buffer.pixels) { + existingLayer.buffer.pixels.fill(0); + } + existingLayer.firstLineColorApplied = false; + } + const embeddedKidLisp2 = existingLayer.kidlispInstance; + embeddedKidLisp2.onceExecuted.clear(); + const layerCacheKey2 = `${source}_timing`; + let existingTimingState2 = this.embeddedLayerCache?.get?.(layerCacheKey2); + if (existingTimingState2) { + embeddedKidLisp2.frameCount = existingTimingState2.frameCount || 0; + embeddedKidLisp2.frameCounter = existingTimingState2.frameCounter || 0; + embeddedKidLisp2.lastSecondExecutions = existingTimingState2.lastSecondExecutions || {}; + embeddedKidLisp2.sequenceCounters = new Map(existingTimingState2.sequenceCounters || []); + embeddedKidLisp2.timingStates = new Map(existingTimingState2.timingStates || []); + if (existingTimingState2.randomState) { + embeddedKidLisp2.randomState = existingTimingState2.randomState; + } + } else { + embeddedKidLisp2.frameCount = 0; + embeddedKidLisp2.frameCounter = 0; + embeddedKidLisp2.lastSecondExecutions = {}; + embeddedKidLisp2.sequenceCounters = /* @__PURE__ */ new Map(); + embeddedKidLisp2.timingStates = /* @__PURE__ */ new Map(); + if (!embeddedKidLisp2.randomSeed) { + embeddedKidLisp2.randomSeed = Date.now() + Math.random() + (source.hashCode?.() || 0); + embeddedKidLisp2.randomState = embeddedKidLisp2.randomSeed; + } + } + embeddedKidLisp2.localEnv = { ...this.localEnv }; + embeddedKidLisp2.isNestedInstance = true; + existingLayer.alpha = alpha; + existingLayer.localFrameCount = 0; + return existingLayer; + } + const embeddedKidLisp = new _KidLisp(); + embeddedKidLisp.embeddedSourceId = cacheId; + embeddedKidLisp.randomSeed = Date.now() + Math.random() + (source.hashCode?.() || 0); + embeddedKidLisp.randomState = embeddedKidLisp.randomSeed; + const layerCacheKey = `${source}_timing`; + let existingTimingState = this.embeddedLayerCache?.get?.(layerCacheKey); + if (existingTimingState) { + embeddedKidLisp.frameCount = existingTimingState.frameCount || 0; + embeddedKidLisp.frameCounter = existingTimingState.frameCounter || 0; + embeddedKidLisp.lastSecondExecutions = existingTimingState.lastSecondExecutions || {}; + embeddedKidLisp.sequenceCounters = new Map(existingTimingState.sequenceCounters || []); + embeddedKidLisp.timingStates = new Map(existingTimingState.timingStates || []); + if (existingTimingState.randomState) { + embeddedKidLisp.randomState = existingTimingState.randomState; + } + } else { + embeddedKidLisp.frameCount = 0; + embeddedKidLisp.frameCounter = 0; + embeddedKidLisp.lastSecondExecutions = {}; + embeddedKidLisp.sequenceCounters = /* @__PURE__ */ new Map(); + embeddedKidLisp.timingStates = /* @__PURE__ */ new Map(); + } + embeddedKidLisp.localEnv = { ...this.localEnv }; + embeddedKidLisp.isNestedInstance = true; + embeddedKidLisp.embeddedSourceCache = this.embeddedSourceCache; + embeddedKidLisp.embeddedLayerCache = this.embeddedLayerCache; + let processedSource = source; + processedSource = processedSource.replace(/^scroll\s+([\d.]+)$/gm, "(scroll $1)"); + processedSource = processedSource.replace(/^scroll\s+(.+)$/gm, "(scroll $1)"); + const parsedCode = embeddedKidLisp.parse(processedSource); + const precompiledCode = embeddedKidLisp.precompileAST(parsedCode); + embeddedKidLisp.ast = JSON.parse(JSON.stringify(precompiledCode)); + embeddedKidLisp.detectFirstLineColor(); + let embeddedBuffer; + if (!this.embeddedLayers) { + console.log("\u{1F6A8} embeddedLayers was null, reinitializing to empty array"); + this.embeddedLayers = []; + } + const persistentLayer = this.embeddedLayers.find((layer) => layer.cacheId === layerKey); + if (persistentLayer && persistentLayer.buffer) { + if (persistentLayer.source === source) { + embeddedBuffer = persistentLayer.buffer; + } else { + if (persistentLayer.buffer && persistentLayer.buffer.pixels) { + for (let i2 = 0; i2 < persistentLayer.buffer.pixels.length; i2 += 4) { + persistentLayer.buffer.pixels[i2] = 0; + persistentLayer.buffer.pixels[i2 + 1] = 0; + persistentLayer.buffer.pixels[i2 + 2] = 0; + persistentLayer.buffer.pixels[i2 + 3] = 0; + } + console.log(`\u{1F3A8} EMBEDDED BUFFER CLEAR: Reinitialized buffer with transparent black [0,0,0,0]`); + } + embeddedBuffer = persistentLayer.buffer; + persistentLayer.source = source; + persistentLayer.parsedCode = precompiledCode; + persistentLayer.kidlispInstance = embeddedKidLisp; + persistentLayer.firstLineColorApplied = false; + } + } else { + let foundCompatibleBuffer = false; + if (existingLayer && existingLayer.buffer) { + const currentSize = existingLayer.buffer.width * existingLayer.buffer.height; + const newSize = width2 * height2; + if (newSize <= currentSize && existingLayer.buffer.width >= width2 && existingLayer.buffer.height >= height2) { + embeddedBuffer = existingLayer.buffer; + foundCompatibleBuffer = true; + console.log(`\u{1F504} Reusing existing buffer during reframe: ${width2}x${height2} fits in ${existingLayer.buffer.width}x${existingLayer.buffer.height}`); + if (embeddedBuffer.pixels) { + const totalPixels = width2 * height2; + for (let i2 = 0; i2 < totalPixels; i2++) { + const pixelIndex = i2 * 4; + embeddedBuffer.pixels[pixelIndex] = 0; + embeddedBuffer.pixels[pixelIndex + 1] = 0; + embeddedBuffer.pixels[pixelIndex + 2] = 0; + embeddedBuffer.pixels[pixelIndex + 3] = 255; + } + console.log(`\u{1F3A8} EMBEDDED BUFFER REFRAME: Initialized used area with opaque black [0,0,0,255]`); + } + } + } + if (!foundCompatibleBuffer) { + const bufferSizeKey = `${width2}x${height2}`; + const pooledBuffers = this.bufferPool.get(bufferSizeKey); + if (pooledBuffers && pooledBuffers.length > 0) { + embeddedBuffer = pooledBuffers.pop(); + if (embeddedBuffer.pixels) { + for (let i2 = 0; i2 < embeddedBuffer.pixels.length; i2 += 4) { + embeddedBuffer.pixels[i2] = 0; + embeddedBuffer.pixels[i2 + 1] = 0; + embeddedBuffer.pixels[i2 + 2] = 0; + embeddedBuffer.pixels[i2 + 3] = 0; + } + console.log(`\u{1F3A8} EMBEDDED BUFFER INIT: Initialized buffer with transparent black [0,0,0,0]`); + } + } else { + embeddedBuffer = this.createOrReuseBuffer(width2, height2); + } + } + } + if (!embeddedBuffer) { + console.error("\u274C Failed to create embedded buffer for:", layerKey); + return void 0; + } + if (embeddedKidLisp.firstLineColor && embeddedBuffer.pixels) { + const flc = embeddedKidLisp.firstLineColor; + let bgRgb = null; + if (typeof flc === "string" && flc.startsWith("fade:")) { + const fadeColors2 = embeddedKidLisp.parseFadeString(flc); + if (fadeColors2 && fadeColors2.length > 0) bgRgb = fadeColors2[0]; + } else { + const resolved = embeddedKidLisp.resolveColorToRGBA(flc, {}); + if (resolved && resolved[3] > 0) bgRgb = resolved; + } + if (bgRgb) { + const px = embeddedBuffer.pixels; + const r2 = bgRgb[0], g = bgRgb[1], b2 = bgRgb[2]; + for (let i2 = 0, len5 = px.length; i2 < len5; i2 += 4) { + px[i2] = r2; + px[i2 + 1] = g; + px[i2 + 2] = b2; + px[i2 + 3] = 255; + } + } + } + const embeddedLayer = { + id: `embedded_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + // Unique ID for each layer + cacheId: layerKey, + originalCacheId: cacheId, + width: width2, + height: height2, + x, + y, + alpha, + // Store alpha value for blending + buffer: embeddedBuffer, + kidlispInstance: embeddedKidLisp, + parsedCode: precompiledCode, + source, + sourceCode: source, + // Store for timing pattern extraction + timingPattern: this.extractTimingPattern(source), + // Pre-extract timing pattern + localFrameCount: 0, + // Start with fresh frame count for proper timing + // Store timing context information if this layer was created within a timing expression + timingContext: this.currentTimingContext ? { + timingKey: this.currentTimingContext.timingKey, + argumentIndex: this.currentTimingContext.currentIndex, + cacheId + // Store the specific cache ID this layer corresponds to + } : null + }; + if (!this.embeddedLayers) { + console.log("\u{1F6A8} embeddedLayers was null during push, reinitializing to empty array"); + this.embeddedLayers = []; + } + const existingIndex = this.embeddedLayers.findIndex((l2) => l2.cacheId === layerKey); + if (existingIndex !== -1) { + console.log(`\u26A0\uFE0F Replacing existing embedded layer at index ${existingIndex}: ${layerKey}`); + const oldLayer = this.embeddedLayers[existingIndex]; + if (oldLayer.buffer) { + this.returnBufferToPool(oldLayer.buffer, oldLayer.width, oldLayer.height); + } + this.embeddedLayers[existingIndex] = embeddedLayer; + } else { + this.embeddedLayers.push(embeddedLayer); + if (this.embeddedLayers.length === 1 && !this.autoDensityOverride) { + this.enableAutoDensity(); + } + } + this.embeddedLayerCache.set(layerKey, embeddedLayer); + return embeddedLayer; + } + // Clear embedded layer cache (called on reload/initialization) + clearEmbeddedLayerCache() { + if (this.embeddedLayers) { + this.embeddedLayers.forEach((layer) => { + if (layer && layer.buffer) { + this.returnBufferToPool(layer.buffer, layer.width, layer.height); + } + }); + } + this.embeddedLayers = []; + this.embeddedLayerCache.clear(); + this.embeddedSourceCache.clear(); + this.embeddedApiCache.clear(); + if (this.alphaBufferCache) { + this.alphaBufferCache.clear(); + } + this.cleanBufferPools(); + } + // 🍞 Clear baked layers (similar to embedded layer clearing) + clearBakedLayers() { + if (this.layer0 && this.layer0.pixels) { + try { + this.layer0.pixels.fill(0); + } catch (e2) { + console.warn("Failed to clear layer0 pixels:", e2); + } + } + if (this.burnedBuffer && this.burnedBuffer.pixels) { + try { + this.burnedBuffer.pixels.fill(0); + } catch (e2) { + console.warn("Failed to clear burnedBuffer pixels:", e2); + } + } + if (this.bakes && this.bakes.length > 0) { + for (const bakeLayer of this.bakes) { + if (bakeLayer && bakeLayer.pixels) { + try { + bakeLayer.pixels.fill(0); + } catch (e2) { + console.warn("Failed to clear bake layer pixels:", e2); + } + } + } + } + if (this.bakedLayers && this.bakedLayers.length > 0) { + for (const bakedLayer of this.bakedLayers) { + if (bakedLayer?.buffer && bakedLayer.buffer.pixels) { + try { + bakedLayer.buffer.pixels.fill(0); + } catch (e2) { + console.warn("Failed to clear bakedLayer buffer pixels:", e2); + } + } + } + } + this.layer0 = null; + this.burnedBuffer = null; + this.displayBuffer = null; + this._burnScrollAccX = 0; + this._burnScrollAccY = 0; + if (this.bakes) { + this.bakes.length = 0; + } else { + this.bakes = []; + } + if (this.bakedLayers) { + this.bakedLayers.length = 0; + } else { + this.bakedLayers = []; + } + this.currentBakeIndex = -1; + this.bakeCallCount = 0; + this.hasBakedContent = false; + this.suppressDrawingBeforeBake = false; + this.postBakeLayer = null; + this.cachedComposite = null; + this.compositeInvalidated = true; + this.needsInitialWipe = true; + if (this.bufferPool) { + this.bufferPool.clear(); + } + if (this.onceExecuted) { + this.onceExecuted.delete("bake_call"); + } + } + // 🔄 RESPONSIVE CACHE: Check if screen dimensions changed and clear responsive entries + checkAndClearResponsiveCacheOnReframe(api) { + if (!api || !api.screen) return false; + const currentWidth = api.screen.width; + const currentHeight = api.screen.height; + const widthChanged = this.lastScreenWidth === null || Math.abs(this.lastScreenWidth - currentWidth) > 2; + const heightChanged = this.lastScreenHeight === null || Math.abs(this.lastScreenHeight - currentHeight) > 2; + if (widthChanged || heightChanged) { + console.log(`\u{1F4D0} Screen dimensions changed significantly: ${this.lastScreenWidth}x${this.lastScreenHeight} \u2192 ${currentWidth}x${currentHeight}`); + if (this.embeddedLayerCache) { + const entriesToDelete = []; + for (const [key, layer] of this.embeddedLayerCache.entries()) { + if (key.includes("_screen")) { + entriesToDelete.push(key); + if (layer && layer.buffer) { + this.returnBufferToPool(layer.buffer, layer.width, layer.height); + } + } + } + entriesToDelete.forEach((key) => { + this.embeddedLayerCache.delete(key); + console.log(`\u{1F5D1}\uFE0F Cleared responsive cache entry: ${key}`); + }); + if (entriesToDelete.length > 0) { + console.log(`\u2728 Cleared ${entriesToDelete.length} responsive cache entries for screen resize`); + } + } + this.lastScreenWidth = currentWidth; + this.lastScreenHeight = currentHeight; + return true; + } + return false; + } + // 🖼️ REFRAME HANDLER: Called when screen dimensions change (called from disk systems) + onReframe(newWidth, newHeight) { + console.log(`\u{1F5BC}\uFE0F KidLisp received reframe event: ${newWidth}x${newHeight}`); + if (this.embeddedLayerCache) { + const entriesToDelete = []; + for (const [key, layer] of this.embeddedLayerCache.entries()) { + if (key.includes("_screen")) { + entriesToDelete.push(key); + if (layer && layer.buffer) { + this.returnBufferToPool(layer.buffer, layer.width, layer.height); + } + } + } + entriesToDelete.forEach((key) => { + this.embeddedLayerCache.delete(key); + console.log(`\u{1F5D1}\uFE0F Cleared responsive cache entry on reframe: ${key}`); + }); + if (entriesToDelete.length > 0) { + console.log(`\u2728 Cleared ${entriesToDelete.length} responsive cache entries for reframe`); + } + } + let bgColor = this.getBackgroundFillColor(); + if (bgColor) { + console.log(`\u{1F3A8} Reframe: Wiping layer0 with first-line color:`, bgColor); + if (this.layer0 && this.layer0.pixels) { + const color3 = this.resolveColorToRGBA(bgColor, globalThis.$paintApiUnwrapped || globalThis.$paintApi); + if (color3) { + const pixels2 = this.layer0.pixels; + for (let i2 = 0; i2 < pixels2.length; i2 += 4) { + pixels2[i2] = color3[0]; + pixels2[i2 + 1] = color3[1]; + pixels2[i2 + 2] = color3[2]; + pixels2[i2 + 3] = color3[3] !== void 0 ? color3[3] : 255; + } + console.log(`\u{1F3A8} Reframe: Filled layer0 buffer directly with`, bgColor); + } + } + } + this.lastScreenWidth = newWidth; + this.lastScreenHeight = newHeight; + } + // 🛡️ SAFETY: Clean buffer pools of any detached buffers + cleanBufferPools() { + if (!this.bufferPool) return; + for (const [sizeKey, buffers] of this.bufferPool.entries()) { + const validBuffers = buffers.filter((buffer) => { + return buffer && buffer.pixels && buffer.pixels.buffer && !buffer.pixels.buffer.detached; + }); + if (validBuffers.length !== buffers.length) { + console.log(`\u{1F9F9} Cleaned ${buffers.length - validBuffers.length} detached buffers from ${sizeKey} pool`); + } + if (validBuffers.length > 0) { + this.bufferPool.set(sizeKey, validBuffers); + } else { + this.bufferPool.delete(sizeKey); + } + } + } + // 🔄 BUFFER POOLING: Return a buffer to the pool for reuse + returnBufferToPool(buffer, width2, height2) { + if (!buffer || !buffer.pixels) return; + if (buffer.pixels.buffer && buffer.pixels.buffer.detached) { + console.warn("\u{1F6A8} Refusing to pool detached buffer"); + return; + } + const bufferSizeKey = `${width2}x${height2}`; + if (!this.bufferPool) { + this.bufferPool = /* @__PURE__ */ new Map(); + } + let pooledBuffers = this.bufferPool.get(bufferSizeKey); + if (!pooledBuffers) { + pooledBuffers = []; + this.bufferPool.set(bufferSizeKey, pooledBuffers); + } + const maxPoolSize = width2 * height2 > 1e5 ? 2 : 8; + if (pooledBuffers.length < maxPoolSize) { + try { + buffer.pixels.fill(0); + pooledBuffers.push(buffer); + } catch (error) { + console.warn("\u{1F6A8} Failed to clear buffer for pooling:", error); + } + } + } + // 🚀 OPTIMIZED BUFFER CREATION: Use pooling and faster allocation + createOrReuseBuffer(width2, height2) { + const bufferSizeKey = `${width2}x${height2}`; + const pooledBuffers = this.bufferPool?.get(bufferSizeKey); + if (pooledBuffers && pooledBuffers.length > 0) { + const buffer = pooledBuffers.pop(); + if (buffer.pixels && buffer.pixels.buffer && !buffer.pixels.buffer.detached) { + buffer.width = width2; + buffer.height = height2; + buffer.pixels.fill(0); + return buffer; + } + console.warn("\u{1F6A8} Discarded detached buffer from pool"); + } + return { + width: width2, + height: height2, + pixels: new Uint8ClampedArray(width2 * height2 * 4) + }; + } + // 🔥 Scroll all persistent layers (layer0 + bakes) after burn. + // Uses its own accumulator to avoid interfering with graph.mjs's shared one. + _scrollPersistentLayers(api, dx, dy) { + if (!this._burnScrollAccX) this._burnScrollAccX = 0; + if (!this._burnScrollAccY) this._burnScrollAccY = 0; + this._burnScrollAccX += dx; + this._burnScrollAccY += dy; + const intDx = Math.trunc(this._burnScrollAccX); + const intDy = Math.trunc(this._burnScrollAccY); + this._burnScrollAccX -= intDx; + this._burnScrollAccY -= intDy; + if (intDx === 0 && intDy === 0) return; + const layers = []; + if (this.layer0?.pixels) layers.push(this.layer0); + if (this.bakes) { + for (const b2 of this.bakes) { + if (b2?.pixels) layers.push(b2); + } + } + for (const buf of layers) { + api.page(buf); + api.resetScrollState?.(); + api.scroll(intDx, intDy); + } + api.page(this.burnedBuffer); + } + // 🔥 Spin/smoothspin all persistent layers after burn. + _spinPersistentLayers(api, args, smooth = false) { + const layers = []; + if (this.layer0?.pixels) layers.push(this.layer0); + if (this.bakes) { + for (const b2 of this.bakes) { + if (b2?.pixels) layers.push(b2); + } + } + for (const buf of layers) { + api.page(buf); + if (smooth) { + api.smoothSpin(...args); + } else { + api.spin(...args); + } + } + api.page(this.burnedBuffer); + } + // Helper function to paste a buffer with alpha blending + // 🚀 ULTRA-OPTIMIZED: Pre-cache alpha buffers and use fast paths + pasteWithAlpha(api, sourceBuffer, x, y, alpha, respectSourceAlpha = true) { + if (!sourceBuffer || !sourceBuffer.pixels || !api.screen || !api.screen.pixels) { + return; + } + if (sourceBuffer.pixels.buffer && sourceBuffer.pixels.buffer.detached) { + console.warn("\u{1F6A8} Attempted to paste from detached buffer, skipping"); + return; + } + const destWidth = api.screen.width; + const destHeight = api.screen.height; + const noHorizontalOverlap = x >= destWidth || x + sourceBuffer.width <= 0; + const noVerticalOverlap = y >= destHeight || y + sourceBuffer.height <= 0; + if (noHorizontalOverlap || noVerticalOverlap) { + return; + } + if (alpha === 255 && !respectSourceAlpha) { + if (api.paste) { + api.paste(sourceBuffer, x, y); + } else { + this.fastDirectPaste(api, sourceBuffer, x, y); + } + return; + } + if (alpha === 255 && respectSourceAlpha) { + let hasTransparency = false; + const pixels2 = sourceBuffer.pixels; + for (let i2 = 3; i2 < pixels2.length; i2 += 4) { + if (pixels2[i2] < 255) { + hasTransparency = true; + break; + } + } + if (!hasTransparency) { + if (api.paste) { + api.paste(sourceBuffer, x, y); + } else { + this.fastDirectPaste(api, sourceBuffer, x, y); + } + return; + } + } + const alphaBufferKey = `${sourceBuffer.width}x${sourceBuffer.height}_${alpha}`; + if (!this.alphaBufferCache) { + this.alphaBufferCache = /* @__PURE__ */ new Map(); + } + let cachedAlphaBuffer = this.alphaBufferCache.get(alphaBufferKey); + if (!cachedAlphaBuffer || this.needsAlphaBufferUpdate(sourceBuffer, cachedAlphaBuffer)) { + if (cachedAlphaBuffer && cachedAlphaBuffer.pixels.length === sourceBuffer.pixels.length) { + this.updateAlphaBuffer(sourceBuffer, cachedAlphaBuffer, alpha); + } else { + cachedAlphaBuffer = { + width: sourceBuffer.width, + height: sourceBuffer.height, + pixels: new Uint8ClampedArray(sourceBuffer.pixels.length), + sourceHash: this.quickPixelHash(sourceBuffer.pixels), + alpha + }; + this.updateAlphaBuffer(sourceBuffer, cachedAlphaBuffer, alpha); + this.alphaBufferCache.set(alphaBufferKey, cachedAlphaBuffer); + } + } + this.fallbackPasteWithAlpha(api, sourceBuffer, x, y, alpha); + } + // 🚀 Check if alpha buffer needs updating (avoids expensive pixel operations) + needsAlphaBufferUpdate(sourceBuffer, cachedBuffer) { + if (!cachedBuffer || !cachedBuffer.sourceHash) return true; + if (!cachedBuffer.pixels || !cachedBuffer.pixels.buffer || cachedBuffer.pixels.buffer.detached) { + return true; + } + const currentHash2 = this.quickPixelHash(sourceBuffer.pixels); + return currentHash2 !== cachedBuffer.sourceHash; + } + // 🚀 Update alpha buffer with optimized SIMD-style operations where possible + updateAlphaBuffer(sourceBuffer, targetBuffer, alpha) { + const alphaFactor = alpha / 255; + const src = sourceBuffer.pixels; + const dst = targetBuffer.pixels; + const len5 = src.length; + let i2 = 0; + for (; i2 < len5 - 15; i2 += 16) { + dst[i2] = src[i2]; + dst[i2 + 1] = src[i2 + 1]; + dst[i2 + 2] = src[i2 + 2]; + dst[i2 + 3] = src[i2 + 3] * alphaFactor; + dst[i2 + 4] = src[i2 + 4]; + dst[i2 + 5] = src[i2 + 5]; + dst[i2 + 6] = src[i2 + 6]; + dst[i2 + 7] = src[i2 + 7] * alphaFactor; + dst[i2 + 8] = src[i2 + 8]; + dst[i2 + 9] = src[i2 + 9]; + dst[i2 + 10] = src[i2 + 10]; + dst[i2 + 11] = src[i2 + 11] * alphaFactor; + dst[i2 + 12] = src[i2 + 12]; + dst[i2 + 13] = src[i2 + 13]; + dst[i2 + 14] = src[i2 + 14]; + dst[i2 + 15] = src[i2 + 15] * alphaFactor; + } + for (; i2 < len5; i2 += 4) { + dst[i2] = src[i2]; + dst[i2 + 1] = src[i2 + 1]; + dst[i2 + 2] = src[i2 + 2]; + dst[i2 + 3] = src[i2 + 3] * alphaFactor; + } + targetBuffer.sourceHash = this.quickPixelHash(src); + targetBuffer.alpha = alpha; + } + // 🚀 Optimized direct paste without API overhead + fastDirectPaste(api, sourceBuffer, x, y) { + const src = sourceBuffer.pixels; + const dst = api.screen.pixels; + const srcW = sourceBuffer.width; + const srcH = sourceBuffer.height; + const dstW = api.screen.width; + const dstH = api.screen.height; + const startX = Math.max(0, x); + const startY = Math.max(0, y); + const endX = Math.min(dstW, x + srcW); + const endY = Math.min(dstH, y + srcH); + if (startX >= endX || startY >= endY) return; + for (let dy = startY; dy < endY; dy++) { + const srcY = dy - y; + const srcRowStart = srcY * srcW * 4; + const dstRowStart = dy * dstW * 4; + for (let dx = startX; dx < endX; dx++) { + const srcX = dx - x; + const srcIdx = srcRowStart + srcX * 4; + const dstIdx = dstRowStart + dx * 4; + dst[dstIdx] = src[srcIdx]; + dst[dstIdx + 1] = src[srcIdx + 1]; + dst[dstIdx + 2] = src[srcIdx + 2]; + dst[dstIdx + 3] = src[srcIdx + 3]; + } + } + } + // Fallback manual alpha blending for when graph.paste is not available + fallbackPasteWithAlpha(api, sourceBuffer, x, y, alpha) { + const srcPixels = sourceBuffer.pixels; + const dstPixels = api.screen.pixels; + const srcWidth = sourceBuffer.width; + const srcHeight = sourceBuffer.height; + const dstWidth = api.screen.width; + const dstHeight = api.screen.height; + if (srcPixels === dstPixels || srcPixels?.buffer === dstPixels?.buffer) { + console.warn(`\u26A0\uFE0F SKIPPING PASTE: Source and destination buffers are the same! This would cause self-overwrite.`); + return; + } + const alphaFactor = alpha / 255; + const startX = Math.max(0, x); + const startY = Math.max(0, y); + const endX = Math.min(dstWidth, x + srcWidth); + const endY = Math.min(dstHeight, y + srcHeight); + let pixelsProcessed = 0; + for (let dy = startY; dy < endY; dy++) { + const srcY = dy - y; + if (srcY < 0 || srcY >= srcHeight) continue; + for (let dx = startX; dx < endX; dx++) { + pixelsProcessed++; + const srcX = dx - x; + if (srcX < 0 || srcX >= srcWidth) continue; + const srcIndex = (srcY * srcWidth + srcX) * 4; + const dstIndex = (dy * dstWidth + dx) * 4; + let srcA = srcPixels[srcIndex + 3]; + if (srcA === 0) { + const hasColor = srcPixels[srcIndex] !== 0 || srcPixels[srcIndex + 1] !== 0 || srcPixels[srcIndex + 2] !== 0; + if (!hasColor) { + continue; + } + srcA = 255; + } + if (alphaFactor === 1 && srcA === 255) { + dstPixels[dstIndex] = srcPixels[srcIndex]; + dstPixels[dstIndex + 1] = srcPixels[srcIndex + 1]; + dstPixels[dstIndex + 2] = srcPixels[srcIndex + 2]; + dstPixels[dstIndex + 3] = 255; + } else { + const effectiveAlpha = srcA * alphaFactor + 1 | 0; + if (dstPixels[dstIndex + 3] === 0) { + dstPixels[dstIndex] = srcPixels[srcIndex]; + dstPixels[dstIndex + 1] = srcPixels[srcIndex + 1]; + dstPixels[dstIndex + 2] = srcPixels[srcIndex + 2]; + dstPixels[dstIndex + 3] = effectiveAlpha; + } else { + const invAlpha = 256 - effectiveAlpha; + dstPixels[dstIndex] = effectiveAlpha * srcPixels[srcIndex] + invAlpha * dstPixels[dstIndex] >> 8; + dstPixels[dstIndex + 1] = effectiveAlpha * srcPixels[srcIndex + 1] + invAlpha * dstPixels[dstIndex + 1] >> 8; + dstPixels[dstIndex + 2] = effectiveAlpha * srcPixels[srcIndex + 2] + invAlpha * dstPixels[dstIndex + 2] >> 8; + dstPixels[dstIndex + 3] = Math.min(255, dstPixels[dstIndex + 3] + effectiveAlpha); + } + } + } + } + } + // Fast pixel hash for change detection (samples key pixels to avoid full buffer comparison) + quickPixelHash(pixels2) { + if (!pixels2 || pixels2.length === 0) return 0; + let hash = 0; + const len5 = pixels2.length; + for (let i2 = 0; i2 < len5; i2 += 64) { + hash = (hash << 5) - hash + pixels2[i2] | 0; + } + return hash; + } + // Render and update embedded layers each frame + renderEmbeddedLayers(api) { + if (!this.embeddedLayers || this.embeddedLayers.length === 0) { + return; + } + const currentScreenSize = `${api.screen?.width || 0}x${api.screen?.height || 0}`; + if (this.lastScreenSize && this.lastScreenSize !== currentScreenSize) { + const timeSinceLastResize = performance.now() - (this.lastResizeTime || 0); + if (timeSinceLastResize < 100) { + if (this.embeddedLayers) { + this.embeddedLayers.forEach((embeddedLayer) => { + if (embeddedLayer && embeddedLayer.buffer && api.paste) { + this.pasteWithAlpha(api, embeddedLayer.buffer, embeddedLayer.x, embeddedLayer.y, embeddedLayer.alpha); + } + }); + } + return; + } + } + this.lastScreenSize = currentScreenSize; + this.lastResizeTime = performance.now(); + const layerCount = this.embeddedLayers.length; + if (layerCount > 3 && performance.now() - (this.lastComplexRender || 0) < 16) { + return; + } + if (layerCount > 3) { + this.lastComplexRender = performance.now(); + } + if (api.frame && api.frame % 300 === 0) { + this.cleanBufferPools(); + if (this.embeddedLayers && this.embeddedLayerCache) { + const initialCount = this.embeddedLayers.length; + this.embeddedLayers = this.embeddedLayers.filter((layer) => { + if (!layer.cacheId) return true; + const inCache = this.embeddedLayerCache.has(layer.cacheId); + if (!inCache && layer.buffer) { + this.returnBufferToPool(layer.buffer, layer.width, layer.height); + } + return inCache; + }); + if (this.embeddedLayers.length < initialCount) { + console.log(`\u{1F9F9} Cleaned up ${initialCount - this.embeddedLayers.length} orphaned embedded layers`); + } + } + } + const frameValue = api.frame || this.frameCount || 0; + if (this.embeddedLayers) { + this.embeddedLayers.forEach((embeddedLayer, index) => { + if (embeddedLayer && embeddedLayer.kidlispInstance && embeddedLayer.buffer) { + if (embeddedLayer.timingContext) { + const timingCtx = embeddedLayer.timingContext; + const currentIndex = this.sequenceCounters.get(timingCtx.timingKey); + if (currentIndex !== void 0 && currentIndex !== timingCtx.argumentIndex) { + return; + } + } + const shouldEvaluate = true; + try { + this.renderSingleLayer(api, embeddedLayer, frameValue, shouldEvaluate); + } catch (error) { + console.error("Error rendering embedded layer:", error); + } + } + }); + } + } + // 🚀 OPTIMIZED: Determine if layer needs evaluation (dirty checking) + shouldLayerEvaluate(embeddedLayer, frameValue) { + if (!embeddedLayer.hasBeenEvaluated) { + return true; + } + if (embeddedLayer.lastFrameEvaluated === frameValue) { + return false; + } + const source = embeddedLayer.kidlispInstance.source || embeddedLayer.sourceCode || embeddedLayer.source || ""; + const hasTimingExpression = /\d+\.?\d*[s]\.\.\.?|\d+\.?\d*[s]!/.test(source); + const hasDynamicContent = hasTimingExpression || source.includes("frame") || source.includes("scroll") || source.includes("flip") || source.includes("clock") || source.includes("pen") || source.includes("mouse") || source.includes("key") || source.includes("touch") || source.includes("tap") || source.includes("random") || source.includes("spin") || source.includes("wiggle") || source.includes("fade") || source.includes("sort") || source.includes("noise"); + if (hasDynamicContent) { + return true; + } + if (source) { + return false; + } + return frameValue % 10 === 0; + } + // 🚀 OPTIMIZED: Render single layer with minimal overhead + renderSingleLayer(api, embeddedLayer, frameValue, shouldEvaluate) { + const currentScreenSize = `${api.screen?.width || 0}x${api.screen?.height || 0}`; + const timeSinceLastRender = performance.now() - (embeddedLayer.lastRenderTime || 0); + if (this.lastScreenSize && this.lastScreenSize !== currentScreenSize && timeSinceLastRender < 50) { + if (embeddedLayer.buffer && api.paste) { + this.pasteWithAlpha(api, embeddedLayer.buffer, embeddedLayer.x, embeddedLayer.y, embeddedLayer.alpha); + } + return; + } + embeddedLayer.lastRenderTime = performance.now(); + api.page(embeddedLayer.buffer); + if (!embeddedLayer.localFrameCount) { + embeddedLayer.localFrameCount = 0; + } + embeddedLayer.localFrameCount += 1; + embeddedLayer.kidlispInstance.frameCount = embeddedLayer.localFrameCount; + embeddedLayer.kidlispInstance.frameCounter = embeddedLayer.localFrameCount; + embeddedLayer.kidlispInstance.isEmbeddedLayer = true; + if (!embeddedLayer.kidlispInstance.timingStates) { + embeddedLayer.kidlispInstance.timingStates = /* @__PURE__ */ new Map(); + } + if (shouldEvaluate) { + const embeddedApi = this.getOptimizedLayerApi(embeddedLayer, api); + api.page(embeddedLayer.buffer); + api.screen.pixels = embeddedLayer.buffer.pixels; + embeddedApi.frame = embeddedLayer.localFrameCount; + embeddedApi.screen.pixels = embeddedLayer.buffer.pixels; + embeddedApi.width = embeddedLayer.width; + embeddedApi.height = embeddedLayer.height; + if (!embeddedLayer.firstLineColorApplied && embeddedLayer.kidlispInstance.firstLineColor) { + try { + api.wipe(embeddedLayer.kidlispInstance.firstLineColor); + } catch (err) { + console.warn( + "\u26A0\uFE0F embedded first-line wipe failed:", + err?.message + ); + } + embeddedLayer.firstLineColorApplied = true; + } + const localEnv = embeddedLayer.kidlispInstance.localEnv; + localEnv.frame = embeddedLayer.localFrameCount; + localEnv.scroll = frameValue % (embeddedLayer.width + embeddedLayer.height); + const scrollNodesInAST = Array.isArray(embeddedLayer.parsedCode) ? embeddedLayer.parsedCode.filter((node) => { + if (node && typeof node === "object" && node.type === "list" && node.value && node.value[0] && node.value[0].value === "scroll") { + return true; + } + if (Array.isArray(node)) { + if (node.length > 1 && typeof node[0] === "string" && /^\d*\.?\d+[s]\.\.\.?$/.test(node[0])) { + return node.slice(1).some((elem) => { + if (Array.isArray(elem) && elem.length > 0 && elem[0] === "scroll") { + return true; + } + return false; + }); + } + if (node.length > 0 && node[0] === "scroll") { + return true; + } + } + return false; + }).length : 0; + embeddedLayer.kidlispInstance.embeddedLayerWipedOnce = false; + embeddedLayer.kidlispInstance.evaluate( + embeddedLayer.parsedCode, + embeddedApi, + localEnv + ); + const layerCacheKey = `${embeddedLayer.source}_timing`; + if (this.embeddedLayerCache) { + this.embeddedLayerCache.set(layerCacheKey, { + frameCount: embeddedLayer.kidlispInstance.frameCount, + frameCounter: embeddedLayer.kidlispInstance.frameCounter, + lastSecondExecutions: embeddedLayer.kidlispInstance.lastSecondExecutions ? Array.from(embeddedLayer.kidlispInstance.lastSecondExecutions) : [], + sequenceCounters: Array.from(embeddedLayer.kidlispInstance.sequenceCounters || []), + timingStates: Array.from(embeddedLayer.kidlispInstance.timingStates || []), + randomState: embeddedLayer.kidlispInstance.randomState + }); + } + embeddedLayer.hasBeenEvaluated = true; + embeddedLayer.lastFrameEvaluated = frameValue; + } + api.page(this.layer0); + api.screen.pixels = this.layer0.pixels; + } + // 🚀 CACHE OPTIMIZED API: Minimal API object creation + getOptimizedLayerApi(embeddedLayer, api) { + const cacheKey = `${embeddedLayer.width}x${embeddedLayer.height}`; + const globalEnv = this.getGlobalEnv(); + const embeddedApi = { + // Include KidLisp functions from global environment + ...globalEnv, + // Include system context for functions like 'painting' + system: api.system, + // Direct passthrough of most functions to main API + line: (...args) => api.line(...args), + ink: (...args) => api.ink(...args), + wipe: (...args) => { + const argsStr = args.length > 0 ? JSON.stringify(args) : ""; + const hasDynamicColor = argsStr.includes("rainbow") || argsStr.includes("zebra"); + if (args.length > 0 && embeddedLayer.hasBeenEvaluated && !hasDynamicColor) { + if (!embeddedLayer._wipeCache) embeddedLayer._wipeCache = {}; + const cache = embeddedLayer._wipeCache; + if (cache.key === argsStr && cache.snapshot && cache.width === embeddedLayer.width && cache.height === embeddedLayer.height) { + embeddedLayer.buffer.pixels.set(cache.snapshot); + return; + } + api.wipe(...args); + const px = embeddedLayer.buffer.pixels; + if (px && px.length > 0) { + cache.snapshot = new Uint8ClampedArray(px); + cache.key = argsStr; + cache.width = embeddedLayer.width; + cache.height = embeddedLayer.height; + } + return; + } + api.wipe(...args); + }, + circle: (...args) => api.circle(...args), + tri: (...args) => api.tri(...args), + box: (...args) => api.box(...args), + point: (...args) => api.point(...args), + poly: (...args) => api.poly(...args), + paste: (...args) => api.paste(...args), + stamp: (...args) => api.stamp(...args), + write: (...args) => api.write(...args), + flood: (...args) => api.flood(...args), + // IMPORTANT: For scroll, call the main API function directly + // No complex wrapper system - just execute scroll immediately + scroll: (...args) => { + if (typeof api.scroll === "function") { + return api.scroll(...args); + } + }, + // IMPORTANT: Add missing transform functions that embedded pieces need + spin: (...args) => { + if (typeof api.spin === "function") { + return api.spin(...args); + } + }, + resetSpin: (...args) => { + if (typeof api.resetSpin === "function") { + return api.resetSpin(...args); + } + }, + smoothspin: (...args) => { + if (typeof api.smoothspin === "function") { + return api.smoothspin(...args); + } + }, + zoom: (...args) => { + if (typeof api.zoom === "function") { + return api.zoom(...args); + } + }, + flip: (...args) => { + if (typeof api.flip === "function") { + return api.flip(...args); + } + }, + blur: (...args) => { + if (typeof api.blur === "function") { + return api.blur(...args); + } + }, + contrast: (...args) => { + if (typeof api.contrast === "function") { + return api.contrast(...args); + } + }, + // Screen properties + screen: { + width: embeddedLayer.width, + height: embeddedLayer.height, + pixels: null + // Updated per render + }, + width: embeddedLayer.width, + height: embeddedLayer.height, + frame: 0, + // Updated per render + // 🚨 CRITICAL: Include clock API for timing expressions in embedded layers + clock: api.clock, + // 🚨 FIX: Override globalEnv.fps from the spread above. + // globalEnv.fps calls api.fps(targetFps) — if api.fps is globalEnv.fps (from the + // spread), it recurses with one arg causing fps(number, undefined) → args crash. + // Use the BIOS api.fps directly so the call resolves correctly. + fps: typeof api.fps === "function" ? api.fps : void 0 + }; + return embeddedApi; + } + // Check if an embedded layer should execute this frame based on its timing patterns + shouldLayerExecuteThisFrame(api, embeddedLayer) { + if (!embeddedLayer.sourceCode) { + return true; + } + const timingExpressions = this.extractTimingExpressions(embeddedLayer.sourceCode); + if (timingExpressions.length === 0) { + return true; + } + for (const timingExpr of timingExpressions) { + if (this.evaluateTimingExpression(api, timingExpr)) { + return true; + } + } + return false; + } + // Extract timing expressions like "3s...", "0.25s..." from source code + extractTimingExpressions(sourceCode) { + const expressions = []; + const timingRegex = /\(\s*(\d+(?:\.\d+)?[s]\.\.\.)/g; + let match; + while ((match = timingRegex.exec(sourceCode)) !== null) { + expressions.push(match[1]); + } + return expressions; + } + // 🚀 EMBEDDED ENVIRONMENT: Simplified functions for embedded layers (no deferred execution) + getSimpleEmbeddedEnv(api) { + return { + // Time function for clock expressions (provides both clock.time and time) + time: () => { + if (api.clock && api.clock.time) { + return api.clock.time(); + } + return /* @__PURE__ */ new Date(); + } + }; + } + // Evaluate a timing expression using the parent's timing state + evaluateTimingExpression(api, timingExpr) { + const match = timingExpr.match(/^(\d+(?:\.\d+)?)s\.\.\.$/); + if (!match) { + return true; + } + const interval = parseFloat(match[1]); + const timingKey = timingExpr + "_1"; + const clockResult = api.clock?.time(); + if (!clockResult) { + return true; + } + const currentTimeMs = clockResult.getTime ? clockResult.getTime() : Date.now(); + const currentTime = currentTimeMs / 1e3; + if (!this.lastSecondExecutions.hasOwnProperty(timingKey)) { + this.lastSecondExecutions[timingKey] = currentTime; + return true; + } + const lastExecution = this.lastSecondExecutions[timingKey]; + const diff = currentTime - lastExecution; + if (diff >= interval) { + this.lastSecondExecutions[timingKey] = currentTime; + return true; + } + return false; + } + // Check if a timing pattern should be active (without executing the layer) + checkTimingActive(api, timingPattern) { + return true; + } + // Extract timing pattern from embedded layer source code + extractTimingPattern(sourceCode) { + if (!sourceCode) return null; + const timingMatch = sourceCode.match(/(\d+(?:\.\d+)?[s]\.\.\.)/); + return timingMatch ? timingMatch[1] : null; + } + // 🚀 PROGRAMMATIC EMBEDDED LAYER CREATION + // Create an embedded layer programmatically (for JavaScript API calls) + // This uses the same pipeline as $code embedded layers for consistency + createProgrammaticEmbeddedLayer(source, x, y, width2, height2, options = {}) { + const cacheId = `prog_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + console.log(`\u{1F527} Creating programmatic embedded layer: ${cacheId}`, { + bounds: `${x},${y} ${width2}x${height2}`, + sourceLength: source.length, + options + }); + if (!this.embeddedLayers) { + this.embeddedLayers = []; + } + const kidlispInstance = new _KidLisp(); + kidlispInstance.isEmbeddedContext = true; + kidlispInstance.isNestedInstance = options.isNestedInstance !== false; + kidlispInstance.embeddedContext = { x, y, width: width2, height: height2 }; + let parsedCode; + try { + parsedCode = kidlispInstance.parse(source); + } catch (error) { + console.error(`\u274C Failed to parse programmatic layer ${cacheId}:`, error); + return null; + } + const buffer = { + width: width2, + height: height2, + pixels: new Uint8ClampedArray(width2 * height2 * 4) + }; + const embeddedLayer = { + id: `prog_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + // Unique ID for each layer + cacheId, + x, + y, + width: width2, + height: height2, + buffer, + source, + parsedCode, + kidlispInstance, + localFrameCount: 0, + hasBeenEvaluated: false, + lastFrameEvaluated: -1, + alpha: options.alpha || 255, + timingPattern: this.extractTimingPattern(source), + isProgrammatic: true + // Mark as programmatically created + }; + if (!this.embeddedLayers) { + console.log("\u{1F6A8} embeddedLayers was null during programmatic push, reinitializing to empty array"); + this.embeddedLayers = []; + } + this.embeddedLayers.push(embeddedLayer); + console.log(`\u2705 Created programmatic embedded layer ${cacheId}, total layers: ${this.embeddedLayers.length}`); + return cacheId; + } + // Remove a programmatic embedded layer + removeProgrammaticEmbeddedLayer(cacheId) { + if (!this.embeddedLayers) return false; + const initialLength = this.embeddedLayers.length; + this.embeddedLayers = this.embeddedLayers.filter( + (layer) => !(layer.isProgrammatic && layer.cacheId === cacheId) + ); + const removed = this.embeddedLayers.length < initialLength; + if (removed) { + console.log(`\u{1F5D1}\uFE0F Removed programmatic embedded layer ${cacheId}`); + } + return removed; + } + // Update a single embedded layer (for nested embedding) + // Returns true if the layer should be rendered/visible this frame + updateEmbeddedLayer(api, embeddedLayer) { + if (!embeddedLayer || !embeddedLayer.kidlispInstance || !embeddedLayer.buffer) { + console.log("\u274C Cannot update embedded layer - missing components:", { + hasLayer: !!embeddedLayer, + hasInstance: !!embeddedLayer?.kidlispInstance, + hasBuffer: !!embeddedLayer?.buffer + }); + return false; + } + if (!api || typeof api.page !== "function" || !api.screen) { + console.error(`\u274C Invalid API object for embedded layer ${embeddedLayer.cacheId}:`, { + hasApi: !!api, + hasPageMethod: typeof api?.page === "function", + hasScreen: !!api?.screen + }); + embeddedLayer.failed = true; + embeddedLayer.failureCount = (embeddedLayer.failureCount || 0) + 1; + if (embeddedLayer.failureCount >= 3) { + console.warn(`\u26A0\uFE0F Disabling failed embedded layer ${embeddedLayer.cacheId} after ${embeddedLayer.failureCount} failures`); + embeddedLayer.disabled = true; + } + return false; + } + if (!embeddedLayer.parsedCode || embeddedLayer.parsedCode.length === 0) { + console.warn(`\u26A0\uFE0F Empty parsed code for embedded layer ${embeddedLayer.cacheId}`); + return false; + } + if (embeddedLayer.disabled) { + return false; + } + let didRender = false; + const originalPage = api?.screen ? { + width: api.screen.width, + height: api.screen.height, + pixels: api.screen.pixels + } : null; + try { + api.page(embeddedLayer.buffer); + if (!embeddedLayer.localFrameCount) { + embeddedLayer.localFrameCount = 0; + } + embeddedLayer.localFrameCount += 1; + embeddedLayer.kidlispInstance.frameCount = embeddedLayer.localFrameCount; + embeddedLayer.kidlispInstance.frameCounter = embeddedLayer.localFrameCount; + embeddedLayer.kidlispInstance.isEmbeddedLayer = true; + if (!embeddedLayer.kidlispInstance.timingStates) { + embeddedLayer.kidlispInstance.timingStates = /* @__PURE__ */ new Map(); + } + const nestedApiCacheKey = `nested_${embeddedLayer.cacheId}_${embeddedLayer.width}x${embeddedLayer.height}`; + let embeddedApi = this.embeddedApiCache.get(nestedApiCacheKey); + if (!embeddedApi) { + const globalEnv = this.getGlobalEnv(); + embeddedApi = { + ...globalEnv, + ...api, + system: api.system, + // Ensure system context is available in embedded layers + screen: { + ...api.screen, + width: embeddedLayer.width, + height: embeddedLayer.height, + pixels: embeddedLayer.buffer.pixels + }, + // 🔧 CRITICAL: Include page function for nested embeds + page: api.page, + // Execute drawing commands directly to the embedded buffer and track rendering + line: (...args) => { + didRender = true; + return api.line(...args); + }, + ink: (...args) => { + didRender = true; + return api.ink(...args); + }, + wipe: (...args) => { + didRender = true; + const argsStr = args.length > 0 ? JSON.stringify(args) : ""; + const hasDynamicColor = argsStr.includes("rainbow") || argsStr.includes("zebra"); + if (args.length > 0 && embeddedLayer.hasBeenEvaluated && !hasDynamicColor) { + if (!embeddedLayer._wipeCache) embeddedLayer._wipeCache = {}; + const cache = embeddedLayer._wipeCache; + if (cache.key === argsStr && cache.snapshot && cache.width === embeddedLayer.width && cache.height === embeddedLayer.height) { + embeddedLayer.buffer.pixels.set(cache.snapshot); + return; + } + api.wipe(...args); + const px = embeddedLayer.buffer.pixels; + if (px && px.length > 0) { + cache.snapshot = new Uint8ClampedArray(px); + cache.key = argsStr; + cache.width = embeddedLayer.width; + cache.height = embeddedLayer.height; + } + return; + } + api.wipe(...args); + }, + circle: (...args) => { + didRender = true; + return api.circle(...args); + }, + tri: (...args) => { + didRender = true; + return api.tri(...args); + }, + box: (...args) => { + didRender = true; + return api.box(...args); + }, + point: (...args) => { + didRender = true; + return api.point(...args); + }, + poly: (...args) => { + didRender = true; + return api.poly(...args); + }, + paste: (...args) => { + didRender = true; + return api.paste(...args); + }, + stamp: (...args) => { + didRender = true; + return api.stamp(...args); + }, + write: (...args) => { + didRender = true; + return api.write(...args); + }, + flood: (...args) => { + didRender = true; + return api.flood(...args); + }, + fade: (...args) => { + didRender = true; + return api.fade(...args); + } + }; + this.embeddedApiCache.set(nestedApiCacheKey, embeddedApi); + } + const frameValue = api.frame || this.frameCount || 0; + const smoothFrameValue = embeddedLayer.localFrameCount; + embeddedApi.frame = smoothFrameValue; + embeddedApi.width = embeddedLayer.width; + embeddedApi.height = embeddedLayer.height; + embeddedApi.screen.width = embeddedLayer.width; + embeddedApi.screen.height = embeddedLayer.height; + embeddedApi.screen.pixels = embeddedLayer.buffer.pixels; + if (!embeddedLayer.firstLineColorApplied && embeddedLayer.kidlispInstance.firstLineColor) { + try { + api.wipe(embeddedLayer.kidlispInstance.firstLineColor); + } catch (err) { + console.warn( + "\u26A0\uFE0F embedded first-line wipe failed:", + err?.message + ); + } + embeddedLayer.firstLineColorApplied = true; + } + const localEnv = embeddedLayer.kidlispInstance.localEnv; + localEnv.width = embeddedLayer.width; + localEnv.height = embeddedLayer.height; + localEnv.frame = smoothFrameValue; + localEnv["width/2"] = embeddedLayer.width / 2; + localEnv["height/2"] = embeddedLayer.height / 2; + const modScrollValue = frameValue % (embeddedLayer.width + embeddedLayer.height); + const embeddedEnv = localEnv; + embeddedEnv.scroll = modScrollValue; + embeddedLayer.kidlispInstance.embeddedLayerWipedOnce = false; + embeddedLayer.kidlispInstance.evaluate( + embeddedLayer.parsedCode, + embeddedApi, + embeddedEnv + ); + embeddedLayer.hasBeenEvaluated = true; + embeddedLayer.lastFrameEvaluated = frameValue; + } catch (error) { + console.error(`\u274C Error updating nested embedded layer ${embeddedLayer.cacheId}:`, error); + embeddedLayer.failed = true; + embeddedLayer.failureCount = (embeddedLayer.failureCount || 0) + 1; + embeddedLayer.lastError = error.message; + if (embeddedLayer.failureCount >= 3) { + console.warn(`\u26A0\uFE0F Disabling failed embedded layer ${embeddedLayer.cacheId} after ${embeddedLayer.failureCount} failures`); + embeddedLayer.disabled = true; + } + } finally { + if (originalPage && typeof api?.page === "function") { + try { + api.page(originalPage); + if (api.screen) { + api.screen.pixels = originalPage.pixels; + } + } catch (restoreError) { + console.error("\u274C Error switching back to original page:", restoreError); + } + } + } + return didRender; + } + // Manual pixel compositing for embedded layers (optimized) + compositeEmbeddedLayer(api, embeddedLayer) { + if (!api || !embeddedLayer?.buffer) { + return; + } + const destinationPage = api.screen ? { + width: api.screen.width, + height: api.screen.height, + pixels: api.screen.pixels + } : null; + if (destinationPage && typeof api.page === "function") { + api.page(destinationPage); + api.screen.pixels = destinationPage.pixels; + } + const alpha = typeof embeddedLayer.alpha === "number" ? embeddedLayer.alpha : 255; + if (alpha < 255) { + this.preserveLayer0NextFrame = true; + } + const destX = Math.round(embeddedLayer.x || 0); + const destY = Math.round(embeddedLayer.y || 0); + if (destX === 0 && destY === 0 && alpha < 255) { + } + this.pasteWithAlpha( + api, + embeddedLayer.buffer, + destX, + destY, + alpha + ); + } +}; +var globalKidLispInstance = null; +function ensureGlobalInstance() { + if (!globalKidLispInstance) { + globalKidLispInstance = new KidLisp(); + } + return globalKidLispInstance; +} +function module(source, isLispFile = false) { + return ensureGlobalInstance().module(source, isLispFile); +} +function parse(program) { + const lisp = new KidLisp(); + return lisp.parse(program); +} +function slideUpdate(source) { + if (globalKidLispInstance) { + globalKidLispInstance.slideUpdate(source); + } else { + console.warn("\u{1F39A}\uFE0F slideUpdate called but no global KidLisp instance exists"); + } +} +function updateKidLispAudio(audioData) { + if (globalKidLispInstance) { + globalKidLispInstance.updateAudioGlobals(audioData); + } +} +function getGlobalInstance() { + return globalKidLispInstance; +} +function evaluate(parsed, api = {}) { + const lisp = new KidLisp(); + return lisp.evaluate(parsed, api); +} +function isValidRGBComponent(value) { + const num = parseInt(value, 10); + return !isNaN(num) && num >= 0 && num <= 255; +} +function isValidRGBString(text) { + if (!text || typeof text !== "string") return false; + const trimmed = text.trim(); + const spaceSeparated = trimmed.split(/\s+/); + if (spaceSeparated.length === 3 || spaceSeparated.length === 4) { + return spaceSeparated.every(isValidRGBComponent); + } + const commaSeparated = trimmed.split(/,\s*/); + if (commaSeparated.length === 3 || commaSeparated.length === 4) { + return commaSeparated.every(isValidRGBComponent); + } + return false; +} +function parseRGBString(text) { + if (!isValidRGBString(text)) return null; + const trimmed = text.trim(); + const spaceSeparated = trimmed.split(/\s+/); + if ((spaceSeparated.length === 3 || spaceSeparated.length === 4) && spaceSeparated.every(isValidRGBComponent)) { + return spaceSeparated.map((v2) => parseInt(v2, 10)); + } + const commaSeparated = trimmed.split(/,\s*/); + if ((commaSeparated.length === 3 || commaSeparated.length === 4) && commaSeparated.every(isValidRGBComponent)) { + return commaSeparated.map((v2) => parseInt(v2, 10)); + } + return null; +} +function isKidlispSource(text) { + if (!text) return false; + if (text.startsWith("$") && text.length > 1) { + const cacheId = text.slice(1); + if (/^[0-9A-Za-z]{4,12}$/.test(cacheId)) { + const hasDigit = /\d/.test(cacheId); + const hasMixedCase = /[a-z]/.test(cacheId) && /[A-Z]/.test(cacheId); + if (hasDigit || hasMixedCase) { + return true; + } + } + } + if (text.startsWith("(")) { + return true; + } + if (text.includes("//") || text.includes("function ") || text.includes("const ") || text.includes("let ") || text.includes("var ") || text.includes("=>") || text.includes("console.") || text.includes("import ") || text.includes("export ") || text.includes(".mjs") || text.includes(".js") || text.match(/[a-zA-Z_$]\w*\.\w+/)) { + return false; + } + const trimmedText = text.trim(); + const loweredTrimmedText = trimmedText.toLowerCase(); + if (loweredTrimmedText.startsWith(";")) { + return true; + } + if (loweredTrimmedText.startsWith("share")) { + return false; + } + if (text.includes("\n")) { + const lines = text.split(/\r?\n/).map((l2) => l2.trim()).filter(Boolean); + if (lines.length === 0) return false; + if (lines.every((l2) => l2.startsWith(";"))) { + return true; + } + const kidlispLineFunctions = /* @__PURE__ */ new Set([ + "wipe", + "ink", + "line", + "box", + "flood", + "circle", + "write", + "paste", + "stamp", + "point", + "poly", + "print", + "debug", + "random", + "sin", + "cos", + "tan", + "floor", + "ceil", + "round", + "noise", + "choose", + "overtone", + "rainbow", + "zebra", + "mic", + "amplitude", + "melody", + "speaker", + "resolution", + "lines", + "wiggle", + "shape", + "scroll", + "flip", + "spin", + "resetspin", + "smoothspin", + "sort", + "zoom", + "blur", + "contrast", + "pan", + "unpan", + "mask", + "unmask", + "steal", + "putback", + "label", + "len", + "now", + "die", + "tap", + "draw", + "not", + "range", + "mul", + "log", + "no", + "yes", + "fade", + "repeat", + "jump", + "def", + "later", + "frame" + ]); + const looksLikeKidlispLine = (line2) => { + if (!line2 || line2.startsWith(";")) return false; + if (line2.startsWith("(")) return true; + const lowered = line2.toLowerCase(); + if (lowered.startsWith("fade:") || /^c\d+$/.test(lowered) || /^p\d+$/.test(lowered) || lowered === "rainbow" || lowered === "zebra" || cssColors2 && cssColors2[lowered]) { + return true; + } + const withSpaces2 = lowered.replace(/_/g, " "); + if (isValidRGBString(withSpaces2)) { + return true; + } + const firstToken = lowered.split(/\s+/)[0] || ""; + const cleanedToken = firstToken.replace(/^[^a-z0-9]+/i, "").replace(/[^a-z0-9]+$/i, ""); + return kidlispLineFunctions.has(cleanedToken); + }; + if (lines.some(looksLikeKidlispLine)) { + return true; + } + return false; + } + if (loweredTrimmedText.startsWith("share")) { + return false; + } + if (trimmedText.startsWith("fade:") || trimmedText.match(/^c\d+$/) || trimmedText.match(/^p\d+$/) || // Pattern codes like p0, p1, etc. + cssColors2[trimmedText] || trimmedText === "rainbow" || trimmedText === "zebra") { + return true; + } + const withSpaces = trimmedText.replace(/_/g, " "); + if (isValidRGBString(withSpaces)) { + return true; + } + if (text.includes(",")) { + if (VERBOSE) console.log("\u{1F50D} [KidLisp Detection] Comma-separated text:", text); + const kidlispFunctions = [ + "wipe", + "ink", + "line", + "box", + "flood", + "circle", + "write", + "paste", + "stamp", + "point", + "poly", + "print", + "debug", + "random", + "sin", + "cos", + "tan", + "floor", + "ceil", + "round", + "noise", + "choose", + "overtone", + "rainbow", + "zebra", + "mic", + "amplitude", + "melody", + "speaker", + "resolution", + "lines", + "wiggle", + "shape", + "scroll", + "flip", + "spin", + "resetSpin", + "smoothspin", + "sort", + "zoom", + "blur", + "contrast", + "pan", + "unpan", + "mask", + "unmask", + "steal", + "putback", + "label", + "len", + "now", + "die", + "tap", + "draw", + "not", + "range", + "mul", + "log", + "no", + "yes", + "fade", + "repeat", + "jump" + ]; + const parts = text.split(",").map((part) => part.trim()).filter(Boolean); + if (VERBOSE) console.log("\u{1F50D} [KidLisp Detection] Parts:", parts); + const analyzePart = (part) => { + const normalized = part.replace(/^[{\[(]+/, "").replace(/[}\])]+$/, "").trim(); + if (!normalized) return { isFunction: false, isColor: false, isNumber: false, isRGB: false }; + const firstTokenMatch = normalized.match(/^[^\s]+/); + const firstToken = firstTokenMatch ? firstTokenMatch[0].toLowerCase() : ""; + const firstTokenStripped = firstToken.replace(/^[^a-z0-9]+/i, ""); + const cleanedToken = firstTokenStripped.replace(/[^a-z0-9:._-]+$/i, ""); + if (VERBOSE) console.log(`\u{1F50D} [KidLisp Detection] Analyzing part: "${part}" -> normalized: "${normalized}" -> cleanedToken: "${cleanedToken}"`); + const isFunction = kidlispFunctions.includes(cleanedToken) || cleanedToken.startsWith("(") && kidlispFunctions.includes(cleanedToken.slice(1)); + const isColor = !!(cssColors2 && cssColors2[cleanedToken]) || /^c\d+$/.test(cleanedToken) || cleanedToken.startsWith("fade:"); + const isNumber = /^-?\d+(\.\d+)?$/.test(cleanedToken); + const isRGB = isValidRGBString(cleanedToken.replace(/_/g, " ")); + const isPaintingCode = normalized.startsWith("#") && /^#[0-9A-Za-z]{1,8}$/.test(normalized) && (() => { + const codepart = normalized.substring(1); + const isHexColor = /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{4}$|^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{8}$/.test(codepart); + return !isHexColor; + })(); + if (VERBOSE) console.log(`\u{1F50D} [KidLisp Detection] isFunction: ${isFunction}, isColor: ${isColor}, isNumber: ${isNumber}, isRGB: ${isRGB}, isPaintingCode: ${isPaintingCode}`); + return { isFunction, isColor, isNumber, isRGB, isPaintingCode }; + }; + const analyses = parts.map(analyzePart); + const kidParts = analyses.filter(({ isFunction, isColor, isNumber, isRGB, isPaintingCode }) => isFunction || isColor || isNumber || isRGB || isPaintingCode); + const functionParts = analyses.filter(({ isFunction }) => isFunction); + if (VERBOSE) console.log(`\u{1F50D} [KidLisp Detection] kidParts: ${kidParts.length}, functionParts: ${functionParts.length}, total parts: ${parts.length}`); + const result = kidParts.length > 0 && (functionParts.length > 0 || kidParts.length >= 2 || kidParts.length / parts.length >= 0.75); + if (VERBOSE) console.log(`\u{1F50D} [KidLisp Detection] Final result: ${result}`); + if (result) { + return true; + } + } + if (text.includes("\xA7")) { + const decoded = text.replace(/_/g, " ").replace(/§/g, "\n"); + if (decoded.startsWith("(") || decoded.includes("\n")) { + return true; + } + } + if (text.includes("_") && (text.includes("__") || // Multiple consecutive underscores (likely encoded spaces) + text.match(/\b(wipe|ink|line|box|def|later)_/))) { + const decoded = text.replace(/_/g, " ").replace(/§/g, "\n"); + if (decoded.startsWith("(") || decoded.includes("\n")) { + return true; + } + } + return false; +} +function encodeKidlispForUrl(source) { + const isKidlisp = isKidlispSource(source); + if (!isKidlisp) { + return source; + } + const encoded = source.replace(/ /g, "_").replace(/\n/g, "\xA7").replace(/%/g, "\xA4").replace(/;/g, "\xA8").replace(/#/g, "%23"); + return encoded; +} +function decodeKidlispFromUrl(encoded) { + let decoded = encoded.replace(/_/g, " ").replace(/%C2%A7/g, "\n").replace(/%C2%A4/g, "%").replace(/%C2%A8/g, ";").replace(/%23/g, "#").replace(/§/g, "\n").replace(/¤/g, "%").replace(/¨/g, ";").replace(/%28/g, "(").replace(/%29/g, ")").replace(/%2E/g, ".").replace(/%22/g, '"').replace(/%3B/g, ";"); + if (!encoded.startsWith("prompt~") && isKidlispSource(decoded)) { + decoded = decoded.replace(/~/g, "\n"); + } + return decoded; +} +function shouldUseSyntaxHighlighting(promptText) { + if (!promptText || typeof promptText !== "string") return false; + const trimmed = promptText.trim(); + if (isKidlispSource(promptText)) { + return true; + } + const nopaintBrushes = [ + "box", + "shape", + "line", + "rect", + "fill", + "smear", + "spray", + "plot", + "oval", + "circle", + "handprint", + "word", + "paste", + "stamp" + ]; + const firstWord = trimmed.split(/\s+/)[0].toLowerCase(); + if (nopaintBrushes.includes(firstWord)) { + return true; + } + return false; +} +function isActualKidLisp(promptText) { + if (!promptText || typeof promptText !== "string") return false; + return isKidlispSource(promptText); +} +function isPromptInKidlispMode(promptText) { + if (!promptText || typeof promptText !== "string") return false; + return shouldUseSyntaxHighlighting(promptText); +} +async function fetchMultipleCachedCodes(codeArray, api = null) { + if (!codeArray || codeArray.length === 0) { + return {}; + } + const isObjktMode = checkPackMode(); + if (isObjktMode) { + return {}; + } + console.log("\u{1F527} fetchMultipleCachedCodes called with:", codeArray, "- attempting batch HTTPS fetch"); + const codesParam = encodeURIComponent(codeArray.join(",")); + const fullUrl = `/api/store-kidlisp?codes=${codesParam}`; + console.log("\u{1F310} Batch fetch URL:", fullUrl); + try { + console.log("\u{1F310} About to fetch batch..."); + const response = await fetch(fullUrl); + console.log("\u{1F310} Batch fetch completed, response received"); + console.log("\u{1F310} Response details:", { + url: response.url, + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + if (response.ok) { + const data = await response.json(); + console.log(`\u2705 Successfully loaded batch of ${codeArray.length} codes`, data.summary); + const sources = {}; + Object.entries(data.results).forEach(([code2, result]) => { + if (result && result.source) { + sources[code2] = result.source; + } else { + console.warn(`\u274C No source found for code: ${code2}`); + sources[code2] = null; + } + }); + return sources; + } else { + console.error(`\u274C Failed to load batch of cached codes: HTTP ${response.status}: ${response.statusText}`); + return {}; + } + } catch (error) { + console.error(`\u274C Network error loading batch of cached codes`, error); + return {}; + } +} +async function fetchCachedCode(nanoidCode, api = null) { + const isObjktMode = checkPackMode(); + if (isObjktMode) { + return null; + } + const tryFetch = async (url, isProduction = false) => { + return new Promise(async (resolve) => { + if (typeof process !== "undefined" && process.versions && process.versions.node) { + const https = await import("https"); + const options = { rejectUnauthorized: false }; + https.get(url, options, (res) => { + let data = ""; + res.on("data", (chunk) => { + data += chunk; + }); + res.on("end", () => { + try { + const parsed = JSON.parse(data); + if (parsed && parsed.source) { + resolve(parsed.source); + } else { + console.error(`\u274C Failed to load cached code: ${nanoidCode} - No source in response from ${url}`, parsed); + resolve(null); + } + } catch (err) { + console.error(`\u274C Failed to parse JSON for cached code: ${nanoidCode} from ${url}`, err, "Raw response:", data); + resolve(null); + } + }); + }).on("error", (err) => { + console.error(`\u274C Network error loading cached code: ${nanoidCode} from ${url}`, err); + resolve(null); + }); + } else { + fetch(url).then((response) => { + if (response.ok) { + return response.json().then((data) => { + if (data && data.source) { + resolve(data.source); + } else { + console.error(`\u274C Failed to load cached code: ${nanoidCode} - No source in response from ${url}`, data); + resolve(null); + } + }); + } else { + if (response.status !== 404) { + console.error(`\u274C Failed to load cached code: ${nanoidCode} - HTTP ${response.status}: ${response.statusText} from ${url}`); + } + resolve(null); + } + }).catch((error) => { + console.warn(`\u26A0\uFE0F Network error loading embedded piece $${nanoidCode}:`, error.message || error); + resolve(null); + }); + } + }); + }; + if (typeof window !== "undefined" && window.acSPIDER) { + const productionUrl2 = `https://aesthetic.computer/api/store-kidlisp?code=${nanoidCode}`; + const productionSource2 = await tryFetch(productionUrl2, true); + return productionSource2; + } + const localUrl = `/api/store-kidlisp?code=${nanoidCode}`; + const localSource = await tryFetch(localUrl, false); + if (localSource) { + return localSource; + } + const productionUrl = `https://aesthetic.computer/api/store-kidlisp?code=${nanoidCode}`; + const productionSource = await tryFetch(productionUrl, true); + return productionSource; +} +async function fetchKidlispMetadata(nanoidCode) { + const isObjktMode = checkPackMode(); + if (isObjktMode) return null; + const tryFetchMeta = async (url) => { + try { + const response = await fetch(url); + if (response.ok) { + const data = await response.json(); + if (data && data.source) { + return { + source: data.source, + handle: data.handle || null, + when: data.when || null, + hits: data.hits || 0, + kept: data.kept || null + }; + } + } + } catch (error) { + console.error(`\u274C Failed to fetch metadata: ${nanoidCode}`, error); + } + return null; + }; + const localUrl = `/api/store-kidlisp?code=${nanoidCode}`; + const localMeta = await tryFetchMeta(localUrl); + if (localMeta) return localMeta; + const productionUrl = `https://aesthetic.computer/api/store-kidlisp?code=${nanoidCode}`; + return await tryFetchMeta(productionUrl); +} +function getSyntaxHighlightingColors(source) { + if (!source) { + return [{ type: "default", r: 220, g: 60, b: 60, weight: 1 }]; + } + const tokens = tokenize(source); + if (tokens.length === 0) { + return [{ type: "default", r: 220, g: 60, b: 60, weight: 1 }]; + } + const tempInstance = new KidLisp(); + tempInstance.isEditMode = true; + const colors = tokens.map((token) => { + const weight = Math.max(0.01, token.length / source.length); + const colorStr = tempInstance.getTokenColor(token, tokens, tokens.indexOf(token)); + let r2, g, b2; + if (colorStr.includes(",")) { + const parts = colorStr.split(",").map((p) => parseInt(p.trim())); + r2 = parts[0] || 220; + g = parts[1] || 60; + b2 = parts[2] || 60; + } else { + switch (colorStr) { + case "cyan": + case "teal": + r2 = 64; + g = 224; + b2 = 208; + break; + case "lime": + r2 = 50; + g = 205; + b2 = 50; + break; + case "green": + r2 = 34; + g = 139; + b2 = 34; + break; + case "yellow": + r2 = 255; + g = 255; + b2 = 0; + break; + case "orange": + r2 = 255; + g = 165; + b2 = 0; + break; + case "purple": + r2 = 128; + g = 0; + b2 = 128; + break; + case "magenta": + r2 = 255; + g = 0; + b2 = 255; + break; + case "red": + r2 = 255; + g = 0; + b2 = 0; + break; + case "gray": + case "grey": + r2 = 128; + g = 128; + b2 = 128; + break; + case "white": + r2 = 255; + g = 255; + b2 = 255; + break; + default: + r2 = 220; + g = 80; + b2 = 80; + } + } + let type = "default"; + if (token === "(" || token === ")" || token === ",") { + type = "punctuation"; + } else if (/^-?\d+(\.\d+)?$/.test(token)) { + type = "number"; + } else if (token.startsWith('"') && token.endsWith('"')) { + type = "string"; + } else if (token.startsWith(";")) { + type = "comment"; + } else if (["+", "-", "*", "/", "%", "mod", "=", ">", "<", ">=", "<=", "abs", "sqrt", "min", "max"].includes(token)) { + type = "operator"; + } else if (["def", "if", "cond", "later", "once", "lambda", "let", "do", "repeat"].includes(token)) { + type = "special"; + } else { + type = "function"; + } + return { type, r: r2, g, b: b2, weight, token }; + }); + return colors; +} + +// public/aesthetic.computer/lib/parse.mjs +var LEGITIMATE_PARAMS = [ + "icon", + "preview", + "signup", + "supportSignUp", + "success", + "code", + "supportForgotPassword", + "message", + "vscode", + "nogap", + "nolabel", + "shellhtml", + "noboot", + "density", + "zoom", + "duration", + "session-aesthetic", + "session-sotce", + "notice", + "tv", + "highlight", + "daw", + "width", + "height" +]; +var LEGITIMATE_PARAM_SET = new Set( + LEGITIMATE_PARAMS.map((param) => param.toLowerCase()) +); +var AUTH0_PARAMS_TO_STRIP = ["state", "code", "error", "error_description"]; +function isInsideParentheses(text, index) { + let depth = 0; + for (let i2 = 0; i2 < index; i2++) { + if (text[i2] === "(") { + depth += 1; + } else if (text[i2] === ")" && depth > 0) { + depth -= 1; + } + } + return depth > 0; +} +function shouldTreatAsQuery(text, questionIndex) { + if (questionIndex <= 0 || questionIndex >= text.length) return false; + const prevChar = text[questionIndex - 1]; + const nextChar = text[questionIndex + 1]; + if (prevChar === "~" || nextChar === "~") return false; + if (isInsideParentheses(text, questionIndex)) return false; + const rawCandidate = text.slice(questionIndex + 1); + if (!rawCandidate) return false; + const candidate = rawCandidate.replace(/^~+/, ""); + if (!candidate) return false; + const firstChunk = candidate.split(/[~&#]/)[0] || ""; + if (!firstChunk) return false; + const [key] = firstChunk.split("="); + const loweredKey = key.toLowerCase(); + if (LEGITIMATE_PARAM_SET.has(loweredKey) || AUTH0_PARAMS_TO_STRIP.includes(loweredKey)) { + return true; + } + if (firstChunk.includes("=")) return true; + if (/^[a-z0-9_-]+$/i.test(firstChunk) && firstChunk.length > 1) { + return true; + } + return false; +} +function parse2(text, location2 = self?.location) { + let path, host, params, search, hash; + let externalPath; + if (text.startsWith("https") && (text.endsWith(".mjs") || text.endsWith(".lisp") || text.endsWith(".lua"))) { + const url = new URL(text); + location2 = { hostname: url.hostname, port: url.port }; + externalPath = url.pathname.split("/").slice(0, -1).join("/").slice(1); + text = text.split("https://")[1].split(/\.mjs|\.lisp|\.lua/)[0].split("/").pop(); + } + text = text.trim(); + if (text.startsWith("prompt~")) { + let promptContent = text.slice(7); + const qIdx = promptContent.indexOf("?"); + let promptSearch; + if (qIdx >= 0 && shouldTreatAsQuery(promptContent, qIdx)) { + promptSearch = promptContent.slice(qIdx + 1); + promptContent = promptContent.slice(0, qIdx); + } + let decodedContent = decodeKidlispFromUrl(promptContent); + if (!isKidlispSource(decodedContent)) { + decodedContent = promptContent; + } + return { + host: location2.hostname + (location2.port ? ":" + location2.port : ""), + path: "aesthetic.computer/disks/prompt", + piece: "prompt", + colon: void 0, + params: [decodedContent], + // Pass the content as a parameter + search: promptSearch, + hash: void 0, + text + }; + } + const kidlispCheck = isKidlispSource(text); + const hasSpecialChars = text.includes("\xA7") || text.includes("~") || text.includes("_") || text.includes(",") || // Comma-separated kidlisp syntax + text.includes("\n") || text.startsWith("(") || text.startsWith(";"); + const textWithSpaces = text.replace(/_/g, " "); + const isKidlispAfterDecode = isKidlispSource(textWithSpaces); + if (kidlispCheck && hasSpecialChars || isKidlispAfterDecode) { + const decodedSource = decodeKidlispFromUrl(text); + return { + host: location2.hostname + (location2.port ? ":" + location2.port : ""), + path: "(...)", + // Use a special path indicator for kidlisp + piece: "(...)", + colon: void 0, + params: [], + search: void 0, + hash: void 0, + text: decodedSource, + source: decodedSource, + // Include the decoded source code + name: decodedSource + // Use the source as the name too + }; + } + text = text.replace(/\s*:\s*/g, ":"); + text = text.replace(/ /g, "~"); + try { + text = decodeURIComponent(text); + } catch (e2) { + console.log("\u26A0\uFE0F URI decode skipped (literal legacy % or # symbols):", text); + } + text = text.replace(/~#([^~]*)/g, (match, code2) => { + return `~\xA7HASH\xA7${code2}`; + }); + text = text.replace(/\{#([^}]*)\}/g, (match, code2) => { + return `{\xA7HASH\xA7${code2}}`; + }); + [text, hash] = text.split("#"); + text = text.replace(/§HASH§/g, "#"); + if (text[0] === "?") { + search = text.slice(1); + text = window?.acSTARTING_PIECE || "prompt"; + } else { + let questionIndex = text.indexOf("?"); + while (questionIndex >= 0) { + if (shouldTreatAsQuery(text, questionIndex)) { + search = text.slice(questionIndex + 1); + text = text.slice(0, questionIndex); + break; + } + questionIndex = text.indexOf("?", questionIndex + 1); + } + } + if (text.endsWith("/")) text = text.slice(0, -1); + const tokens = text.split("~"); + if (tokens[0] && tokens[0].startsWith("!") && tokens[0].length > 1) { + tokens.unshift("video"); + } + if (tokens[0] && tokens[0].startsWith("*") && tokens[0].length > 1) { + tokens.unshift("clock"); + } + if (tokens[0] && tokens[0].startsWith("+") && tokens[0].length > 1) { + tokens.unshift("mug"); + } + if (tokens[0] && tokens[0].startsWith("^")) { + tokens.unshift("bag"); + } + if (tokens[0]) { + const [maybeSlug, ...colonParts] = tokens[0].split(":"); + if (/^mo\d+$/.test(maybeSlug)) { + const seconds = maybeSlug.slice(2); + tokens[0] = "mo" + (colonParts.length ? ":" + colonParts.join(":") : ""); + tokens.splice(1, 0, seconds); + } + } + if (tokens.length === 1 && isKidlispSource(tokens[0])) { + const decodedSource = decodeKidlispFromUrl(tokens[0]); + return { + host: location2.hostname + (location2.port ? ":" + location2.port : ""), + path: "(...)", + // Use a special path indicator for kidlisp + piece: "(...)", + colon: void 0, + params: [], + search: void 0, + hash: void 0, + text: decodedSource, + source: decodedSource, + // Include the decoded source code + name: decodedSource + // Use the source as the name too + }; + } + let handlePiece = false; + if (tokens[0].indexOf("@") === 0 && tokens[0].indexOf("/") !== -1) { + handlePiece = true; + } + let colonParam; + const colonSplit = tokens[0].split(":"); + if (colonSplit.length > 0) { + tokens[0] = colonSplit[0]; + colonParam = colonSplit.slice(1); + } + const piece = tokens[0]; + if (handlePiece) { + host = location2.hostname; + if (location2.port) host += ":" + location2.port; + const [handle2, name] = tokens[0].split("/"); + path = `media/${handle2}/piece/${name}`; + } else { + host = location2.hostname; + if (location2.port) host += ":" + location2.port; + if (externalPath !== void 0) { + if (externalPath.length === 0) { + path = tokens[0]; + } else { + path = externalPath + "/" + tokens[0]; + } + } else { + path = "aesthetic.computer/disks/" + tokens[0]; + } + } + params = tokens.slice(1); + return { host, path, piece, colon: colonParam, params, search, hash, text }; +} +function inferTitleDesc(source) { + let title, desc; + const lines = source.split("\n"); + if (lines[0].startsWith("//") || lines[0].startsWith(";")) { + title = lines[0].split(",")[0].slice(2).trim(); + } + if (lines[1]?.startsWith("//") || lines[1]?.startsWith(";")) { + desc = lines[1].slice(2).trim(); + } + const standaloneTitle = lines[0]?.startsWith(";") && !!title; + return { title, desc, standaloneTitle }; +} +function metadata(host, slug, pieceMetadata2, protocol = "https:", objktContext = null) { + const notAesthetic = host.indexOf("sotce") > -1 || host.indexOf("botce") > -1 || host.indexOf("wipppps.world") > -1; + const isStandaloneTitle = pieceMetadata2?.standaloneTitle === true; + let title; + if (checkPackMode()) { + try { + const colophon = typeof window !== "undefined" && window.acPACK_COLOPHON || typeof globalThis !== "undefined" && globalThis.acPACK_COLOPHON; + if (colophon?.piece?.name && colophon?.build?.author) { + let timestamp2 = (/* @__PURE__ */ new Date()).getFullYear(); + if (colophon.build.zipFilename) { + const timestampMatch = colophon.build.zipFilename.match(/(2025\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})/); + if (timestampMatch) { + timestamp2 = timestampMatch[1]; + } + } else if (colophon.build.packTime) { + const date = new Date(colophon.build.packTime); + timestamp2 = `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}.${String(date.getHours()).padStart(2, "0")}.${String(date.getMinutes()).padStart(2, "0")}.${String(date.getSeconds()).padStart(2, "0")}.${String(date.getMilliseconds()).padStart(3, "0")}`; + } + title = `${colophon.piece.name} by ${colophon.build.author}, ${timestamp2}`; + } else if (objktContext?.author) { + const year = (/* @__PURE__ */ new Date()).getFullYear(); + title = `${slug} by ${objktContext.author}, ${year}`; + } else { + title = slug; + } + } catch (e2) { + if (objktContext?.author) { + const year = (/* @__PURE__ */ new Date()).getFullYear(); + title = `${slug} by ${objktContext.author}, ${year}`; + } else { + title = slug; + } + } + } else if (pieceMetadata2?.title) { + title = pieceMetadata2.title + (notAesthetic || isStandaloneTitle ? "" : " \xB7 Aesthetic Computer"); + } else { + title = slug !== "prompt" ? slug + " \xB7 Aesthetic Computer" : "Aesthetic Computer"; + } + const desc = pieceMetadata2?.desc || "An Aesthetic Computer piece."; + let ogImage, twitterImage; + let icon2; + if (pieceMetadata2?.image_url) { + ogImage = twitterImage = pieceMetadata2.image_url; + } else { + if ("notepat" === slug) { + ogImage = `https://oven.aesthetic.computer/notepat-og.png`; + twitterImage = `https://oven.aesthetic.computer/notepat-og.png`; + } else { + ogImage = `https://oven.aesthetic.computer/preview/1200x630/${slug}.png`; + twitterImage = `https://oven.aesthetic.computer/preview/1800x900/${slug}.png`; + } + } + const slugParts = slug.split("~"); + let pieceName = slugParts[0]; + if (pieceName === "keep" && slugParts.length > 1 && slugParts[1].startsWith("$")) { + pieceName = slugParts[1]; + } + icon2 = pieceMetadata2?.icon_url || `https://oven.aesthetic.computer/icon/128x128/${pieceName}.png`; + const animatedIcon = pieceMetadata2?.animatedIcon; + const iconWebp = animatedIcon ? `https://oven.aesthetic.computer/icon/128x128/${pieceName}.webp` : null; + const manifest = `https://${host}/manifest.json`; + return { title, desc, ogImage, twitterImage, icon: icon2, iconWebp, manifest }; +} +function updateCode(sourceToRun, host, debug4, protocol = location?.protocol, serverRewrites = false, serverRewritesPath = "") { + let updatedCode = sourceToRun; + const namedImports = /^(import|export) {([^{}]*?)} from ["'](\.\.\/|\.\.|\.\/)(.*?)["'];?/gm; + const namespaceImportsTwoDots = /^(import|export) \* as ([^ ]+) from ["'](\.\.\/)(.*?)["'];?/gm; + const namespaceImportsOneDot = /^(import|export) \* as ([^ ]+) from ["']\.?\/(.*?)["'];?/gm; + const replaceNamedImports = (match, p1, p22, p3, p4) => { + let url; + if (serverRewrites) { + url = `..${serverRewritesPath}/public/aesthetic.computer${p3 === "./" ? "/disks" : ""}/${p4.replace(/\.\.\//g, "")}`; + } else { + url = `${protocol}//${host}/aesthetic.computer${p3 === "./" ? "/disks" : ""}/${p4.replace(/\.\.\//g, "")}`; + } + return `${p1} {${p22}} from "${url}";`; + }; + const replaceNamespaceTwoDots = (match, p1, p22, p3, p4) => { + let url; + if (serverRewrites) { + url = `..${serverRewritesPath}/public/aesthetic.computer/${p4.replace(/\.\.\//g, "")}`; + } else { + url = `${protocol}//${host}/aesthetic.computer/${p4.replace(/\.\.\//g, "")}`; + } + return `${p1} * as ${p22} from "${url}";`; + }; + const replaceNamespaceOneDot = (match, p1, p22, p3) => { + let url; + if (serverRewrites) { + url = `..${serverRewritesPath}/public/aesthetic.computer${p3.startsWith("disks/") ? "" : "/disks"}/${p3.replace(/^disks\//, "")}`; + } else { + url = `${protocol}//${host}/aesthetic.computer${p3.startsWith("disks/") ? "" : "/disks"}/${p3.replace(/^disks\//, "")}`; + } + return `${p1} * as ${p22} from "${url}";`; + }; + updatedCode = updatedCode.replace(namedImports, replaceNamedImports); + updatedCode = updatedCode.replace(namespaceImportsTwoDots, replaceNamespaceTwoDots); + updatedCode = updatedCode.replace(namespaceImportsOneDot, replaceNamespaceOneDot); + updatedCode = `const DEBUG = ${debug4}; +${updatedCode}`; + updatedCode = addExportsToCode(updatedCode); + return updatedCode; +} +function addExportsToCode(code2) { + const whitelist = [ + "paint", + "boot", + "act", + "sim", + "meta", + "brush", + "preview", + "icon", + "beat", + "brush", + "filter" + ]; + const codeWithoutComments = code2.split("\n").map((line2) => { + let inString = false; + let stringChar = ""; + let result = ""; + for (let i2 = 0; i2 < line2.length; i2++) { + const char = line2[i2]; + const nextChar = line2[i2 + 1]; + if (!inString && (char === '"' || char === "'" || char === "`")) { + inString = true; + stringChar = char; + result += char; + } else if (inString && char === stringChar && line2[i2 - 1] !== "\\") { + inString = false; + stringChar = ""; + result += char; + } else if (!inString && char === "/" && nextChar === "/") { + break; + } else if (!inString && char === "/" && nextChar === "*") { + i2 += 2; + while (i2 < line2.length - 1) { + if (line2[i2] === "*" && line2[i2 + 1] === "/") { + i2 += 2; + break; + } + i2++; + } + i2--; + } else { + result += char; + } + } + return result; + }).join("\n"); + const hasExportObject = /export\s+{[^}]*}/m.test(codeWithoutComments); + if (!hasExportObject) { + const topLevelFunctionRegex = /^function\s+(\w+)\s*\(/gm; + const topLevelFunctions = []; + let match; + while ((match = topLevelFunctionRegex.exec(codeWithoutComments)) !== null) { + const functionName = match[1]; + if (whitelist.includes(functionName)) { + topLevelFunctions.push(functionName); + } + } + if (topLevelFunctions.length > 0) { + code2 += ` +export { ${topLevelFunctions.join(", ")} };`; + } + } + return code2; +} + +// public/aesthetic.computer/lib/socket.mjs +var logs2 = { socket: false }; +var { min: min6 } = Math; +var Socket = class { + id; + // Will be filled in with the user identifier after the first message. + connected = false; + #sendToBIOS; + #debug; + #killSocket = false; + #ws; + #reconnectTime = 1e3; + #reconnectTimeout; + #queue = []; + constructor(debug4, sendToBIOS) { + this.#debug = debug4; + this.#sendToBIOS = sendToBIOS; + } + // Connects a WebSocket object and takes a handler for messages. + connect(host, receive2, reload, protocol = "wss", connectCallback, disconnectCallback) { + if (typeof window !== "undefined" && window.acOBJKT_MODE) { + if (this.#debug && logs2.socket) console.log("\u{1F9E6} Sockets disabled in OBJKT mode."); + return; + } + if (this.connected) { + console.warn("\u{1F9E6} Already connected..."); + return; + } + if (this.#debug && logs2.socket) console.log("\u{1F9E6} Connecting...", host); + try { + this.#ws = new WebSocket(`${protocol}://${host}`); + } catch { + if (!checkPackMode()) { + console.log("%cconnection failed, retrying in " + this.#reconnectTime / 1e3 + "s...", "color: orange; background: black; padding: 2px;"); + } + return; + } + const socket2 = this; + const ws = this.#ws; + ws.onopen = (e2) => { + socket2.#queue.forEach((q) => socket2.send(...q)); + socket2.connected = true; + socket2.#queue.length = 0; + socket2.#reconnectTime = 1; + connectCallback?.(); + }; + ws.onmessage = (e2) => { + const msg = JSON.parse(e2.data); + socket2.#preReceive(msg, receive2, reload, this.#sendToBIOS); + }; + ws.onclose = (e2) => { + if (logs2.socket) + console.warn("\u{1F9E6} Disconnected...", e2.currentTarget?.url); + clearTimeout(this.pingTimeout); + socket2.connected = false; + if (socket2.#killSocket === false && !checkPackMode()) { + console.log("%cconnection failed, retrying in " + socket2.#reconnectTime / 1e3 + "s...", "color: orange; background: black; padding: 2px;"); + this.#reconnectTimeout = setTimeout(() => { + socket2.connect(host, receive2, reload, protocol, connectCallback); + }, socket2.#reconnectTime); + socket2.#reconnectTime = min6(socket2.#reconnectTime, 16e3); + socket2.#reconnectTime *= 2; + } + disconnectCallback?.(); + }; + ws.onerror = (err) => { + if (checkPackMode()) { + ws.close(); + } else { + console.log("%cconnection failed, retrying...", "color: orange; background: black; padding: 2px;"); + ws.close(); + } + }; + } + // Send a formatted message to the connected WebSocket server. + // Passes silently on no connection. + send(type, content) { + if (this.#ws?.readyState === WebSocket.OPEN) { + this.#ws.send(JSON.stringify({ type, content })); + } else { + this.#queue.push([type, content]); + } + } + // Kills the socket permanently. + kill(reconnectIn) { + if (!reconnectIn) { + this.#killSocket = true; + clearTimeout(this.#reconnectTimeout); + } else { + this.#reconnectTime = reconnectIn * 1e3; + if (logs2.socket) { + console.log("\u{1F9E6} Reconnecting in:", this.#reconnectTime, "seconds."); + } + } + this.#ws?.close(); + } + // Before passing messages to disk code, handle some system messages here. + // Note: "reload" should only be defined when in development / debug mode. + #preReceive({ id, type, content }, receive2, reload, sendToBIOS) { + if (this.#killSocket) return; + if (type === "connected") { + const c4 = JSON.parse(content); + this.id = id; + if (logs2.socket) { + console.log( + `\u{1F9E6} You joined: ${c4.ip} id: ${c4.id} \u{1F939} Connections open: ${c4.playerCount}` + ); + } + receive2?.(id, type, c4); + } else if (type === "joined") { + const c4 = JSON.parse(content); + if (logs2.socket) console.log(`\u{1F9E6} ${c4.text || c4}`); + receive2?.(id, type, c4); + } else if (type === "vscode-extension:reload") { + sendToBIOS?.({ + type: "post-to-parent", + content: { type: "vscode-extension:reload" } + }); + } else if (type === "reload" && reload && this.#debug) { + let c4; + if (typeof content === "object") { + c4 = content; + } else { + c4 = JSON.parse(content); + } + reload(c4); + } else if (type === "code") { + const parsed = typeof content === "string" ? JSON.parse(content) : content; + if (id === "development") { + reload?.({ + name: parsed.piece, + source: parsed.source, + codeChannel: parsed.codeChannel + }); + } + } else if (type === "left") { + const c4 = typeof content === "string" ? JSON.parse(content) : content; + if (logs2.socket) + console.log(`\u{1F9E6} ${id} has left. Connections open: ${content.count}`); + receive2?.(id, type, c4); + } else { + try { + receive2?.(id, type, content); + } catch (err) { + console.error("\u{1F9E6} Socket message error:", err); + } + } + } +}; + +// public/aesthetic.computer/lib/diagnostics.mjs +var FRAME_SAMPLE = 1024; +var HEARTBEAT_MS = 4e3; +var MAX_LOGS_PER_SECOND = 20; +var MAX_LOG_LENGTH = 2e3; +var QUANTIZE = 3; +var STEPS = [7919, 6151, 3079, 1543, 769, 389, 193, 97, 11, 3, 1]; +function stepFor(total) { + for (const step of STEPS) if (step < total && total % step !== 0) return step; + return 1; +} +function frameSignature(pixels2, width2, height2, sample = FRAME_SAMPLE) { + const count = Math.floor((Number(width2) || 0) * (Number(height2) || 0)); + if (!pixels2 || count <= 0) return null; + const length5 = pixels2.length || 0; + if (length5 < 4) return null; + const total = Math.min(count, Math.floor(length5 / 4)); + const step = stepFor(total); + const want = Math.min(total, sample); + const seen = /* @__PURE__ */ new Set(); + let first = -1; + let sampled = 0; + for (let n2 = 0; n2 < want; n2 += 1) { + const o2 = n2 * step % total * 4; + const key = pixels2[o2] >> QUANTIZE << 16 | pixels2[o2 + 1] >> QUANTIZE << 8 | pixels2[o2 + 2] >> QUANTIZE; + if (first < 0) first = o2; + seen.add(key); + sampled += 1; + if (seen.size > 64) break; + } + if (sampled === 0) return null; + return { + colors: seen.size, + blank: seen.size === 1, + sampled, + // The colour it is stuck on, which is usually the `wipe` and is usually the + // fastest way to recognise what happened. + color: seen.size === 1 && first >= 0 ? [pixels2[first], pixels2[first + 1], pixels2[first + 2]] : null + }; +} +var Diagnostics = class { + constructor({ send: send2, now = () => Date.now(), heartbeat = HEARTBEAT_MS } = {}) { + this.send = send2; + this.now = now; + this.heartbeat = heartbeat; + this.room = ""; + this.last = null; + this.lastAt = 0; + this.window = 0; + this.spent = 0; + this.dropped = 0; + } + get on() { + return Boolean(this.room); + } + // Point at a channel, or pass nothing to go quiet. Everything resets: the + // counts described the piece that was being watched before. + channel(name) { + const next = String(name || ""); + if (next === this.room) return false; + this.room = next; + this.last = null; + this.lastAt = 0; + this.dropped = 0; + return true; + } + #post(kind, body) { + if (!this.on) return; + try { + this.send("diagnostics:report", { channel: this.room, kind, ...body }); + } catch { + } + } + // One painted frame, as it was handed to the display. + frame(pixels2, width2, height2) { + if (!this.on) return; + const signature = frameSignature(pixels2, width2, height2); + if (!signature) return; + const at = this.now(); + const changed = !this.last || this.last.blank !== signature.blank || this.last.colors !== signature.colors; + if (!changed && at - this.lastAt < this.heartbeat) return; + this.last = signature; + this.lastAt = at; + this.#post("frame", { + colors: signature.colors, + blank: signature.blank, + color: signature.color, + width: width2, + height: height2 + }); + } + // One console line or caught error. `args` arrives already serialized by the + // caller, which owns the console hijack. + note(level, args) { + if (!this.on) return; + const at = this.now(); + const second = Math.floor(at / 1e3); + if (second !== this.window) { + const missed = this.dropped; + this.window = second; + this.spent = 0; + this.dropped = 0; + if (missed > 0) this.#post("dropped", { count: missed }); + } + this.spent += 1; + if (this.spent > MAX_LOGS_PER_SECOND) { + this.dropped += 1; + return; + } + const text = (Array.isArray(args) ? args : [args]).map((value) => String(value ?? "")).join(" ").slice(0, MAX_LOG_LENGTH); + if (!text) return; + this.#post("log", { level: String(level || "log"), text, at }); + } + // Uncaught errors, including the ones only the worker ever sees. + watch(scope = globalThis) { + if (!scope?.addEventListener) return this; + scope.addEventListener("error", (event) => { + const where = event?.filename ? ` at ${event.filename}:${event.lineno}:${event.colno}` : ""; + this.note("error", [`Uncaught: ${event?.message || event?.type}${where}`]); + }); + scope.addEventListener("unhandledrejection", (event) => { + this.note("error", [`Unhandled promise: ${event?.reason}`]); + }); + return this; + } +}; + +// public/aesthetic.computer/lib/redact.mjs +function redact(msg) { + msg.redactedText = msg.text; + msg.text = msg.text.replace(/\S/g, "_"); +} +function unredact(msg) { + msg.text = msg.redactedText || msg.text; +} + +// public/aesthetic.computer/lib/chat.mjs +var validInstances = ["chat-system", "chat-sotce", "chat-clock"]; +var Chat = class { + system; + $commonApi; + // Set by `disk` after `$commonApi` is defined. + #debug = false; + constructor(debug4, send2, disconnect) { + this.system = { + server: new Socket(debug4, send2), + chatterCount: 0, + onlineHandles: [], + // Realtime list of online user handles (all connected) + hereHandles: [], + // Users actually viewing the chat piece right now + messages: [], + hearts: /* @__PURE__ */ new Map(), + // Map + // receiver: // A custom receiver that can be defined in a piece. + // like `chat` to get the events. + disconnect, + // A custom disconnection that triggers below. + connecting: true + // Start in connecting state, set to false when connected + }; + this.system.sendHeart = (id, token) => { + if (!id || !token) return; + const existing2 = this.system.hearts.get(id) || { count: 0, heartedByMe: false }; + const heartedByMe = !existing2.heartedByMe; + const count = existing2.count + (heartedByMe ? 1 : -1); + this.system.hearts.set(id, { count: Math.max(0, count), heartedByMe }); + this.system.server.send("chat:heart", { for: id, token }); + }; + this.#debug = debug4; + } + // Connect to a chat instance. + // Instance options are `system` for AC users and `sotce` for Sotce Net + // as of 24.11.02.00.31 + connect(instanceName) { + instanceName = "chat-" + instanceName; + if (validInstances.indexOf(instanceName) === -1) { + console.warn( + "\u{1FAAB} Chat connection aborted. Invalid instance name:", + instanceName + ); + return; + } + let chatUrl; + if (this.#debug) { + if (location.hostname === "local.aesthetic.computer") { + chatUrl = `${instanceName}.${location.hostname}`; + } else { + let port; + if (instanceName === "chat-system") { + port = 8083; + } else if (instanceName === "chat-sotce") { + port = 8084; + } else if (instanceName === "chat-clock") { + port = 8085; + } + chatUrl = `${location.hostname}:${port}`; + } + } else { + if (instanceName === "chat-system") { + chatUrl = "chat-system.aesthetic.computer"; + } else if (instanceName === "chat-sotce") { + chatUrl = "chat.sotce.net"; + } else if (instanceName === "chat-clock") { + chatUrl = "chat-clock.aesthetic.computer"; + } + } + console.log("\u{1F5E8}\uFE0F Chat connect:", instanceName, "\u2192", chatUrl, "debug:", this.#debug); + this.system.server.connect( + chatUrl, + // host + (id, type, content) => { + const extra = {}; + if (type === "connected") { + this.system.connecting = false; + this.system.chatterCount = content?.chatters || this.system.chatterCount; + this.system.onlineHandles = content?.handles || []; + this.system.messages.length = 0; + this.system.messages.push(...content.messages); + this.system.hearts.clear(); + const hc = content?.heartCounts || {}; + for (const [id2, count] of Object.entries(hc)) { + this.system.hearts.set(id2, { count, heartedByMe: false }); + } + for (const msg of content.messages) { + if (msg.id && msg.hearts > 0 && !this.system.hearts.has(msg.id)) { + this.system.hearts.set(msg.id, { count: msg.hearts, heartedByMe: false }); + } + } + if (logs.chat) { + console.log( + `\u{1F4AC} %c${content.message}`, + `color: cyan; background: rgba(10, 20, 40);` + ); + } + } + if (type === "unauthorized") { + if (logs.chat) console.log("\u{1F534} Chat message unauthorized!", content); + this.$commonApi?.notice("Unauthorized", ["red", "yellow"]); + } + if (type === "message") { + const msg = JSON.parse(content); + if (logs.chat) console.log("\u{1F4AC} Chat message received:", msg); + this.system.messages.push(msg); + if (this.system.messages.length > 500) this.system.messages.shift(); + content = msg; + extra.layoutChanged = true; + } + if (type === "message:update") { + const updateData = JSON.parse(content); + if (logs.chat) console.log("\u{1F4AC} Chat message updated:", updateData); + if (this.system.messages[updateData.index]) { + this.system.messages[updateData.index].count = updateData.count; + extra.layoutChanged = true; + } + } + if (type === "message:edit") { + const editData = JSON.parse(content); + if (logs.chat) console.log("\u{1F4AC} Chat message edited:", editData); + const msg = this.system.messages.find((m) => m.id === editData.id); + if (msg) { + msg.text = editData.text; + msg.edited = true; + msg.editedWhen = editData.editedWhen; + delete msg.redactedText; + delete msg._colorLineCache; + delete msg._colorLineHoverKey; + delete msg._cachedLastLineWidth; + extra.layoutChanged = true; + } + content = editData; + } + if (type === "message:delete") { + const deleteData = JSON.parse(content); + if (logs.chat) console.log("\u{1F4AC} Chat message deleted:", deleteData); + const msg = this.system.messages.find((m) => m.id === deleteData.id); + if (msg) { + msg.text = "[deleted]"; + msg.deleted = true; + delete msg.redactedText; + extra.layoutChanged = true; + } + } + if (type === "message:hearts") { + const heartsData = JSON.parse(content); + if (logs.chat) console.log("\u{1F4AC} Hearts update:", heartsData); + const existing2 = this.system.hearts.get(heartsData.for) || { heartedByMe: false }; + this.system.hearts.set(heartsData.for, { + count: heartsData.count, + heartedByMe: existing2.heartedByMe + }); + extra.layoutChanged = true; + } + if (type === "handle:update" || type === "handle:strip") { + const msg = JSON.parse(content); + content = msg; + console.log("\u{1F471}\uFE0F\u200D `handle` edit received:", type, content); + this.system.messages.forEach((message) => { + if (message.sub === content.user) { + message.from = content.handle; + extra.layoutChanged = true; + } + }); + if (type === "handle:strip" && msg.user === this.$commonApi?.user?.sub) { + console.log("\u{1FA79} Your handle has been stripped!"); + this.$commonApi?.notice("HANDLE STRIPPED", ["red", "yellow"]); + setTimeout(() => { + this.$commonApi?.net.refresh(); + }, 750); + } + } + if (type === "handle:colors") { + const msg = JSON.parse(content); + console.log("\u{1F3A8} Handle colors update received:", msg.handle); + const cleanHandle = msg.handle.startsWith("@") ? msg.handle.slice(1) : msg.handle; + handleColorsCache.set(cleanHandle, msg.colors); + this.system.messages.forEach((message) => { + const msgHandle = message.from?.startsWith("@") ? message.from.slice(1) : message.from; + if (msgHandle?.toLowerCase() === cleanHandle.toLowerCase()) { + extra.layoutChanged = true; + } + }); + } + if (type === "chat-system:mute" || type === "chat-system:unmute") { + const msg = JSON.parse(content); + content = msg; + if (this.$commonApi?.user?.sub === content.user) { + if (type === "chat-system:mute") + this.$commonApi.notice("MUTED", ["red", "yellow"]); + if (type === "chat-system:unmute") + this.$commonApi.notice("UNMUTED"); + } + this.system.messages.forEach((message) => { + if (message.sub === content.user) { + if (type === "chat-system:mute") redact(message); + if (type === "chat-system:unmute") unredact(message); + extra.layoutChanged = true; + } + }); + } + if (type === "left") { + if (logs.chat) console.log("\uFE0F\u270C\uFE0F Goodbye:", id, type, content); + this.system.chatterCount = content.chatters; + if (content.handles) this.system.onlineHandles = content.handles; + } + if (type === "joined") { + if (logs.chat) console.log("\uFE0F\u{1F44B} Hello:", id, type, content); + this.system.chatterCount = content.chatters; + if (content.handles) this.system.onlineHandles = content.handles; + } + if (type === "online-handles") { + if (logs.chat) console.log("\u{1F465} Online handles:", content.handles); + this.system.onlineHandles = content.handles || []; + } + if (type === "presence") { + if (logs.chat) console.log("\u{1F465} Presence update - online:", content.online, "here:", content.here); + this.system.onlineHandles = content.online || content.handles || []; + this.system.hereHandles = content.here || []; + } + this.system.receiver?.(id, type, content, extra); + }, + void 0, + // reload + "wss", + // protocol + void 0, + // connectionCallback + () => { + if (logs.chat) console.log("\u{1F4AC}\u{1F6AB} Chat disconnected."); + this.system.chatterCount = 0; + this.system.onlineHandles = []; + this.system.connecting = true; + this.system.disconnect?.(); + } + ); + } +}; + +// public/aesthetic.computer/lib/helpers.mjs +function notArray(obj) { + return !Array.isArray(obj); +} +function defaultTemplateStringProcessor(strings, ...vars) { + let result = ""; + strings.forEach((str7, i2) => { + result += `${str7}${i2 === strings.length - 1 ? "" : vars[i2]}`; + }); + return result; +} +function uint8ArrayToBase64(buffer) { + let binary = ""; + let bytes = new Uint8Array(buffer); + let len5 = bytes.byteLength; + for (let i2 = 0; i2 < len5; i2++) binary += String.fromCharCode(bytes[i2]); + return btoa(binary); +} +function base64ToUint8Array(base64) { + let binaryString = atob(base64); + let len5 = binaryString.length; + let bytes = new Uint8Array(len5); + for (let i2 = 0; i2 < len5; i2++) { + bytes[i2] = binaryString.charCodeAt(i2); + } + return bytes; +} + +// public/aesthetic.computer/lib/piece-permissions.mjs +var grantedPermissions = /* @__PURE__ */ new Map(); +var pendingRequests = /* @__PURE__ */ new Map(); +var nextRequestId = 0; +var isWorker = typeof document === "undefined"; +async function requestPermission(pieceCode, permission, details = {}) { + const piecePerms = grantedPermissions.get(pieceCode); + if (piecePerms?.has(permission)) { + return true; + } + const granted = await showPermissionPrompt(pieceCode, permission, details); + if (granted) { + if (!grantedPermissions.has(pieceCode)) { + grantedPermissions.set(pieceCode, /* @__PURE__ */ new Set()); + } + grantedPermissions.get(pieceCode).add(permission); + } + return granted; +} +function clearPermissions(pieceCode) { + grantedPermissions.delete(pieceCode); +} +function resolvePermissionRequest(requestId, granted) { + const pending = pendingRequests.get(requestId); + if (pending) { + pending.resolve(granted); + pendingRequests.delete(requestId); + } +} +async function showPermissionPrompt(pieceCode, permission, details) { + if (isWorker) { + const requestId = nextRequestId++; + return new Promise((resolve) => { + pendingRequests.set(requestId, { resolve }); + postMessage({ + type: "permission-request", + content: { requestId, pieceCode, permission, details } + }); + }); + } + return showDOMPermissionPrompt(pieceCode, permission, details); +} +function showDOMPermissionPrompt(pieceCode, permission, details) { + return new Promise((resolve) => { + const messages = { + network: { + title: "Network Access", + message: `The piece "${pieceCode}" wants to make network requests.`, + warning: "This allows the piece to send data to external servers.", + details: details.url ? `URL: ${details.url}` : null + }, + auth: { + title: "Authentication Access", + message: `The piece "${pieceCode}" wants to access your authentication tokens.`, + warning: "This gives the piece access to your account credentials.", + details: null + }, + "storage-full": { + title: "Full Storage Access", + message: `The piece "${pieceCode}" wants to access all stored data.`, + warning: "This allows the piece to read data from other pieces.", + details: null + }, + "upload-external": { + title: "External Upload", + message: `The piece "${pieceCode}" wants to upload a file to an external server.`, + warning: "The file will be sent outside aesthetic.computer.", + details: details.url ? `Destination: ${details.url}` : null + }, + "navigate-external": { + title: "External Navigation", + message: `The piece "${pieceCode}" wants to navigate to an external URL.`, + warning: "You will leave aesthetic.computer.", + details: details.url ? `URL: ${details.url}` : null + } + }; + const config = messages[permission] || { + title: "Permission Request", + message: `The piece "${pieceCode}" wants permission for: ${permission}`, + warning: null, + details: null + }; + const modal = document.createElement("div"); + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 100000; + font-family: sans-serif; + `; + const dialog = document.createElement("div"); + dialog.style.cssText = ` + background: rgb(235, 235, 235); + color: black; + padding: 24px; + max-width: 400px; + border: 2px solid black; + `; + const title = document.createElement("h2"); + title.textContent = config.title; + title.style.cssText = "margin: 0 0 16px 0; font-size: 18px; font-weight: bold;"; + const message = document.createElement("p"); + message.textContent = config.message; + message.style.cssText = "margin: 0 0 12px 0; line-height: 1.4;"; + dialog.appendChild(title); + dialog.appendChild(message); + if (config.warning) { + const warning = document.createElement("p"); + warning.textContent = config.warning; + warning.style.cssText = "margin: 0 0 12px 0; color: rgb(200, 0, 0); font-weight: bold;"; + dialog.appendChild(warning); + } + if (config.details) { + const detailsEl = document.createElement("p"); + detailsEl.textContent = config.details; + detailsEl.style.cssText = "margin: 0 0 12px 0; font-family: monospace; font-size: 12px; word-break: break-all;"; + dialog.appendChild(detailsEl); + } + const buttonContainer = document.createElement("div"); + buttonContainer.style.cssText = "display: flex; gap: 12px; margin-top: 20px;"; + const allowButton = document.createElement("button"); + allowButton.textContent = "Allow"; + allowButton.style.cssText = ` + flex: 1; + padding: 10px; + background: black; + color: white; + border: none; + cursor: pointer; + font-size: 14px; + font-weight: bold; + `; + const denyButton = document.createElement("button"); + denyButton.textContent = "Deny"; + denyButton.style.cssText = ` + flex: 1; + padding: 10px; + background: white; + color: black; + border: 2px solid black; + cursor: pointer; + font-size: 14px; + font-weight: bold; + `; + allowButton.addEventListener("click", () => { + document.body.removeChild(modal); + resolve(true); + }); + denyButton.addEventListener("click", () => { + document.body.removeChild(modal); + resolve(false); + }); + buttonContainer.appendChild(allowButton); + buttonContainer.appendChild(denyButton); + dialog.appendChild(buttonContainer); + modal.appendChild(dialog); + document.body.appendChild(modal); + denyButton.focus(); + }); +} + +// public/aesthetic.computer/lib/restricted-api.mjs +function createRestrictedApi(fullApi, pieceMetadata2) { + const { code: pieceCode, trustLevel = "untrusted" } = pieceMetadata2; + if (trustLevel === "trusted" || trustLevel === "kidlisp") { + return fullApi; + } + const restrictedApi = { + // Copy all safe APIs (rendering, input, utilities) + ...fullApi, + // Wrap network APIs with permission checks + net: createRestrictedNetApi(fullApi.net, pieceCode), + // Namespace storage per piece + store: createNamespacedStore(fullApi.store, pieceCode), + // Block auth APIs for untrusted pieces (no user-uploaded piece should have auth tokens) + authorize: async () => { + throw new Error(`Auth tokens are not available to user-uploaded pieces for security reasons`); + }, + // Wrap upload with external URL check + upload: createRestrictedUpload(fullApi.upload, pieceCode), + // Wrap jump with external URL check + jump: createRestrictedJump(fullApi.jump, pieceCode) + // Block dynamic imports by not exposing import functionality + // (pieces can still use static imports in their source) + }; + restrictedApi._restricted = true; + restrictedApi._pieceCode = pieceCode; + return restrictedApi; +} +function createRestrictedNetApi(netApi, pieceCode) { + return { + ...netApi, + // Wrap preload (fetch wrapper) + preload: async (url, options) => { + const granted = await requestPermission(pieceCode, "network", { url }); + if (!granted) { + throw new Error( + `Network access denied. The piece "${pieceCode}" requested access to: ${url}` + ); + } + return netApi.preload(url, options); + }, + // Wrap userRequest (authenticated fetch) + userRequest: async (url, options) => { + const granted = await requestPermission(pieceCode, "network", { url }); + if (!granted) { + throw new Error( + `Network access denied. The piece "${pieceCode}" requested access to: ${url}` + ); + } + throw new Error( + `Authenticated requests are not available to user-uploaded pieces for security reasons` + ); + }, + // Block getToken for untrusted pieces + getToken: async () => { + throw new Error(`Auth tokens are not available to user-uploaded pieces for security reasons`); + } + // Keep other net APIs that don't involve external requests + // (signup, login, etc. can stay since they're internal to AC) + }; +} +function createNamespacedStore(fullStore, pieceCode) { + const namespace = `piece:${pieceCode}:`; + return { + async get(key) { + return await fullStore.get(namespace + key); + }, + async set(key, value) { + return await fullStore.set(namespace + key, value); + }, + async retrieve(key, path) { + return await fullStore.retrieve(namespace + key, path); + }, + async delete(key) { + return await fullStore.delete(namespace + key); + }, + async keys() { + const allKeys = await fullStore.keys(); + return allKeys.filter((k) => k.startsWith(namespace)).map((k) => k.slice(namespace.length)); + }, + async clear() { + const keys4 = await this.keys(); + for (const key of keys4) { + await this.delete(key); + } + }, + // Expose method to request full storage access + async requestFullAccess() { + const granted = await requestPermission(pieceCode, "storage-full", {}); + if (granted) { + return fullStore; + } + throw new Error("Full storage access denied"); + } + }; +} +function createRestrictedUpload(uploadFn, pieceCode) { + return async (file, options = {}) => { + if (options.url && !isAestheticComputerUrl(options.url)) { + const granted = await requestPermission(pieceCode, "upload-external", { + url: options.url + }); + if (!granted) { + throw new Error( + `External upload denied. The piece "${pieceCode}" tried to upload to: ${options.url}` + ); + } + } + return uploadFn(file, options); + }; +} +function createRestrictedJump(jumpFn, pieceCode) { + return async (destination, ...args) => { + if (typeof destination === "string" && (destination.startsWith("http://") || destination.startsWith("https://")) && !isAestheticComputerUrl(destination)) { + const granted = await requestPermission( + pieceCode, + "navigate-external", + { url: destination } + ); + if (!granted) { + throw new Error( + `External navigation denied. The piece "${pieceCode}" tried to navigate to: ${destination}` + ); + } + } + return jumpFn(destination, ...args); + }; +} +function isAestheticComputerUrl(url) { + try { + const loc = typeof location !== "undefined" ? location : self.location; + const parsed = new URL(url, loc.href); + return parsed.hostname === "aesthetic.computer" || parsed.hostname.endsWith(".aesthetic.computer") || parsed.hostname === loc.hostname; + } catch { + return false; + } +} +function shouldRestrictPiece(pieceMetadata2) { + const { trustLevel, authorSub, anonymous } = pieceMetadata2; + if (anonymous !== false) { + return true; + } + if (trustLevel === "trusted") { + return false; + } + return true; +} + +// public/aesthetic.computer/lib/color-highlighting.mjs +var CHANNEL_COLORS = ["red", "lime", "deepskyblue"]; +var GRAYSCALE_COLOR = "silver"; +var NUMERIC_TOKEN_REGEX = /^-?\d+(?:\.\d+)?$/; +var RANGE_TOKEN_REGEX = /^-?\d+\s*-\s*-?\d+$/; +function isWildcardToken(token) { + return token === "?"; +} +function isNumericLikeToken(token) { + if (!token && token !== 0) return false; + const trimmed = `${token}`.trim(); + if (!trimmed) return false; + return NUMERIC_TOKEN_REGEX.test(trimmed) || RANGE_TOKEN_REGEX.test(trimmed) || isWildcardToken(trimmed); +} +function isHexColorToken(token) { + if (!token && token !== 0) return false; + const trimmed = `${token}`.trim(); + if (!trimmed) return false; + if (trimmed.startsWith("#")) { + const body = trimmed.slice(1); + return /^[0-9a-fA-F]{3,8}$/.test(body); + } + if (/^0x[0-9a-fA-F]{3,8}$/i.test(trimmed)) { + return true; + } + if (/^[0-9a-fA-F]{3,8}$/.test(trimmed) && /[a-fA-F]/.test(trimmed)) { + return true; + } + return false; +} +function expandShortHex(body) { + return body.split("").map((char) => char + char).join(""); +} +function clampChannelValue(value) { + if (value === void 0 || value === null || Number.isNaN(value)) return null; + return clamp(Math.round(value), 0, 255); +} +function channelColorString(index, value, { grayscale = false } = {}) { + const clamped = clampChannelValue(value); + if (clamped === null) { + return CHANNEL_COLORS[index] || "orange"; + } + if (grayscale) { + return `${clamped},${clamped},${clamped}`; + } + if (index === 0) return `${clamped},0,0`; + if (index === 1) return `0,${clamped},0`; + if (index === 2) return `0,0,${clamped}`; + return `${clamped},${clamped},${clamped}`; +} +function parseHexChannelValue(channelHex) { + if (!channelHex) return 0; + const normalized = channelHex.length === 1 ? channelHex.repeat(2) : channelHex; + return parseInt(normalized, 16); +} +function parseAlphaFromToken(token) { + if (token === void 0 || token === null) return null; + const trimmed = `${token}`.trim(); + if (!trimmed || trimmed === "?" || trimmed.includes("-")) return null; + const value = Number(trimmed); + if (Number.isNaN(value)) return null; + if (value <= 1 && trimmed.includes(".")) { + return clamp(Math.round(value * 255), 0, 255); + } + return clamp(Math.round(value), 0, 255); +} +function parseHexColorToken(token) { + if (!token && token !== 0) return null; + const original = `${token}`.trim(); + if (!original) return null; + let body = original; + let prefix = ""; + if (body.startsWith("#")) { + body = body.slice(1); + prefix = "#"; + } else if (body.startsWith("0x") || body.startsWith("0X")) { + prefix = body.slice(0, 2); + body = body.slice(2); + } + if (!/^[0-9a-fA-F]+$/.test(body)) return null; + if (![3, 4, 6, 8].includes(body.length)) return null; + const uppercaseBody = body.toUpperCase(); + const bodyLength = uppercaseBody.length; + let rgbHex = uppercaseBody; + let alphaHex = null; + if (bodyLength === 3) { + rgbHex = expandShortHex(uppercaseBody); + } else if (bodyLength === 4) { + rgbHex = expandShortHex(uppercaseBody.slice(0, 3)); + alphaHex = `${uppercaseBody[3]}${uppercaseBody[3]}`; + } else if (bodyLength === 6) { + rgbHex = uppercaseBody.slice(0, 6); + } else if (bodyLength === 8) { + rgbHex = uppercaseBody.slice(0, 6); + alphaHex = uppercaseBody.slice(6, 8); + } + rgbHex = rgbHex.toUpperCase(); + let rgb; + try { + rgb = hexToRgb(`#${rgbHex}`); + } catch (error) { + return null; + } + const alpha = alphaHex !== null ? parseInt(alphaHex, 16) : null; + const channelDisplays = []; + let alphaDisplay = null; + if (bodyLength === 3 || bodyLength === 4) { + channelDisplays.push(...uppercaseBody.slice(0, 3).split("")); + if (bodyLength === 4) { + alphaDisplay = uppercaseBody[3]; + } + } else { + const segments = uppercaseBody.slice(0, 6).match(/.{1,2}/g) || []; + channelDisplays.push(...segments); + if (bodyLength === 8) { + alphaDisplay = uppercaseBody.slice(6, 8); + } + } + return { + rgb, + alpha, + alphaHex, + prefix, + channelDisplays, + alphaDisplay + }; +} +function formatAlphaToken(rawToken, parsedAlpha) { + if (parsedAlpha === void 0 || parsedAlpha === null || Number.isNaN(parsedAlpha)) { + const tokenText = rawToken !== void 0 ? rawToken : "?"; + return `\\magenta\\${tokenText}`; + } + const alphaInt = clampChannelValue(parsedAlpha) ?? 0; + return `\\magenta\\${alphaInt}`; +} +function colorizeHexToken(token) { + const parsed = parseHexColorToken(token); + if (!parsed || !parsed.rgb) { + return { text: `\\orange\\${token}`, consumed: 1, brushColor: null, alphaIncluded: false }; + } + const [r2, g, b2] = parsed.rgb; + const colorValue = `${r2},${g},${b2}`; + let block = `\\${colorValue}\\${parsed.prefix || "#"}`; + parsed.channelDisplays.forEach((channelHex, index) => { + const channelValue = parseHexChannelValue(channelHex); + const channelColor = channelColorString(index, channelValue); + block += `\\${channelColor}\\${channelHex}`; + }); + let alphaIncluded = false; + if (parsed.alpha !== null && !Number.isNaN(parsed.alpha)) { + const alphaSegment = formatAlphaToken(parsed.alphaDisplay, parsed.alpha); + if (alphaSegment) { + block += ` ${alphaSegment}`; + alphaIncluded = true; + } + } + return { text: block, consumed: 1, brushColor: colorValue, alphaIncluded }; +} +function colorizeNumericChannels(rawTokens, parsedArray) { + if (!Array.isArray(parsedArray) || parsedArray.length === 0 || rawTokens.length === 0) { + return { text: "", consumed: 0, brushColor: null, alphaIncluded: false }; + } + const available = Math.min(rawTokens.length, parsedArray.length); + const channelCount = Math.min(3, available); + const segments = []; + let brushColor = null; + let alphaIncluded = false; + if (channelCount === 1) { + const value = clampChannelValue(parsedArray[0]); + const colorString = value === null ? GRAYSCALE_COLOR : channelColorString(0, value, { grayscale: true }); + segments.push(`\\${colorString}\\${rawTokens[0]}`); + brushColor = value === null ? null : `${value},${value},${value}`; + if (available >= 2) { + const alphaValue = parsedArray[1]; + segments.push(formatAlphaToken(rawTokens[1], alphaValue)); + alphaIncluded = true; + return { + text: segments.join(" "), + consumed: 2, + brushColor, + alphaIncluded + }; + } + return { + text: segments.join(" "), + consumed: 1, + brushColor, + alphaIncluded + }; + } + const channelValues = []; + for (let i2 = 0; i2 < channelCount; i2++) { + const value = clampChannelValue(parsedArray[i2]); + channelValues.push(value === null ? 0 : value); + const channelString = channelColorString(i2, value); + segments.push(`\\${channelString}\\${rawTokens[i2]}`); + } + let consumed = channelCount; + if (channelValues.length === 3) { + brushColor = `${channelValues[0]},${channelValues[1]},${channelValues[2]}`; + } else if (channelValues.length === 2) { + brushColor = `${channelValues[0]},${channelValues[1]},0`; + } + if (available > channelCount) { + const alphaValue = parsedArray[channelCount]; + const alphaToken = rawTokens[channelCount]; + segments.push(formatAlphaToken(alphaToken, alphaValue)); + consumed += 1; + alphaIncluded = true; + } + return { + text: segments.join(" "), + consumed, + brushColor, + alphaIncluded + }; +} +function isFadeColorObject(colorParams) { + return colorParams && typeof colorParams === "object" && !Array.isArray(colorParams) && colorParams.type === "fade"; +} +function highlightColorTokens(colorParams, rawParams = []) { + if (!rawParams || rawParams.length === 0) { + return { text: "", brushColor: null }; + } + const remaining = [...rawParams]; + const segments = []; + let brushColor = null; + let alphaHandled = false; + const pushSegment = (text2, { spaceBefore = true } = {}) => { + if (!text2) return; + segments.push({ text: text2, spaceBefore }); + }; + if (isFadeColorObject(colorParams)) { + const first = remaining.shift(); + if (first) { + pushSegment(colorizeColorName(first), { spaceBefore: false }); + } + if (typeof colorParams.alpha === "number" && remaining.length > 0) { + pushSegment(formatAlphaToken(remaining.shift(), colorParams.alpha)); + alphaHandled = true; + } + } else if (isHexColorToken(remaining[0])) { + const { text: text2, consumed, brushColor: hexColor, alphaIncluded } = colorizeHexToken(remaining[0]); + pushSegment(text2, { spaceBefore: false }); + remaining.splice(0, consumed); + brushColor = hexColor; + alphaHandled = alphaIncluded; + } else if (Array.isArray(colorParams) && colorParams.length > 0 && isNumericLikeToken(remaining[0])) { + const { text: text2, consumed, brushColor: numericColor, alphaIncluded } = colorizeNumericChannels(remaining, colorParams); + pushSegment(text2, { spaceBefore: false }); + remaining.splice(0, consumed); + brushColor = numericColor; + alphaHandled = alphaIncluded; + } else { + const first = remaining.shift(); + if (first) { + pushSegment(colorizeColorName(first), { spaceBefore: false }); + } + } + if (!alphaHandled && remaining.length > 0 && isNumericLikeToken(remaining[0])) { + const alphaToken = remaining.shift(); + const alphaValue = parseAlphaFromToken(alphaToken); + pushSegment(formatAlphaToken(alphaToken, alphaValue)); + alphaHandled = true; + } + remaining.forEach((token) => { + pushSegment(`\\orange\\${token}`); + }); + const text = segments.reduce((acc, seg, index) => { + if (index === 0 || seg.spaceBefore === false) { + return acc + seg.text; + } + return `${acc} ${seg.text}`; + }, ""); + return { text, brushColor }; +} +function getColorTokenHighlight(token) { + if (!token) return "white"; + const cleanToken = token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1) : token; + if (cleanToken.startsWith("#") && cleanToken.length > 1) { + const codepart = cleanToken.substring(1); + if (/^[0-9A-Za-z]{1,8}$/.test(codepart)) { + const isHexColor = /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{4}$|^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{8}$/.test(codepart); + if (!isHexColor) { + return "COMPOUND:magenta:orange"; + } + } + } + const lowerToken = cleanToken.toLowerCase(); + if (cssColors2 && cssColors2[lowerToken]) { + const colorValue = cssColors2[lowerToken]; + if (Array.isArray(colorValue) && colorValue.length >= 3) { + const rgbColor = `${colorValue[0]},${colorValue[1]},${colorValue[2]}`; + return rgbColor; + } + } + if (lowerToken.match(/^c\d+$/)) { + const index = parseInt(lowerToken.substring(1)); + if (staticColorMap && staticColorMap[index]) { + const colorValue = staticColorMap[index]; + if (Array.isArray(colorValue) && colorValue.length >= 3) { + const rgbColor = `${colorValue[0]},${colorValue[1]},${colorValue[2]}`; + return rgbColor; + } + } + } + if (lowerToken === "rainbow") { + return "RAINBOW"; + } + if (lowerToken === "zebra") { + return "ZEBRA"; + } + if (lowerToken.startsWith("fade:")) { + return `FADE:${cleanToken}`; + } + return "orange"; +} +function colorizeColorName(colorName) { + if (!colorName) return colorName; + const cleanName = colorName.startsWith('"') && colorName.endsWith('"') ? colorName.slice(1, -1) : colorName; + if (isHexColorToken(cleanName)) { + const parsedHex = parseHexColorToken(cleanName); + if (parsedHex && parsedHex.rgb) { + const [r2, g, b2] = parsedHex.rgb; + const colorValue = `${r2},${g},${b2}`; + return `\\${colorValue}\\${parsedHex.display}`; + } + } + const color3 = getColorTokenHighlight(cleanName); + if (color3 === "RAINBOW" && cleanName === "rainbow") { + const rainbowColors2 = ["red", "orange", "yellow", "lime", "blue", "purple", "magenta"]; + let result = ""; + for (let charIndex = 0; charIndex < cleanName.length; charIndex++) { + const charColor = rainbowColors2[charIndex % rainbowColors2.length]; + result += `\\${charColor}\\${cleanName[charIndex]}`; + } + return result; + } + if (color3 === "ZEBRA" && cleanName === "zebra") { + const zebraColors2 = ["black", "white"]; + let result = ""; + for (let charIndex = 0; charIndex < cleanName.length; charIndex++) { + const charColor = zebraColors2[charIndex % zebraColors2.length]; + result += `\\${charColor}\\${cleanName[charIndex]}`; + } + return result; + } + if (cleanName.startsWith("fade:") && color3 === "mediumseagreen") { + return colorFadeExpression(cleanName); + } + return `\\${color3}\\${cleanName}`; +} +function colorFadeExpression(fadeToken) { + if (!fadeToken.startsWith("fade:")) { + return fadeToken; + } + const parts = fadeToken.split(":"); + if (parts.length < 2) { + return fadeToken; + } + let isNeat = false; + let colorPart = parts[1]; + let direction = parts[2]; + if (parts[1] === "neat" && parts[2]) { + isNeat = true; + colorPart = parts[2]; + direction = parts[3]; + } else if (parts[2] === "neat") { + isNeat = true; + direction = void 0; + } else if (parts[3] === "neat") { + isNeat = true; + direction = parts[2]; + } else if (parts.includes("neat")) { + isNeat = true; + const filteredParts = parts.filter((p) => p !== "neat"); + if (filteredParts.length >= 2) { + colorPart = filteredParts[1]; + direction = filteredParts[2]; + } + } + const colorNames = colorPart.split("-"); + let result = "\\mediumseagreen\\fade\\lime\\:"; + if (isNeat) { + result += "\\cyan\\neat\\lime\\:"; + } + for (let i2 = 0; i2 < colorNames.length; i2++) { + const colorName = colorNames[i2]; + let colorValue = "white"; + if (cssColors2 && cssColors2[colorName]) { + const rgbColor = cssColors2[colorName]; + if (Array.isArray(rgbColor) && rgbColor.length >= 3) { + colorValue = `${rgbColor[0]},${rgbColor[1]},${rgbColor[2]}`; + } + } else if (colorName.match(/^c\d+$/)) { + const index = parseInt(colorName.substring(1)); + if (staticColorMap && staticColorMap[index]) { + const rgbColor = staticColorMap[index]; + if (Array.isArray(rgbColor) && rgbColor.length >= 3) { + colorValue = `${rgbColor[0]},${rgbColor[1]},${rgbColor[2]}`; + } + } + } else if (colorName === "rainbow") { + colorValue = "RAINBOW"; + } else if (colorName === "zebra") { + colorValue = "ZEBRA"; + } + if (colorValue === "RAINBOW") { + const rainbowColors2 = ["red", "orange", "yellow", "lime", "blue", "purple", "magenta"]; + for (let charIndex = 0; charIndex < colorName.length; charIndex++) { + const charColor = rainbowColors2[charIndex % rainbowColors2.length]; + result += `\\${charColor}\\${colorName[charIndex]}`; + } + } else if (colorValue === "ZEBRA") { + const zebraColors2 = ["black", "white"]; + for (let charIndex = 0; charIndex < colorName.length; charIndex++) { + const charColor = zebraColors2[charIndex % zebraColors2.length]; + result += `\\${charColor}\\${colorName[charIndex]}`; + } + } else { + result += `\\${colorValue}\\${colorName}`; + } + if (i2 < colorNames.length - 1) { + result += "\\mediumseagreen\\-"; + } + } + if (direction) { + result += "\\lime\\:"; + const numericAngle = parseFloat(direction); + if (!isNaN(numericAngle)) { + result += `\\yellow\\${direction}`; + } else { + result += `\\cyan\\${direction}`; + } + } + return result; +} +function generateNopaintHUDLabel(brushName, colorParams, rawParams, modifiers = "") { + const { text: colorSection, brushColor } = highlightColorTokens(colorParams, rawParams); + const brushColorCode = brushColor || "white"; + let label = `\\${brushColorCode}\\${brushName}`; + if (modifiers) { + const parts = modifiers.split(":"); + let coloredMods = ""; + for (const part of parts) { + if (part === "") { + coloredMods += "\\gray\\:"; + } else { + coloredMods += `\\yellow\\${part}`; + } + } + label += coloredMods; + } + if (colorSection) { + label += ` ${colorSection}`; + } else if (rawParams && rawParams.length > 0) { + const fallback = rawParams.map((token) => `\\orange\\${token}`).join(" "); + label += ` ${fallback}`; + } + return label; +} + +// public/aesthetic.computer/systems/nopaint.mjs +function stripColorCodes3(str7) { + if (!str7) return ""; + return str7.replace(/\\[^\\]*\\/g, ""); +} +var state = "idle"; +var previousState = "idle"; +var bakeFlashTime = 0; +var bakeFlashDuration = 300; +var needsPaintRef = null; +var animationId = null; +var cursor = { x: 0, y: 0 }; +var sessionId = null; +var logBuffer = []; +var LOG_BUFFER_SIZE = 100; +var currentStrokePointCount = 0; +function initSessionLogging(api) { + if (!sessionId) { + sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + log2(api, "SESSION_START", { sessionId, timestamp: Date.now(), userAgent: navigator.userAgent }); + } +} +function log2(api, event, data = {}) { + const timestamp2 = Date.now(); + const logEntry = { + timestamp: timestamp2, + sessionId, + event, + data: { ...data } + }; + logBuffer.push(logEntry); + if (logBuffer.length > LOG_BUFFER_SIZE) { + logBuffer.shift(); + } + const device = data.device || "unknown"; + const filename = `${device}_touch_debug.log`; + try { + api.net.log(`/tmp/dev-logs/${filename}`, JSON.stringify(logEntry)); + } catch (error) { + console.error("Failed to send log:", error); + } +} +function nopaint_boot({ + system: system2, + store: store2, + nopaint: nopaint2, + screen: screen2, + wipe, + api, + params, + colon, + hud, + num, + painting: painting2 +}) { + cursor.x = screen2.width / 2; + cursor.y = screen2.height / 2; + nopaint_adjust(api); + initSessionLogging(api); + if (!system2.painting) { + console.warn("\u{1F97E} nopaint_boot: system.painting is undefined, skipping nopaint initialization"); + return; + } + system2.nopaint.buffer = painting2( + system2.painting.width, + system2.painting.height, + (p) => { + p.wipe(255, 255, 255, 0); + } + ); + if (params && num) { + system2.nopaint.color = num.parseColor(params); + const modifiers = colon && colon.length > 0 ? `:${colon.join(":")}` : ""; + const brushName = api.slug || "brush"; + const pieceName = brushName.split("~")[0].split(":")[0]; + const label = generateNopaintHUDLabel(pieceName, system2.nopaint.color, params, modifiers); + if (typeof window !== "undefined" && window.currentHUDTxt !== void 0) { + window.currentHUDTxt = label; + window.currentHUDPlainTxt = stripColorCodes3(label); + } else if (hud && hud.label) { + hud.label(label); + } + } + system2.nopaint.present(api); +} +function nopaint_is(stateQuery) { + return state === stateQuery; +} +function nopaint_cancelStroke() { + state = "idle"; +} +function nopaint_triggerBakeFlash() { + bakeFlashTime = performance.now(); + if (animationId) { + cancelAnimationFrame(animationId); + } + const animateFlash = () => { + const currentTime = performance.now(); + const timeSinceBake = currentTime - bakeFlashTime; + if (timeSinceBake < bakeFlashDuration) { + if (needsPaintRef) { + needsPaintRef(); + } + animationId = requestAnimationFrame(animateFlash); + } else { + animationId = null; + } + }; + animationId = requestAnimationFrame(animateFlash); +} +function nopaint_act({ + event: e2, + download, + screen: screen2, + system: system2, + painting: painting2, + loading: loading2, + store: store2, + pens, + pen, + api, + num, + jump: jump2, + debug: debug4 +}) { + if (system2?.nopaint?.robotActive && e2?.device && e2.device !== "robot") { + const penEventNames = ["touch", "lift", "draw", "move"]; + const isPenEvent = penEventNames.some((name) => e2.is?.(name) || e2.is?.(`${name}:1`) || e2.is?.(`${name}:2`)); + if (isPenEvent) return; + } + if (e2.device && (e2.device === "touch" || e2.device === "pen" || e2.device === "robot" || e2.device !== "mouse")) { + log2(api, "RAW_EVENT", { + device: e2.device, + eventType: e2.type || "unknown", + touchState: e2.is ? `touch:${e2.is("touch:1") ? "1" : e2.is("touch:2") ? "2" : "none"}` : "no-is-method", + liftState: e2.is ? `lift:${e2.is("lift:1") ? "1" : e2.is("lift:2") ? "2" : "none"}` : "no-is-method", + drawState: e2.is ? `draw:${e2.is("draw:1") ? "1" : "none"}` : "no-is-method", + rawX: e2.x, + rawY: e2.y, + currentPaintingState: nopaint_is("painting"), + timestamp: num.timestamp() + }); + } + if (e2.is && (e2.is("lift:1") || e2.is("lift:2"))) { + } + if (e2.is("touch:1")) { + const timestamp2 = performance.now(); + state = "painting"; + system2.nopaint.updateBrush(api, "touch"); + system2.nopaint.finalDragBox = null; + system2.nopaint.finalStartDrag = null; + system2.nopaint.finalEndPoint = null; + currentStrokePointCount = 0; + log2(api, "TOUCH_START", { + device: e2.device, + rawX: e2.x, + rawY: e2.y, + brushX: system2.nopaint.brush.x, + brushY: system2.nopaint.brush.y, + state, + timestamp: num.timestamp() + }); + system2.nopaint.gestureRecord.push([ + num.timestamp(), + "touch:1", + system2.nopaint.brush.x, + system2.nopaint.brush.y + ]); + } + if (nopaint_is("painting") && e2.is("draw:1")) { + system2.nopaint.updateBrush(api, "draw"); + const rec = system2.nopaint.gestureRecord; + const lastRecord = rec.length > 0 ? rec[rec.length - 1] : null; + const shouldAddPoint = !lastRecord || system2.nopaint.brush.x !== lastRecord[2] || system2.nopaint.brush.y !== lastRecord[3]; + if (shouldAddPoint) { + currentStrokePointCount++; + log2(api, "DRAW_POINT", { + device: e2.device, + rawX: e2.x, + rawY: e2.y, + brushX: system2.nopaint.brush.x, + brushY: system2.nopaint.brush.y, + state, + pointCount: currentStrokePointCount, + timestamp: num.timestamp() + }); + rec.push([ + num.timestamp(), + "draw:1", + system2.nopaint.brush.x, + system2.nopaint.brush.y + ]); + } + } + if (nopaint_is("painting") && e2.is("lift:1") && (e2.device === "mouse" || e2.device === "pen" || e2.device === "touch" || e2.device === "robot")) { + const timestamp2 = performance.now(); + state = "idle"; + if (!system2.nopaint.bakeOnLeave) system2.nopaint.needsBake = true; + api.needsPaint(); + log2(api, "TOUCH_END", { + device: e2.device, + rawX: e2.x, + rawY: e2.y, + brushX: system2.nopaint.brush.x, + brushY: system2.nopaint.brush.y, + previousState: "painting", + newState: state, + gestureLength: currentStrokePointCount + 1, + // +1 to include the final point + timestamp: num.timestamp() + }); + system2.nopaint.gestureRecord.push([ + num.timestamp(), + "lift:1", + system2.nopaint.brush.x, + system2.nopaint.brush.y + ]); + if (system2.nopaint.brush && system2.nopaint.brush.dragBox) { + system2.nopaint.finalDragBox = { + x: system2.nopaint.brush.dragBox.x, + y: system2.nopaint.brush.dragBox.y, + w: system2.nopaint.brush.dragBox.w, + h: system2.nopaint.brush.dragBox.h + }; + } + if (system2.nopaint.brush) { + system2.nopaint.finalEndPoint = { + x: system2.nopaint.brush.x, + y: system2.nopaint.brush.y + }; + } + if (system2.nopaint.startDrag) { + system2.nopaint.finalStartDrag = { + x: system2.nopaint.startDrag.x, + y: system2.nopaint.startDrag.y + }; + } + system2.nopaint.brush = null; + system2.nopaint.startDrag = null; + } + if (e2.is("move")) { + cursor.x = pen.x; + cursor.y = pen.y; + if (nopaint_is("painting")) { + system2.nopaint.updateBrush(api, "move"); + } + if (nopaint_is("painting") || system2.nopaint.needsBake || system2.nopaint.needsPresent) { + api.needsPaint(); + } + } + if (e2.is("keyboard:down:arrowup")) { + system2.nopaint.zoom(api, "in", cursor); + system2.nopaint.present(api); + if (system2.nopaint.brush?.dragBox) { + api.needsPaint(); + } + } + if (e2.is("keyboard:down:arrowdown")) { + system2.nopaint.zoom(api, "out", cursor); + system2.nopaint.present(api); + if (system2.nopaint.brush?.dragBox) { + api.needsPaint(); + } + } + if (!nopaint_is("panning") && (e2.is("keyboard:down:shift") || (e2.is("touch:2") || e2.is("touch:1")) && pens().length === 2)) { + previousState = state; + state = "panning"; + } + if (nopaint_is("panning") && (e2.is("move") && e2.device === "mouse" || e2.is("draw"))) { + system2.nopaint.translate(api, e2.delta.x, e2.delta.y); + const p = pens(); + if (p.length === 2) { + } + system2.nopaint.present(api); + api.needsPaint(); + } + if (nopaint_is("panning") && (e2.is("keyboard:up:shift") || e2.is("lift:2") || e2.is("lift:1") || e2.is("keyboard") && !e2.shift)) { + if ((e2.is("lift:1") || e2.is("lift:2")) && previousState === "painting") { + state = "idle"; + } else { + state = previousState; + } + previousState = "idle"; + system2.nopaint.storeTransform(store2, system2); + } + if (nopaint_is("panning") && (e2.is("keyboard:down:meta") || e2.is("touch:3"))) { + state = previousState; + previousState = "idle"; + system2.nopaint.resetTransform(api); + system2.nopaint.present(api); + } + if (e2.is("reframed")) { + nopaint_adjust(api); + system2.nopaint.present(api); + } +} +function nopaint_adjust(api, size = null, slug = "resize") { + const { screen: screen2, system: sys, painting: painting2, store: store2, dark, theme } = api; + if (!size && store2["painting:resolution-lock"] === true) return; + if (!size && (sys.nopaint.translation.x !== 0 || sys.nopaint.translation.y !== 0)) { + return; + } + let resizing = false; + if (!size) { + if (sys.nopaint.undo.paintings.length === 0) { + size = { w: screen2.width, h: screen2.height }; + } else { + size = { + w: Math.max(screen2.width, sys.painting?.width || 0), + h: Math.max(screen2.height, sys.painting?.height || 0) + }; + } + resizing = true; + } + if (size || !sys.painting) { + let width2, height2; + if (size.w && size.h) { + if (typeof size.w === "string") { + width2 = size.w.endsWith("x") ? parseFloat(size.w.slice(0, -1)) * (sys.painting?.width || screen2.width) : parseInt(size.w); + } else { + width2 = size.w; + } + if (typeof size.h === "string") { + height2 = size.h.endsWith("x") ? parseFloat(size.h.slice(0, -1)) * (sys.painting?.height || screen2.height) : parseInt(size.h); + } else { + height2 = size.h; + } + } else { + width2 = screen2.width; + height2 = screen2.height; + } + if (isNaN(width2) || isNaN(height2)) return false; + const savedPan = api.graph?.savepan ? { x: api.graph.panTranslation?.x || 0, y: api.graph.panTranslation?.y || 0 } : null; + if (api.graph?.unpan) api.graph.unpan(); + sys.painting = painting2(width2, height2, (p) => { + if (size?.scale) { + p.paste(sys.painting, 0, 0, { width: width2, height: height2 }); + } else { + p.wipe(theme[dark ? "dark" : "light"].wipeNum); + const isNewPainting = slug && (slug === "new" || slug.startsWith("new~")); + if (!isNewPainting && sys.painting) { + p.paste(sys.painting); + } + } + }); + store2["painting"] = { + width: sys.painting.width, + height: sys.painting.height, + pixels: sys.painting.pixels + }; + if (!resizing) sys.nopaint.addUndoPainting(sys.painting, slug); + } + if (size && !resizing) { + store2["painting:resolution-lock"] = true; + store2.persist("painting:resolution-lock", "local:db"); + store2.persist("painting", "local:db"); + sys.nopaint.resetTransform({ system: sys, screen: screen2 }); + sys.nopaint.storeTransform(store2, sys); + } + return true; +} +function nopaint_renderPerfHUD({ + ink: ink3, + write, + box: box2, + screen: screen2, + pen, + system: system2, + nopaintPerf: nopaintPerf2 +}) { + if (!nopaintPerf2 || !ink3 || !write || !box2) return; + ink3("black"); + if (needsPaintRef) { + needsPaintRef(); + } + const hudWidth = 90; + const hudHeight = 80; + const x = screen2.width - hudWidth - 2; + const y = 2; + const currentTime = performance.now(); + const timeSinceBake = currentTime - bakeFlashTime; + const isFlashing = timeSinceBake < bakeFlashDuration; + if (isFlashing) { + const flashIntensity = Math.max(0, 1 - timeSinceBake / bakeFlashDuration); + const flashAlpha = Math.floor(100 + flashIntensity * 155); + ink3(255, 255, 200, flashAlpha); + } else { + ink3(0, 0, 0, 200); + } + box2(x - 1, y - 1, hudWidth + 2, hudHeight + 2, "fill"); + if (isFlashing) { + const flashIntensity = Math.max(0, 1 - timeSinceBake / bakeFlashDuration); + const flashColor = Math.floor(100 + flashIntensity * 155); + ink3(flashColor, 255, flashColor, 255); + } else { + ink3(100, 255, 100, 255); + } + box2(x - 1, y - 1, hudWidth + 2, hudHeight + 2, "outline"); + let lineY = y + 1; + const lineHeight = 8; + ink3(255, 255, 255); + write("NOPAINT PERF", { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight + 2; + if (pen) { + ink3(100, 200, 255); + const penX = typeof pen.x === "number" ? pen.x : 0; + const penY = typeof pen.y === "number" ? pen.y : 0; + write(`X:${penX.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + ink3(100, 200, 255); + write(`Y:${penY.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + if (pen.dragBox && typeof pen.dragBox.x === "number" && typeof pen.dragBox.y === "number") { + ink3(255, 100, 200); + write(`DRG X:${pen.dragBox.x.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + write(`DRG Y:${pen.dragBox.y.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + if (typeof pen.dragBox.w === "number") { + write(`DRG W:${pen.dragBox.w.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + } + if (typeof pen.dragBox.h === "number") { + write(`DRG H:${pen.dragBox.h.toFixed(1)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + } + } + if (pen.pressure !== void 0 && typeof pen.pressure === "number") { + ink3(150, 150, 255); + write(`PRESS:${pen.pressure.toFixed(2)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + } + } else { + ink3(255, 0, 0); + write("NO PEN DATA", { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + } + if (system2?.nopaint) { + ink3(255, 255, 0); + write(`STATE:${state}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + ink3(200, 200, 0); + write(`PREV:${previousState}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + const isPainting = nopaint_is("painting"); + ink3(isPainting ? [0, 255, 0] : [255, 0, 0]); + write(`PAINT:${isPainting ? "YES" : "NO"}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + lineY += lineHeight; + const now = performance.now(); + ink3(128, 128, 128); + write(`TIME:${Math.floor(now % 1e4)}`, { x, y: lineY }, void 0, void 0, false, "MatrixChunky8"); + } +} + +// public/aesthetic.computer/systems/prompt-system.mjs +var conversation; +var input; +var abort; +var messageComplete = true; +var processing = false; +var thinking = false; +var cancel; +function setThinking($, value) { + thinking = value; + if ($?.system?.prompt) $.system.prompt.thinking = value; + $?.needsPaint?.(); +} +function playThinkingMelody($) { + const notes = [440, 554, 659]; + notes.forEach((tone, i2) => { + setTimeout(() => { + $.sound?.synth?.({ + type: "sine", + tone, + attack: 0.01, + decay: 0.92, + volume: 0.18, + duration: 0.08 + }); + }, i2 * 90); + }); +} +async function prompt_boot($, { prompt, program, hint, forgetful, memory, gutterMax, lineSpacing }, reply, halt, scheme, wrap2, copied, activated) { + messageComplete = true; + processing = false; + conversation = new Conversation($.store, $.slug, forgetful, memory); + const messages = []; + if (messages.length > 0) { + prompt = messages[messages.length - 1].text; + } else { + prompt = prompt?.replaceAll("@", $.handle() || "pal"); + } + input = new $.ui.TextInput( + $, + prompt, + async (text) => { + const exits = ["q", "quit", "leave", "exit", "forget", "bye"]; + if (exits.indexOf(text) !== -1 && $.slug !== "prompt") { + await conversation.forget(); + input.blank(); + if ($.slug.indexOf("botce") > -1) { + return $.net.refresh(); + } else { + return $.jump("prompt"); + } + } + input.lock = true; + $.send({ type: "keyboard:lock" }); + input.enter.btn.disabled = true; + input.paste.btn.disabled = true; + input.paste.btn.removeFromDom($, "paste"); + input.canType = false; + input._preventDeactivation = true; + let halted = await halt?.($, text); + if (!$.leaving()) { + input.lock = false; + $.send({ type: "keyboard:unlock" }); + } + if (halted) { + messageComplete = true; + if (halted.left) { + return; + } + if ($.leaving()) { + return; + } + if (halted.replied) { + input.lock = false; + $.send({ type: "keyboard:unlock" }); + input.runnable = false; + input.bakePrintedText(); + input.showButton($); + $.needsPaint(); + $.send({ type: "keyboard:close" }); + return; + } + reply?.(input.text); + input.bakePrintedText(); + input.runnable = false; + input.showButton($); + $.needsPaint(); + input.canType = true; + setTimeout(() => { + input._preventDeactivation = false; + }, 200); + return; + } + processing = input.lock = true; + $.send({ type: "keyboard:close" }); + $.send({ type: "keyboard:lock" }); + let firstAnd = true; + input.submittedText = ""; + cancel = function() { + setThinking($, false); + abort?.(); + }; + setThinking($, true); + playThinkingMelody($); + abort = conversation.ask( + { prompt: text, program, hint }, + function and(msg) { + setThinking($, false); + msg = msg.replace(/[\u2018\u2019\u201C\u201D]/g, (match) => { + if (match === "\u2018" || match === "\u2019") { + return "'"; + } else { + return '"'; + } + }); + msg = msg.replace(/—/g, "-"); + if (firstAnd) input.submittedText = input.text; + input.text = firstAnd ? input.text + "\n\n" + msg : input.text + msg; + input.snap(); + firstAnd = false; + }, + function done() { + setThinking($, false); + messageComplete = true; + processing = input.lock = false; + $.send({ type: "keyboard:unlock" }); + reply?.(input.text, input); + input.bakePrintedText(); + input.clearUserText(); + $.send({ type: "keyboard:text:replace", content: { text: "" } }); + input.runnable = false; + input.showButton($); + $.needsPaint(); + $.sound.synth({ + type: "sine", + tone: 500, + attack: 0.1, + decay: 0.96, + volume: 0.65, + duration: 0.05 + }); + }, + function fail() { + setThinking($, false); + input.activate(input); + input.text = ""; + input.snap(); + $.notice("NETWORK FAILURE", ["yellow", "red"]); + $.send({ type: "keyboard:text:replace", content: { text: "" } }); + $.needsPaint(); + reply?.(input.text); + input.submittedText = ""; + processing = input.lock = false; + $.send({ type: "keyboard:unlock" }); + $.send({ type: "keyboard:open" }); + input.runnable = false; + } + ); + }, + { + // autolock: false, + wrap: wrap2, + scheme, + copied, + activated, + didReset: () => { + messageComplete = true; + }, + gutterMax, + lineSpacing + } + ); + $.needsPaint(); + $.system.prompt = { input, convo: conversation, thinking: false }; +} +function prompt_sim($) { + input?.sim($); +} +function prompt_paint($) { + return input?.paint($); +} +function prompt_leave() { + abort?.(); +} +function prompt_act($) { + const { event: e2, slug } = $; + if (!messageComplete && processing && e2.is("keyboard:down")) { + cancel(); + } + let inputHandled = false; + if (e2.is("ui:cancel-interactions") || e2.is("move") || e2.is("draw") || e2.is("touch") || e2.is("lift") || e2.is("focus") || e2.is("reframed") || e2.is("defocus") || e2.is("keyboard:open") || e2.is("keyboard:close") || // e.is("pasted:text") || + e2.is("prompt:text:replace") || e2.is("prompt:text:select") || e2.is("prompt:text:cursor") || e2.of?.("clipboard")) { + input?.act($); + inputHandled = true; + } + if (!messageComplete && !processing && !inputHandled) input?.act($); + if (messageComplete && e2.is("keyboard:down")) { + if (!inputHandled) input?.act($); + messageComplete = false; + } +} + +// public/aesthetic.computer/systems/world.mjs +var me; +var kids; +var world; +var cam; +var input2; +var inputBtn; +var server; +var spectating = false; +var spectatingKid; +var map2 = false; +var { keys, values } = Object; +async function world_boot({ + api, + help, + handle: handle2, + screen: screen2, + ui, + send: send2, + net: { socket: socket2 }, + sound: sound2, + store: store2, + piece, + system: system2, + num: { number: number2 } +}, worldData) { + kids = {}; + world = new World(worldData?.width, worldData?.height); + let pos; + if (system2.world.teleported === false) { + pos = await store2.retrieve(`world:${piece}:pos`) || { + x: void 0, + y: void 0 + }; + } else { + pos = { ...system2.world.telepos }; + } + console.log("\u{1F5FA}\uFE0F Loaded position:", pos); + me = new Kid( + handle2(), + { + x: number2(pos.x) ? pos.x : world.size.width / 2, + y: number2(pos.y) ? pos.y : world.size.height / 2 + }, + help.choose("meh", "smile", "frown") + ); + system2.world.me = me; + system2.world.size = world.size; + cam = new Cam( + screen2.width / 2 - me.pos.x, + screen2.height / 2 - me.pos.y, + me.pos + ); + const scheme = { + dark: { + text: 255, + background: [0, 100], + block: 255, + highlight: 0, + guideline: 255 + } + }; + input2 = new ui.TextInput( + api, + "...", + async (text) => { + if (text === "smile" || text === "frown" || text === "sad" || text === "meh") { + if (text === "sad") text = "frown"; + me.mood(text); + server.send(`world:${piece}:mood`, me.face); + } else if (text === "red" || text === "yellow" || text === "orange" || text === "black" || text === "brown" || text === "purple" || text === "pink" || text === "blue" || text === "lime" || text === "white") { + me.tint(text); + server.send(`world:${piece}:tint`, me.color); + } else if (text === "show") { + if (system2.painting) { + console.log("\u{1F5BC}\uFE0F Showing:", system2.painting); + server.send( + `world:${piece}:show`, + help.serializePainting(system2.painting) + ); + me.showing = system2.painting; + } else { + console.log("\u274C\u{1F5BC}\uFE0F Nothing to show."); + } + } else if (text === "hide") { + server.send(`world:${piece}:hide`); + me.showing = null; + } else if (text === "map") { + map2 = !map2; + } else { + server.send(`world:${piece}:write`, text); + } + input2.text = ""; + input2.showBlink = false; + input2.mute = true; + send2({ type: "keyboard:close" }); + }, + { + // autolock: false, + // wrap, + scheme, + // copied, + // activated, + // didReset: () => { + // messageComplete = true; + // }, + // gutterMax, + // lineSpacing, + hideGutter: true, + closeOnEmptyEnter: true + } + ); + send2({ type: "keyboard:soft-lock" }); + inputBtn = new ui.Button(); + server = socket2((id, type, content) => { + if (type === "left") { + console.log("\uFE0F\u270C\uFE0F Goodbye:", id, kids[id]?.handle); + delete kids[id]; + return; + } + if (type === "joined") { + console.log("\uFE0F\u{1F44B} Hello:", id, type, content); + return; + } + if (type.startsWith("connected")) { + if (me.handle === "?") me.handle = `nub${id}`; + server.send(`world:${piece}:join`, { + handle: me.handle, + pos: me.pos, + face: me.face, + showing: help.serializePainting(me.showing), + ghost: me.ghost + }); + console.log("\u{1FAB4} Welcome:", me.handle, `(${id})`); + return; + } + if (type === `world:${piece}:ghost`) { + if (kids[id]) { + kids[id].ghost = true; + console.log("\u{1F47B} Ghosted:", kids[id].handle); + } + return; + } + if (type === `world:${piece}:kick`) { + delete kids[id]; + return; + } + if (type === `world:${piece}:list`) { + console.log(`\u{1F5DE}\uFE0F Got list of all '${piece}' clients...`, content); + keys(content).forEach((key) => { + if (!kids[key]) { + const data = content[key]; + if (content[key].handle.startsWith("@") && content[key].handle === me.handle && content[key].ghost === false) { + spectating = true; + spectatingKid = key; + console.log("\u{1F453} Spectating:", spectatingKid); + } + console.log("\u{1F9D2} Joined:", data.handle || id, data); + kids[key] = new Kid( + data.handle || `nub${id}`, + data.pos, + data.face, + true + ); + if (data.ghost) kids[key].ghost = data.ghost; + if (data.showing) + kids[key].showing = help.deserializePainting(data.showing); + } + }); + return; + } + if (type === `world:${piece}:join`) { + console.log("\u{1F5FA}\uFE0F Joining world:", type, content); + if (!kids[id]) { + if (content.handle.startsWith("@")) { + if (content.handle === me.handle) { + console.log("\u{1F453} Spectator joined:", content); + return; + } else { + keys(kids).forEach((key) => { + const kid = kids[key]; + console.log(kid, content.handle, kid.handle); + if (content.handle === kid.handle && kid.ghost) { + console.log("\u{1F47B} Unghosting:", kid.handle); + delete kids[key]; + } + }); + } + } + kids[id] = new Kid( + content.handle || `nub${id}`, + content.pos, + content.face, + true + ); + if (content.ghost) kids[id].ghost = true; + if (content.showing) + kids[id].showing = help.deserializePainting(content.showing); + } + return; + } + if (type === `world:${piece}:tint`) { + const kid = kids[id]; + if (kid) kid.tint(content); + return; + } + if (type === `world:${piece}:mood`) { + const kid = kids[id]; + if (kid) kid.mood(content); + return; + } + if (type === `world:${piece}:slug`) { + const kid = kids[id]; + if (kid) kid.slug(content.slug); + return; + } + if (type === `world:${piece}:write`) { + console.log("\u{1FAA7} Write from:", id, "You:", server.id, "Text:", content); + if (id === server.id) { + me.write(content); + return; + } + const kid = kids[id]; + if (kid) { + kid.write(content); + sound2.synth({ + type: "sine", + tone: 950, + attack: 0.1, + decay: 0.96, + volume: 0.65, + duration: 0.015 + }); + } + return; + } + if (type === `world:${piece}:write:clear`) { + const kid = kids[id]; + if (kid) kid.write(null); + return; + } + if (type === `world:${piece}:move`) { + const kid = kids[id]; + if (kid) kid.netPos = content.pos; + return; + } + if (type === `world:${piece}:show`) { + const kid = kids[id]; + if (kid) kid.showing = help.deserializePainting(content); + return; + } + if (type === `world:${piece}:hide`) { + const kid = kids[id]; + if (kid) kid.showing = null; + return; + } + }); +} +function world_paint({ api, ink: ink3, pan: pan2, unpan: unpan2, pen, screen: screen2, leaving: leaving2, hud, typeface }, paint2, curtain) { + pan2(cam.x, cam.y); + paint2?.(api, world); + if (!spectating) { + inputBtn.paint((btn) => { + ink3("white", btn.down && btn.over ? 128 : 64).circle( + me.pos.x, + me.pos.y, + btn.box.w / 2, + true + ); + }); + } + if (!spectating) me.paint(api); + unpan2(); + keys(kids).forEach((key) => { + pan2(cam.x, cam.y); + const kid = kids[key]; + kid.paint(api); + unpan2(); + }); + [me, ...values(kids)].forEach((kid, i2) => { + const row = i2 * 12; + const handleText = i2 === 0 ? `you are ${kid.handle}` : kid.handle; + const kidOnScreen = i2 !== 0 && onScreen(kid, world, cam, screen2); + const pos = kidOnScreen ? { + x: kid.pos.x - handleText.length / 2 * typeface.blockWidth, + y: kid.pos.y + 22 + } : { x: 6, y: 21 + row }; + if (kidOnScreen) pan2(cam.x, cam.y); + ink3("black").write(handleText, { x: pos.x + 1, y: pos.y + 1 }); + ink3(me === kid ? "yellow" : kid.color).write(handleText, pos); + if (kidOnScreen) unpan2(); + if (i2 === 0) + ink3(kid.color).write(handleText.replace(kid.handle, "").trim(), { + x: 6, + y: 21 + row + }); + }); + const statusColor = hud.currentStatusColor(); + if (typeof statusColor === "string" && statusColor !== "lime") { + ink3(statusColor, 128).box(0, 0, screen2.width, screen2.height); + } + const l2 = me.leash; + if (l2.start) { + if (pen) ink3(0, 255, 0, 90).line(l2.start.x, l2.start.y, pen.x, pen.y); + ink3(l2.len > l2.deadzone ? "yellow" : "red").line( + l2.start.x, + l2.start.y, + l2.start.x + me.leash.x, + l2.start.y + me.leash.y + ); + } + curtain?.(api); + if (input2.canType && !leaving2()) { + input2.paint(api, false, { + x: 0, + y: 18, + width: screen2.width, + height: screen2.height - 18 + }); + } + if (map2) { + const worldAspect = world.width / world.height; + const screenAspect = screen2.width / screen2.height; + const scale7 = (worldAspect > screenAspect ? screen2.width / world.width : screen2.height / world.height) * 0.5; + const bw = world.width * scale7, bh = world.height * scale7; + const x = screen2.width / 2 - bw / 2, y = screen2.height / 2 - bh / 2; + ink3("white", 96).box(x, y, bw, bh); + ink3("red", 64).box( + x + cam.dolly.x * scale7, + y + cam.dolly.y * scale7, + screen2.width * scale7, + screen2.height * scale7, + "center" + ); + [me, ...values(kids)].forEach((kid, i2) => { + ink3(kid.color).box(x + kid.pos.x * 0.25 - 1, y + kid.pos.y * 0.25 - 1, 3); + }); + ink3("orange").write( + `kid x:${me.pos.x.toFixed(1)} y:${me.pos.y.toFixed(1)}`, + { x: 6, bottom: 4 + 12 } + ); + ink3("pink").write( + `cam x:${cam.dolly.x.toFixed(1)} y:${cam.dolly.y.toFixed(1)}`, + { x: 6, bottom: 4 } + ); + } + if (spectating) ink3("red").write("Spectating", { x: 6, y: 32 }); +} +function world_act({ event: e2, api, send: send2, jump: jump2, hud, piece, screen: screen2 }) { + if (e2.is("reframed")) { + cam.x = screen2.width / 2 - cam.dolly.x; + cam.y = screen2.height / 2 - cam.dolly.y; + } + if (spectating) return; + if (!input2.canType) { + me.act(api); + inputBtn.act(e2, { + down: () => { + send2({ type: "keyboard:enabled" }); + send2({ type: "keyboard:soft-unlock" }); + }, + push: () => { + me.off(); + send2({ type: "keyboard:soft-lock" }); + }, + cancel: () => { + send2({ type: "keyboard:soft-lock" }); + }, + rollout: () => { + send2({ type: "keyboard:soft-lock" }); + }, + rollover: () => { + if (inputBtn.down) send2({ type: "keyboard:soft-unlock" }); + } + }); + if (!input2.canType && e2.is("keyboard:down:enter")) { + send2({ type: "keyboard:open" }); + me.off(); + } + if (e2.is("keyboard:down:escape") || e2.is("keyboard:down:`")) jump2("prompt"); + if (e2.is("keyboard:down:backspace")) { + jump2(`prompt~${hud.currentLabel().plainText || piece}`)(() => { + send2({ type: "keyboard:open" }); + }); + } + } + if (input2.canType && (e2.is("keyboard:down:`") || e2.is("keyboard:down:escape") || input2.text.trim().length === 0 && e2.is("keyboard:down:enter") && !e2.shift)) { + send2({ type: "keyboard:close" }); + } + if (input2.canType && e2.is("lift") && !input2.shifting && !input2.paste.down) { + send2({ type: "keyboard:close" }); + } + if (e2.is("keyboard:open") || e2.is("keyboard:close") || input2.canType && !e2.is("keyboard:down:escape")) { + input2.act(api); + } +} +function world_sim({ api, piece, geo, simCount: simCount2, screen: screen2, num }) { + keys(kids).forEach((key) => kids[key].sim(api)); + if (!spectating) { + me.sim(api, function net(kid) { + if (simCount2 % 4n === 0n) { + if (kid.pos) server.send(`world:${piece}:move`, kid); + } + if (kid.clear) server.send(`world:${piece}:write:clear`, kid); + }); + cam.dolly.x = num.lerp(cam.dolly.x, me.pos.x, 0.035); + cam.dolly.y = num.lerp(cam.dolly.y, me.pos.y, 0.035); + cam.x = screen2.width / 2 - cam.dolly.x; + cam.y = screen2.height / 2 - cam.dolly.y; + input2.sim(api); + const btnPos = me.screenPos(cam, world); + inputBtn.box = new geo.Box( + btnPos.x - me.size, + btnPos.y - me.size, + me.size * 2 + ); + } else if (spectating && kids[spectatingKid]) { + const sk = kids[spectatingKid]; + cam.dolly.x = num.lerp(cam.dolly.x, sk.pos.x, 0.035); + cam.dolly.y = num.lerp(cam.dolly.y, sk.pos.y, 0.035); + cam.x = screen2.width / 2 - cam.dolly.x; + cam.y = screen2.height / 2 - cam.dolly.y; + } +} +function world_leave({ system: system2, store: store2, piece }) { + console.log("\u{1F5FA}\uFE0F Leaving world, storing position."); + server.send(`world:${piece}:persist`, { handle: me.handle, pos: me.pos }); + store2[`world:${piece}:pos`] = me.pos; + store2.persist(`world:${piece}:pos`); + delete system2.world.size; + delete system2.world.me; +} +function coversScreen(screen2) { + return cam.x <= 0 && cam.y <= 0 && cam.x + world.size.width > screen2.width && cam.y + world.size.height > screen2.height; +} +var Kid = class { + handle; + net; + pos = { x: 0, y: 0 }; + netPos; + size = 16; + leash = { x: 0, y: 0, len: 0, max: 12, deadzone: 8 }; + face = "meh"; + color = "white"; + #keys = { U: false, D: false, L: false, R: false }; + message; + showing; + // Contains a buffer to be showing. + ghost = false; + #messageDuration; + #messageProgress = 0; + constructor(handle2 = "?", pos = this.pos, face, net = false) { + this.handle = handle2; + console.log("\u{1F9D2} *New* kid from:", this.handle, "Feeling:", face); + this.pos = pos; + if (net) this.netPos = { ...pos }; + this.face = face || this.face; + this.net = net; + } + // Show a message above the kid's head for `time` frames. + write(text, time = 320) { + this.message = text; + this.#messageDuration = time; + this.#messageProgress = 0; + } + // Show a message above the kid's head for an unending period. + // 🐛 Used only if they are `ghosted` for slug updates. + slug(text) { + this.write(text, Infinity); + } + // Change the mood (face) of the kid. + mood(face) { + this.face = face; + } + // Change the color of the kid. + tint(c4) { + this.color = c4; + } + // Render the kid. + paint({ ink: ink3, line: line2, point: point2, pan: pan2, text, typeface, stamp: stamp3 }) { + const leash = this.leash; + pan2(this.pos.x, this.pos.y); + if (this.showing) stamp3(this.showing); + if (!this.showing) { + ink3(this.color).circle(0, 0, this.size); + if (this.ghost) { + line2(-6, -6, -10, -6).line(6, -6, 10, -6); + } else { + point2(-6, -6).point(6, -6); + } + if (this.face === "smile") { + ink3(this.color).line(0, 6, -6, 3); + ink3(this.color).line(0, 6, 6, 3); + } else if (this.face === "frown") { + ink3(this.color).line(0, 3, -6, 8); + ink3(this.color).line(0, 3, 6, 8); + } else if (this.face === "meh") { + ink3(this.color).line(-6, 6, 6, 6); + } + } + ink3(leash.len > leash.deadzone ? this.color : [this.color, 128]).line( + 0, + 0, + leash.x, + leash.y + ); + if (this.message) { + const blockWidth = typeface.glyphs["0"].resolution[0]; + const tb = text.box( + this.message, + void 0, + this.message.length * blockWidth + ); + ink3("black").write(this.message, { + x: -tb.box.width / 2 + 1, + y: -this.size - 14 + 1 + }); + ink3(this.color).write(this.message, { + x: -tb.box.width / 2, + y: -this.size - 14 + }); + } + } + // Control the kid. + act({ event: e2, num }) { + const k = this.#keys; + const leash = this.leash; + if (!this.drag) { + if (e2.is("keyboard:down:w") || e2.is("keyboard:down:arrowup")) k.U = true; + if (e2.is("keyboard:down:s") || e2.is("keyboard:down:arrowdown")) + k.D = true; + if (e2.is("keyboard:down:a") || e2.is("keyboard:down:arrowleft")) + k.L = true; + if (e2.is("keyboard:down:d") || e2.is("keyboard:down:arrowright")) + k.R = true; + if (e2.is("keyboard:up:w") || e2.is("keyboard:up:arrowup")) k.U = false; + if (e2.is("keyboard:up:s") || e2.is("keyboard:up:arrowdown")) k.D = false; + if (e2.is("keyboard:up:a") || e2.is("keyboard:up:arrowleft")) k.L = false; + if (e2.is("keyboard:up:d") || e2.is("keyboard:up:arrowright")) k.R = false; + } + if (e2.is("touch:1")) { + k.U = k.D = k.L = k.R = false; + leash.start = { x: e2.x, y: e2.y }; + leash.x = leash.y = 0; + this.drag = true; + } + if (e2.is("draw:1") && leash.start) { + leash.x = e2.x - leash.start.x; + leash.y = e2.y - leash.start.y; + this.#snapLeash(num); + } + if (e2.is("lift:1")) { + leash.start = null; + this.drag = false; + } + } + // Simulate the kid's movement and time messages. + sim({ num }, net) { + if (this.net && this.netPos) { + const p22 = num.p2; + const STEP_SIZE = 1; + const direction = p22.norm(p22.sub(this.netPos, this.pos)); + const distance4 = p22.len(p22.sub(this.netPos, this.pos)); + if (distance4 < STEP_SIZE) { + this.pos = this.netPos; + } else { + this.pos = p22.inc(this.pos, p22.scl(direction, STEP_SIZE)); + } + return; + } + if (this.message) { + if (this.#messageProgress < this.#messageDuration) { + this.#messageProgress += 1; + } else { + this.message = null; + this.#messageProgress = 0; + net?.({ clear: true }); + } + } + const k = this.#keys, leash = this.leash, pos = this.pos; + if (k.U) leash.y -= 1; + if (k.D) leash.y += 1; + if (k.L) leash.x -= 1; + if (k.R) leash.x += 1; + this.#snapLeash(num); + if (!leash.start) { + leash.y *= 0.97; + leash.x *= 0.97; + } + const newPos = { ...pos }; + if (leash.len > leash.deadzone) { + newPos.x = num.lerp(pos.x, pos.x + leash.x, 0.075); + newPos.y = num.lerp(pos.y, pos.y + leash.y, 0.075); + } else if (leash.len > 1) { + newPos.x = num.lerp(pos.x, pos.x + leash.x, 0.025); + newPos.y = num.lerp(pos.y, pos.y + leash.y, 0.025); + } + if (newPos.x !== pos.x || newPos.y !== pos.y) { + net?.({ pos }); + this.moved = true; + } + pos.x = newPos.x; + pos.y = newPos.y; + if (pos.x < 0) pos.x = 0; + if (pos.x > world.size.width) pos.x = world.size.width; + if (pos.y < 0) pos.y = 0; + if (pos.y > world.size.height) pos.y = world.size.height; + } + // Limit the kid's movement leash. + #snapLeash(num) { + const leash = this.leash; + leash.len = num.p2.len(leash); + if (leash.len > leash.max) { + const scale7 = leash.max / leash.len; + leash.x *= scale7; + leash.y *= scale7; + } + } + // Kill all controls. + off() { + const k = this.#keys; + k.U = k.D = k.L = k.R = false; + this.leash.start = null; + } + // Return the screen position of this kid, given a camera and world, + screenPos(cam2, world2) { + return { x: cam2.x + this.pos.x, y: cam2.y + this.pos.y }; + } +}; +var World = class { + size = {}; + constructor(width2 = 192, height2 = 192) { + this.size.width = width2; + this.size.height = height2; + } + get width() { + return this.size.width; + } + get height() { + return this.size.height; + } +}; +var Cam = class { + x = 0; + y = 0; + dolly = { x: 0, y: 0 }; + constructor(x, y, dolly) { + this.x = x; + this.y = y; + this.dolly = { ...dolly }; + } +}; +function onScreen(obj, world2, cam2, screen2) { + const halfWidth = screen2.width / 2; + const halfHeight = screen2.height / 2; + const viewport = { + left: cam2.dolly.x - halfWidth, + right: cam2.dolly.x + halfWidth, + top: cam2.dolly.y - halfHeight, + bottom: cam2.dolly.y + halfHeight + }; + return obj.pos.x >= viewport.left && obj.pos.x <= viewport.right && obj.pos.y >= viewport.top && obj.pos.y <= viewport.bottom; +} + +// public/aesthetic.computer/lib/headers.mjs +function getDayColorScheme(isDarkMode) { + const day = (/* @__PURE__ */ new Date()).getDay(); + const schemes = { + // Sunday - Purple/Lavender + 0: { + dark: { + header: { bg: "rgba(75, 0, 130)", color: "rgb(230, 230, 250)", border: "rgb(147, 112, 219)" }, + code: { bg: "#2d1b69", color: "#b19cd9", shadow: "rgba(177, 156, 217, 0.3)" } + }, + light: { + header: { bg: "rgba(248, 248, 255)", color: "rgb(75, 0, 130)", border: "rgb(147, 112, 219)" }, + code: { bg: "#f8f8ff", color: "#4b0082", border: "#9370db" } + } + }, + // Monday - Blue/Ocean + 1: { + dark: { + header: { bg: "rgba(0, 20, 40)", color: "rgb(173, 216, 230)", border: "rgb(70, 130, 180)" }, + code: { bg: "#1e3a8a", color: "#7dd3fc", shadow: "rgba(125, 211, 252, 0.3)" } + }, + light: { + header: { bg: "rgba(240, 248, 255)", color: "rgb(30, 58, 138)", border: "rgb(70, 130, 180)" }, + code: { bg: "#f0f8ff", color: "#1e3a8a", border: "#4682b4" } + } + }, + // Tuesday - Green/Forest + 2: { + dark: { + header: { bg: "rgba(0, 50, 0)", color: "rgb(144, 238, 144)", border: "rgb(34, 139, 34)" }, + code: { bg: "#064e3b", color: "#6ee7b7", shadow: "rgba(110, 231, 183, 0.3)" } + }, + light: { + header: { bg: "rgba(240, 255, 240)", color: "rgb(6, 78, 59)", border: "rgb(34, 139, 34)" }, + code: { bg: "#f0fff0", color: "#064e3b", border: "#228b22" } + } + }, + // Wednesday - Orange/Sunset + 3: { + dark: { + header: { bg: "rgba(139, 69, 19)", color: "rgb(255, 218, 185)", border: "rgb(255, 140, 0)" }, + code: { bg: "#ea580c", color: "#fed7aa", shadow: "rgba(254, 215, 170, 0.3)" } + }, + light: { + header: { bg: "rgba(255, 248, 220)", color: "rgb(234, 88, 12)", border: "rgb(255, 140, 0)" }, + code: { bg: "#fff8dc", color: "#ea580c", border: "#ff8c00" } + } + }, + // Thursday - Red/Ruby + 4: { + dark: { + header: { bg: "rgba(139, 0, 0)", color: "rgb(255, 182, 193)", border: "rgb(220, 20, 60)" }, + code: { bg: "#991b1b", color: "#fca5a5", shadow: "rgba(252, 165, 165, 0.3)" } + }, + light: { + header: { bg: "rgba(255, 245, 245)", color: "rgb(153, 27, 27)", border: "rgb(220, 20, 60)" }, + code: { bg: "#fff5f5", color: "#991b1b", border: "#dc143c" } + } + }, + // Friday - Pink/Rose + 5: { + dark: { + header: { bg: "rgba(199, 21, 133)", color: "rgb(252, 231, 243)", border: "rgb(236, 72, 153)" }, + code: { bg: "#be185d", color: "#fbcfe8", shadow: "rgba(251, 207, 232, 0.3)" } + }, + light: { + header: { bg: "rgba(253, 242, 248)", color: "rgb(190, 24, 93)", border: "rgb(236, 72, 153)" }, + code: { bg: "#fdf2f8", color: "#be185d", border: "#ec4899" } + } + }, + // Saturday - Teal/Cyan (Original aesthetic computer style) + 6: { + dark: { + header: { bg: "rgba(10, 20, 40)", color: "rgb(200, 200, 250)", border: "rgb(120, 120, 170)" }, + code: { bg: "#1a1a2e", color: "#16a085", shadow: "rgba(22, 160, 133, 0.3)" } + }, + light: { + header: { bg: "rgba(248, 249, 250)", color: "rgb(52, 58, 64)", border: "rgb(108, 117, 125)" }, + code: { bg: "#f8f9fa", color: "#2c3e50", border: "#6c757d" } + } + } + }; + return schemes[day]; +} +function renderQRToConsole(url, darkColor = "#4ecdc4", lightColor = "#1a1a2e") { + try { + const qr = qrcode(url, { errorCorrectLevel: ErrorCorrectLevel.L }); + const size = qr.getModuleCount(); + const moduleSize = 4; + const margin = moduleSize; + const svgSize = size * moduleSize + margin * 2; + let pathData = ""; + for (let row = 0; row < size; row++) { + for (let col = 0; col < size; col++) { + if (qr.isDark(row, col)) { + const x = col * moduleSize + margin; + const y = row * moduleSize + margin; + pathData += `M${x} ${y}h${moduleSize}v${moduleSize}h-${moduleSize}z`; + } + } + } + const svg = ``; + const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`; + const displaySize = 100; + console.log( + "%c ", + `font-size: 1px; padding: ${displaySize / 2}px; background: url("${dataUrl}") no-repeat; background-size: ${displaySize}px ${displaySize}px;` + ); + } catch (e2) { + console.warn("QR generation failed:", e2); + } +} +function headers(isDarkMode) { + if (isDarkMode === void 0) { + isDarkMode = true; + if (typeof window !== "undefined" && window.matchMedia) { + isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; + } + } + const colorScheme = getDayColorScheme(isDarkMode); + const theme = isDarkMode ? colorScheme.dark : colorScheme.light; + const titleStyle = `background: ${theme.header.bg}; + color: ${theme.header.color}; + font-size: 18px; + padding: 0 0.25em; + border-radius: 0.15em; + border-bottom: 0.75px solid ${theme.header.border}; + border-right: 0.75px solid ${theme.header.border};`; + const teiaStyle = isDarkMode ? `background: rgba(0, 0, 0, 0.9); + color: rgb(255, 255, 255); + font-size: 16px; + padding: 0 0.35em; + border-radius: 0.15em; + border: 0.75px solid rgb(120, 120, 120);` : `background: rgba(255, 255, 255, 0.9); + color: rgb(33, 37, 41); + font-size: 16px; + padding: 0 0.35em; + border-radius: 0.15em; + border: 0.75px solid rgb(108, 117, 125);`; + console.log( + "%cAesthetic%c.%cComputer", + "color: #ff6b9d; font-weight: bold; font-size: 16px;", + "color: #4ecdc4; font-weight: bold; font-size: 16px;", + "color: #ff6b9d; font-weight: bold; font-size: 16px;" + ); + if (typeof window !== "undefined" && window.acPACK_MODE) { + if (window.acPACK_COLOPHON) { + const colophon = window.acPACK_COLOPHON; + const piece = colophon.piece; + const build = colophon.build; + const today = (/* @__PURE__ */ new Date()).getDay(); + const isDarkMode2 = true; + const colorScheme2 = getDayColorScheme(isDarkMode2); + const theme2 = colorScheme2.dark; + const titleChars = "Aesthetic.Computer".split(""); + const colors = ["#ff9999", "#ffcc99", "#ffff99", "#99ff99", "#99ccff", "#cc99ff", "#ff99cc"]; + let titleFormatted = ""; + let titleStyles = []; + titleChars.forEach((char, i2) => { + titleFormatted += `%c${char}`; + const color3 = colors[i2 % colors.length]; + titleStyles.push(`color: ${color3}; font-weight: bold; font-size: 14px;`); + }); + console.log(titleFormatted, ...titleStyles); + const pieceSlugForQR = piece.isKidLisp ? `$${piece.name}` : piece.name; + const pieceUrlForQR = `https://aesthetic.computer/${pieceSlugForQR}`; + renderQRToConsole(pieceUrlForQR); + const piecePrefix = piece.isKidLisp ? "$" : ""; + const authorRaw = build.author.replace(/^@/, ""); + const authorDisplay = authorRaw === "anon" ? "anon" : `@${authorRaw}`; + console.log( + `%c${piecePrefix}${piece.name} %cis a %cpiece%c by %c${authorDisplay}`, + "color: #ffc107; font-weight: bold; font-size: 11px;", + "color: #6c757d; font-size: 10px;", + "color: #343a40; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "color: #dc3545; font-size: 10px;" + ); + if (piece.sourceCode && piece.isKidLisp) { + try { + const highlighted = formatKidLispForConsole(piece.sourceCode); + console.log( + `%cIts %cKidLisp%c source is... %c${highlighted.text}`, + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "font-family: monospace; font-size: 10px;", + ...highlighted.styles + ); + } catch (error) { + console.log( + `%cIts %cKidLisp%c source is... %c${piece.sourceCode}`, + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-family: monospace; font-size: 11px;" + ); + } + } + let formattedDate; + if (build.zipFilename) { + const dateMatch = build.zipFilename.match(/(\d{4})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})/); + if (dateMatch) { + const [, year, month, day, hour, minute, second] = dateMatch; + const packDate = new Date(year, month - 1, day, hour, minute, second); + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ]; + const monthName = monthNames[packDate.getMonth()]; + const dayNum = packDate.getDate(); + const ordinalSuffix = (d2) => { + if (d2 > 3 && d2 < 21) return "th"; + switch (d2 % 10) { + case 1: + return "st"; + case 2: + return "nd"; + case 3: + return "rd"; + default: + return "th"; + } + }; + const time = packDate.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + second: "numeric", + hour12: true + }); + formattedDate = `${monthName} ${dayNum}${ordinalSuffix(dayNum)}, ${year} at ${time}`; + } else { + const packDate = new Date(build.packTime); + formattedDate = packDate.toLocaleDateString(); + } + } else { + const packDate = new Date(build.packTime); + formattedDate = packDate.toLocaleDateString(); + } + const fileCount = build.fileCount ? ` ${build.fileCount} files` : ""; + const gitHash = ` ${build.gitCommit}`; + const dirtyStatus = build.gitIsDirty ? " (dirty)" : ""; + const bundleFilename = build.filename || null; + if (bundleFilename) { + console.log( + `%cPacked as %c${bundleFilename}%c on %c${formattedDate}%c`, + "color: #6c757d; font-size: 11px;", + "color: #e83e8c; font-weight: bold; font-size: 11px;", + "color: #6c757d; font-size: 11px;", + "color: #4ecdc4; font-size: 11px;", + "color: #6c757d; font-size: 11px;" + ); + } else { + console.log( + `%cThis copy was packed on %c${formattedDate}%c`, + "color: #6c757d; font-size: 11px;", + "color: #4ecdc4; font-size: 11px;", + "color: #6c757d; font-size: 11px;" + ); + } + console.log( + `%cUsing %caesthetic-computer%c git version%c${gitHash}%c${dirtyStatus}`, + "color: #6c757d; font-size: 11px;", + "color: #e83e8c; font-weight: bold; font-size: 11px;", + "color: #6c757d; font-size: 11px;", + "color: #ffc107; font-size: 11px;", + build.gitIsDirty ? "color: #dc3545; font-size: 11px;" : "color: #343a40; font-size: 11px;" + ); + const pieceSlug = piece.isKidLisp ? `/$${piece.name}` : `/${piece.name}`; + console.log( + `%cView this piece at %chttps://aesthetic.computer${pieceSlug}`, + "color: #6c757d; font-size: 10px;", + "color: #4ecdc4; font-size: 10px; text-decoration: underline;" + ); + if (piece.isKidLisp) { + const kidlispUrl = `https://kidlisp.com/$${piece.name}`; + console.log( + `%cView this piece in %cKidLisp%c at %c${kidlispUrl}`, + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-size: 10px; text-decoration: underline;" + ); + } + const showProjectLinks = typeof window === "undefined" || window.acKEEP_MODE !== true || window.acPACK_SHOW_PROJECT_LINKS === true; + if (showProjectLinks) { + console.log( + `%cLearn %cKidLisp%c at %chttps://kidlisp.com`, + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-size: 10px; text-decoration: underline;" + ); + console.log( + `%cContribute on %cTangled%c at %chttps://tangled.org/aesthetic.computer/core`, + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-weight: bold; font-size: 10px;", + "color: #6c757d; font-size: 10px;", + "color: #28a745; font-size: 10px; text-decoration: underline;" + ); + } + } + } +} +function formatKidLispForConsole(code2) { + const baseStyle = "color: #6c757d; font-family: monospace; font-size: 11px; white-space: pre;"; + const isNodeEnv = typeof process !== "undefined" && !!process.versions?.node; + const isBrowserConsole = !isNodeEnv && typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.navigator !== "undefined"; + if (typeof code2 !== "string" || code2.length === 0) { + if (isBrowserConsole) { + return { text: "\n%c%c", styles: [baseStyle, ""] }; + } + return { text: "\n", styles: [] }; + } + const normalized = code2.replace(/\r\n/g, "\n"); + if (!isBrowserConsole) { + return { text: ` +${normalized}`, styles: [] }; + } + try { + const tokens = tokenize(normalized); + const segments = []; + const pushSegment = (text, style) => { + if (!text) return; + segments.push({ text, style }); + }; + const applyStyle = (overrides = "") => `${baseStyle} ${overrides}`.trim(); + const pushTokenSegments = (token) => { + if (!token) return; + const rainbowColors2 = ["#ff5555", "#ff9800", "#fdd835", "#4caf50", "#2196f3", "#673ab7", "#ff4081"]; + const zebraColors2 = ["#000000", "#ffffff"]; + const cleanToken = token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1) : token; + if (token === "(" || token === ")" || token === ",") { + pushSegment(token, applyStyle("color: #adb5bd;")); + return; + } + if (token.startsWith('"') && token.endsWith('"')) { + pushSegment(token, applyStyle("color: #e83e8c;")); + return; + } + if (/^-?\d+(?:\.\d+)?$/.test(token)) { + pushSegment(token, applyStyle("color: #28a745; font-weight: bold;")); + return; + } + if (/^\d*\.?\d+s(?:\.\.|!|\.\.\.)?$/.test(token)) { + pushSegment(token, applyStyle("color: #17a2b8; font-weight: bold;")); + return; + } + const highlight = getColorTokenHighlight(token); + if (highlight && highlight !== "orange") { + if (highlight.startsWith("COMPOUND:")) { + const parts = highlight.split(":"); + const prefixColor = parts[1]; + const identifierColor = parts[2]; + const prefixChar = token.charAt(0); + const identifierPart = token.substring(1); + const prefixCss = prefixColor.includes(",") ? `rgb(${prefixColor})` : prefixColor; + const identifierCss = identifierColor.includes(",") ? `rgb(${identifierColor})` : identifierColor; + pushSegment(prefixChar, applyStyle(`color: ${prefixCss}; font-weight: bold;`)); + pushSegment(identifierPart, applyStyle(`color: ${identifierCss}; font-weight: bold;`)); + return; + } + if (highlight === "RAINBOW") { + for (let i2 = 0; i2 < token.length; i2++) { + const ch = token[i2]; + const color3 = rainbowColors2[i2 % rainbowColors2.length]; + pushSegment(ch, applyStyle(`color: ${color3}; font-weight: bold;`)); + } + return; + } + if (highlight === "ZEBRA") { + for (let i2 = 0; i2 < token.length; i2++) { + const ch = token[i2]; + const color3 = zebraColors2[i2 % zebraColors2.length]; + pushSegment(ch, applyStyle(`color: ${color3}; font-weight: bold; background: ${color3 === "#000000" ? "#ffffff" : "#000000"};`)); + } + return; + } + if (highlight.startsWith("FADE:")) { + const fadeToken = highlight.substring(5); + const parts = fadeToken.split(":"); + pushSegment("fade", applyStyle("color: #3cb371; font-weight: bold;")); + pushSegment(":", applyStyle("color: #888;")); + let colorPart = parts[1] || ""; + let remaining = parts.slice(2); + if (colorPart === "neat" && parts[2]) { + pushSegment("neat", applyStyle("color: #888; font-style: italic;")); + pushSegment(":", applyStyle("color: #888;")); + colorPart = parts[2]; + remaining = parts.slice(3); + } + const colors = colorPart.split("-"); + colors.forEach((colorName, i2) => { + const colorHighlight = getColorTokenHighlight(colorName); + let cssColor2 = "#888"; + if (colorHighlight && !colorHighlight.startsWith("FADE:") && colorHighlight !== "orange") { + cssColor2 = colorHighlight.includes(",") ? `rgb(${colorHighlight})` : colorHighlight; + } else if (cssColors && cssColors[colorName.toLowerCase()]) { + const rgb = cssColors[colorName.toLowerCase()]; + cssColor2 = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`; + } + pushSegment(colorName, applyStyle(`color: ${cssColor2}; font-weight: bold;`)); + if (i2 < colors.length - 1) { + pushSegment("-", applyStyle("color: #888;")); + } + }); + remaining.forEach((part) => { + pushSegment(":", applyStyle("color: #888;")); + if (part === "neat" || part === "vertical" || part === "horizontal") { + pushSegment(part, applyStyle("color: #888; font-style: italic;")); + } else { + pushSegment(part, applyStyle("color: #888;")); + } + }); + return; + } + const cssColor = highlight.includes(",") ? `rgb(${highlight})` : highlight; + pushSegment(token, applyStyle(`color: ${cssColor}; font-weight: bold;`)); + return; + } + const lower = cleanToken.toLowerCase(); + const drawingCommands = ["line", "rect", "circle", "box", "tri", "plot", "write", "ink", "paper", "brush", "flood", "wipe", "paste", "stamp", "scroll", "zoom", "contrast", "blur", "repeat", "spin", "jump"]; + const controlFlow = ["if", "when", "unless", "each", "loop", "later", "once", "def"]; + if (drawingCommands.includes(lower)) { + pushSegment(token, applyStyle("color: #17a2b8; font-weight: bold;")); + return; + } + if (controlFlow.includes(lower)) { + pushSegment(token, applyStyle("color: #6f42c1; font-weight: bold;")); + return; + } + if (lower === "kidlisp") { + pushSegment(token, applyStyle("color: #28a745; font-weight: bold;")); + return; + } + pushSegment(token, baseStyle); + }; + let cursor2 = 0; + tokens.forEach((token) => { + const idx = normalized.indexOf(token, cursor2); + if (idx === -1) { + return; + } + if (idx > cursor2) { + pushSegment(normalized.slice(cursor2, idx), baseStyle); + } + pushTokenSegments(token); + cursor2 = idx + token.length; + }); + if (cursor2 < normalized.length) { + pushSegment(normalized.slice(cursor2), baseStyle); + } + const mergedSegments = []; + for (const segment of segments) { + const prev = mergedSegments[mergedSegments.length - 1]; + if (prev && prev.style === segment.style) { + prev.text += segment.text; + } else { + mergedSegments.push({ ...segment }); + } + } + const textParts = ["\n"]; + const styles = []; + mergedSegments.forEach(({ text, style }) => { + if (!text) return; + const escaped = text.replace(/%/g, "%%"); + textParts.push(`%c${escaped}`); + styles.push(style); + }); + textParts.push("%c"); + styles.push(""); + return { + text: textParts.join(""), + styles + }; + } catch (error) { + console.warn("KidLisp syntax highlighting error:", error); + return { text: ` +${normalized}`, styles: [] }; + } +} + +// public/aesthetic.computer/lib/sound/sound-whitelist.mjs +var soundWhitelist = [ + "startup", + "compkey", + "msg_1", + "msg_2", + "msg_3", + "msg_4", + "chat_1", + "chat_2", + "chat_3", + "chat_4" +]; + +// public/aesthetic.computer/lib/gamepad-mappings.mjs +var GAMEPAD_MAPPINGS = { + // 8BitDo M30 (Bluetooth, 2.4G, and Wired for Xbox). In X-input mode + // browsers expose its six face buttons as ABXY + the two bumpers. + "M30": { + vendor: "8BitDo", + product: "M30", + description: "Classic six-button arcade controller", + fight: { + punch: [0, 1, 5], + // A, B, C + block: [2, 3, 4] + // X, Y, Z + }, + buttons: { + 0: { name: "A", position: "face_bottom_left", active: "lime", inactive: "darkgreen" }, + 1: { name: "B", position: "face_bottom_center", active: "red", inactive: "maroon" }, + 2: { name: "X", position: "face_top_left", active: "dodgerblue", inactive: "navy" }, + 3: { name: "Y", position: "face_top_center", active: "yellow", inactive: "darkgoldenrod" }, + 4: { name: "Z", position: "face_top_right", active: "orange", inactive: "sienna" }, + 5: { name: "C", position: "face_bottom_right", active: "turquoise", inactive: "darkcyan" }, + 6: { name: "LT", position: "trigger_left", active: "coral", inactive: "brown" }, + 7: { name: "RT", position: "trigger_right", active: "violet", inactive: "indigo" }, + 8: { name: "Select", position: "center_left", active: "white", inactive: "slategray" }, + 9: { name: "Start", position: "center_right", active: "springgreen", inactive: "darkslategray" }, + 10: { name: "L3", position: "stick_left_press", active: "cyan", inactive: "cadetblue" }, + 11: { name: "R3", position: "stick_right_press", active: "magenta", inactive: "purple" }, + 12: { name: "D-Up", position: "dpad_up", active: "chartreuse", inactive: "forestgreen" }, + 13: { name: "D-Down", position: "dpad_down", active: "greenyellow", inactive: "olivedrab" }, + 14: { name: "D-Left", position: "dpad_left", active: "limegreen", inactive: "seagreen" }, + 15: { name: "D-Right", position: "dpad_right", active: "yellowgreen", inactive: "darkolivegreen" }, + 16: { name: "Home", position: "center", active: "hotpink", inactive: "darkslateblue" } + }, + axes: { + 0: { name: "D-Pad X", type: "dpad", direction: "horizontal", active: { left: "lime", right: "chartreuse" }, inactive: "darkseagreen" }, + 1: { name: "D-Pad Y", type: "dpad", direction: "vertical", active: { up: "greenyellow", down: "yellowgreen" }, inactive: "olivedrab" }, + 2: { name: "Right X", type: "stick", direction: "horizontal", active: "magenta", inactive: "dimgray" }, + 3: { name: "Right Y", type: "stick", direction: "vertical", active: "violet", inactive: "dimgray" } + }, + layout: { type: "m30", hasAnalogSticks: false, width: 56, height: 24 } + }, + // 8BitDo Micro - Ultra-compact Bluetooth controller + "8BitDo Micro gamepad": { + vendor: "8BitDo", + product: "Micro", + description: "Ultra-compact keychain gamepad (72mm \xD7 40.7mm)", + // Button mapping (standard Gamepad API indices -> physical buttons) + buttons: { + 0: { name: "A", position: "face_right", active: "lime", inactive: "darkgreen" }, + 1: { name: "B", position: "face_bottom", active: "red", inactive: "maroon" }, + 2: { name: "?", position: "unmapped", active: "cyan", inactive: "teal" }, + 3: { name: "X", position: "face_top", active: "yellow", inactive: "olive" }, + 4: { name: "Y", position: "face_left", active: "orangered", inactive: "saddlebrown" }, + 5: { name: "?", position: "unmapped", active: "magenta", inactive: "indigo" }, + 6: { name: "L", position: "shoulder_left", active: "orange", inactive: "sienna" }, + 7: { name: "R", position: "shoulder_right", active: "dodgerblue", inactive: "midnightblue" }, + 8: { name: "L2", position: "trigger_left", active: "coral", inactive: "brown" }, + 9: { name: "R2", position: "trigger_right", active: "violet", inactive: "rebeccapurple" }, + 10: { name: "Select", position: "center_left", active: "white", inactive: "slategray" }, + 11: { name: "Start", position: "center_right", active: "springgreen", inactive: "darkslategray" }, + 12: { name: "Home", position: "bottom_center", active: "deeppink", inactive: "darkslateblue" }, + 13: { name: "?", position: "unmapped", active: "aqua", inactive: "steelblue" }, + 14: { name: "?", position: "unmapped", active: "gold", inactive: "darkgoldenrod" }, + 15: { name: "?", position: "unmapped", active: "hotpink", inactive: "crimson" }, + 16: { name: "?", position: "unmapped", active: "lightcoral", inactive: "firebrick" } + }, + // Axis mapping (standard Gamepad API indices -> physical controls) + axes: { + 0: { + name: "D-Pad X", + type: "dpad", + direction: "horizontal", + active: { left: "lime", right: "chartreuse" }, + inactive: "darkseagreen" + }, + 1: { + name: "D-Pad Y", + type: "dpad", + direction: "vertical", + active: { up: "greenyellow", down: "yellowgreen" }, + inactive: "olivedrab" + }, + 2: { name: "?", type: "unmapped", active: "cyan", inactive: "teal" }, + 3: { name: "?", type: "unmapped", active: "aqua", inactive: "steelblue" } + }, + // Physical layout for diagram rendering + layout: { + type: "micro", + hasAnalogSticks: false, + width: 70, + height: 35, + dpad: { + type: "axis", + // Uses axes instead of buttons + x: 13, + y: 17, + size: 5, + spacing: 8 + }, + faceButtons: { + x: 57, + y: 17, + size: 4, + spacing: 7, + mapping: { + top: 3, + bottom: 1, + left: 4, + right: 0 + } + }, + centerButtons: { + select: { x: -6, y: 7, button: 10 }, + // Relative to center + start: { x: 2, y: 7, button: 11 }, + home: { x: 4, y: 30, button: 12 } + // Relative to center, bottom + }, + shoulders: { + left: { x: 7, y: -3, width: 13, button: 6 }, + right: { x: 50, y: -3, width: 13, button: 7 } + // x = width - 20 + }, + triggers: { + left: { x: 21, y: -3, width: 4, button: 8 }, + right: { x: 45, y: -3, width: 4, button: 9 } + // x = width - 25 + } + } + }, + // Standard gamepad (Xbox/PlayStation style) + "standard": { + vendor: "Generic", + product: "Standard Gamepad", + description: "W3C Standard Gamepad mapping", + fight: { + punch: [0, 2, 5], + // A, X, RB + block: [1, 3, 4] + // B, Y, LB + }, + buttons: { + 0: { name: "A/X", position: "face_bottom", active: "lime", inactive: "darkgreen" }, + 1: { name: "B/\u25CB", position: "face_right", active: "red", inactive: "maroon" }, + 2: { name: "X/\u25A1", position: "face_left", active: "dodgerblue", inactive: "navy" }, + 3: { name: "Y/\u25B3", position: "face_top", active: "yellow", inactive: "darkgoldenrod" }, + 4: { name: "LB/L1", position: "shoulder_left", active: "orange", inactive: "sienna" }, + 5: { name: "RB/R1", position: "shoulder_right", active: "turquoise", inactive: "darkcyan" }, + 6: { name: "LT/L2", position: "trigger_left", active: "coral", inactive: "brown" }, + 7: { name: "RT/R2", position: "trigger_right", active: "violet", inactive: "indigo" }, + 8: { name: "Select", position: "center_left", active: "white", inactive: "slategray" }, + 9: { name: "Start", position: "center_right", active: "springgreen", inactive: "darkslategray" }, + 10: { name: "LS", position: "stick_left_press", active: "cyan", inactive: "cadetblue" }, + 11: { name: "RS", position: "stick_right_press", active: "magenta", inactive: "purple" }, + 12: { name: "D-Up", position: "dpad_up", active: "chartreuse", inactive: "forestgreen" }, + 13: { name: "D-Down", position: "dpad_down", active: "greenyellow", inactive: "olivedrab" }, + 14: { name: "D-Left", position: "dpad_left", active: "limegreen", inactive: "seagreen" }, + 15: { name: "D-Right", position: "dpad_right", active: "yellowgreen", inactive: "darkolivegreen" }, + 16: { name: "Home", position: "center", active: "hotpink", inactive: "darkslateblue" } + }, + axes: { + 0: { + name: "LS-X", + type: "stick", + stick: "left", + direction: "horizontal", + active: { left: "cyan", right: "aqua" }, + inactive: "dimgray" + }, + 1: { + name: "LS-Y", + type: "stick", + stick: "left", + direction: "vertical", + active: { up: "deepskyblue", down: "skyblue" }, + inactive: "dimgray" + }, + 2: { + name: "RS-X", + type: "stick", + stick: "right", + direction: "horizontal", + active: { left: "magenta", right: "orchid" }, + inactive: "dimgray" + }, + 3: { + name: "RS-Y", + type: "stick", + stick: "right", + direction: "vertical", + active: { up: "violet", down: "plum" }, + inactive: "dimgray" + } + }, + layout: { + type: "standard", + hasAnalogSticks: true + // Layout details would go here + } + } +}; + +// public/aesthetic.computer/lib/pmove.mjs +var DEFAULT_CFG = Object.freeze({ + runSpeed: 10, + // units/sec + walkSpeed: 5, + // units/sec while crouched + jumpVelocity: 8, + // initial upward velocity on jump (u/s) + gravity: 50, + // u/s² + groundY: 0, + // world Y of the solid ground plane + eyeHeight: 2, + // stand eye height above groundY + crouchEyeHeight: 1.2, + // crouched eye height + crouchLerp: 0.25, + // per-tick lerp toward crouch target + groundBounds: null, + // { xMin, xMax, zMin, zMax } or null + deathFloorY: null, + // world Y clamp for frozen players (lava pit) + deathFloorClearance: 0.3, + simHz: 120, + // Dolly-style horizontal damping. cam-doll uses a 0.9 decay + push that + // settles at `speed` units/sec. We fold that into a direct integration + // here so the server doesn't need the Dolly object: hVelDecay per tick + // and a push matching the same steady state. + hVelDecay: 0.9, + // 🏃 Quake-style physics (opt-in). When `airAccel` is set, the legacy + // hVelDecay lerp is replaced by Q3 friction+accelerate on the ground and + // air-accelerate (with a perpendicular wishspeed cap) in the air. This + // is what makes strafe-jumping / bunny-hopping work: in air, velocity + // perpendicular to the existing motion vector is added without subtracting + // current speed, so turning slightly off-axis lets the player accumulate + // velocity above runSpeed. + airAccel: null, + // u/s² along wishdir while airborne + groundAccel: null, + // u/s² along wishdir while on ground + airCapSpeed: 1.5, + // max wishspeed used during air-accel (Q3 air-control window) + groundFriction: 6, + // Q3-style friction coefficient + // Static obstacles: array of { type:"box", xMin, xMax, yMin, yMax, zMin, zMax } + // or { type:"cylinder", x, z, r, yMin, yMax }. Resolved as horizontal + // push-out after integration so prediction (client) and authority (server) + // converge. yMin/yMax bound the vertical span — only collide when the + // player's feet→eye column overlaps the obstacle's Y range. + obstacles: null, + playerRadius: 0.4 + // horizontal radius for obstacle push-out +}); +var BTN = Object.freeze({ + JUMP: 1 << 0, + CROUCH: 1 << 1, + SHOOT: 1 << 2, + DASH: 1 << 3 +}); +function resolveObstacles(s2, cfg) { + if (!cfg?.obstacles?.length) return; + const r2 = cfg.playerRadius ?? 0.4; + const eyeY = s2.y; + const feetY = s2.y - cfg.eyeHeight; + for (const o2 of cfg.obstacles) { + if (typeof o2.yMax === "number" && feetY > o2.yMax) continue; + if (typeof o2.yMin === "number" && eyeY < o2.yMin) continue; + if (o2.type === "cylinder") { + const dx = s2.x - o2.x; + const dz = s2.z - o2.z; + const d2 = Math.hypot(dx, dz); + const minD = (o2.r ?? 1) + r2; + if (d2 >= minD) continue; + if (d2 < 1e-4) { + s2.x += minD; + continue; + } + const nx = dx / d2, nz = dz / d2; + const push = minD - d2; + s2.x += nx * push; + s2.z += nz * push; + const vd = s2.vx * nx + s2.vz * nz; + if (vd < 0) { + s2.vx -= vd * nx; + s2.vz -= vd * nz; + } + } else if (o2.type === "box") { + const cx = (o2.xMin + o2.xMax) * 0.5; + const cz = (o2.zMin + o2.zMax) * 0.5; + const hx = (o2.xMax - o2.xMin) * 0.5 + r2; + const hz = (o2.zMax - o2.zMin) * 0.5 + r2; + const dx = s2.x - cx; + const dz = s2.z - cz; + const ox = hx - Math.abs(dx); + const oz = hz - Math.abs(dz); + if (ox <= 0 || oz <= 0) continue; + if (ox < oz) { + s2.x += dx >= 0 ? ox : -ox; + if (dx >= 0 && s2.vx < 0 || dx < 0 && s2.vx > 0) s2.vx = 0; + } else { + s2.z += dz >= 0 ? oz : -oz; + if (dz >= 0 && s2.vz < 0 || dz < 0 && s2.vz > 0) s2.vz = 0; + } + } + } +} + +// public/aesthetic.computer/lib/cam-doll.mjs +var { abs: abs4 } = Math; +var CamDoll = class { + cam; + sensitivity; + #dolly; + // Keyboard controls. + #W; + #S; + #A; + #D; + #SPACE; + #SHIFT; + #UP; + #DOWN; + #LEFT; + #RIGHT; + // Gamepad button look controls (8BitDo Micro face buttons) + #BTN_LOOK_UP; + #BTN_LOOK_DOWN; + #BTN_LOOK_LEFT; + #BTN_LOOK_RIGHT; + // Button look velocity for smoothing + #buttonLookVelX = 0; + #buttonLookVelY = 0; + #buttonLookAccel = 0.05; + // Even more subtle acceleration + #buttonLookDecel = 0.85; + #buttonLookMaxSpeed = 1; + // Slower max speed + // Stick based controls. + #ANALOG = { + move: { + x: 0, + y: 0, + z: 0 + }, + look: { + x: 0, + y: 0, + z: 0 + } + }; + #penLocked = false; + // 🧗 Optional Quake-style grounded physics (enabled when opts.gravity is set). + // When enabled, SPACE jumps only while grounded, SHIFT crouches (clamped to + // ground, cannot dip below), WASD moves at runSpeed (walkSpeed while crouched), + // and vertical motion is integrated with gravity + a ground clamp. + // Conventions: speeds in world-units/second, gravity in u/s². cam.y is inverted + // relative to world Y (positive cam.y = below origin), hence world-up = -cam.y. + #physicsEnabled = false; + #runSpeed = 0; + #walkSpeed = 0; + #jumpVelocity = 0; + #gravity = 0; + #groundY = 0; + #eyeHeight = 1.5; + #crouchEyeHeight = 0.8; + #simHz = 120; + #worldYVel = 0; + // world-space vertical velocity, + is up + #onGround = true; + #crouchT = 0; + // 0 standing → 1 crouched + #moveDamp = 2e-3; + // Optional XZ rectangle bounding the solid floor. Outside these bounds the + // floor clamp is disabled so the player falls off the edge. Format: + // { xMin, xMax, zMin, zMax } in world units. + #groundBounds = null; + // When true, all physics input is ignored and the camera just free-falls. + // External callers (e.g. a piece with a death state) set this via setFrozen(). + #frozen = false; + // 🎥 Third-person mode. When enabled, the render camera lazily follows a + // target point (player − forward·distance + up·height). The current render + // position is stored in #tpCurrent and lerped toward the target each tick. + // Physics runs on the *logical* player position; the render offset is + // applied at the end of sim and undone at the start of the next. + #thirdPerson = false; + #thirdPersonDistance = 4.5; + #thirdPersonHeight = 1.5; + #thirdPersonFollow = 0.08; + // per-tick lerp factor (≈100 ms to catch up) + #appliedOffset = [0, 0, 0]; + #tpCurrent = null; + // current render offset (cam-space), init lazy + // 💀 Death floor clamp — when set, a frozen (dead) player lands on this + // world Y instead of falling forever. + #deathFloorY = null; + // 🏃 Quake-style air physics. Set to a number to enable strafe-jumping / + // bunny-hop: in air the dolly's horizontal decay is replaced by an + // air-accelerate primitive that adds velocity along wishdir without + // touching perpendicular speed. On the ground a friction+accelerate model + // takes over. Must mirror lib/pmove.mjs so the server arrives at the same + // state. + #airAccel = null; + #groundAccel = 80; + #airCapSpeed = 1.5; + #groundFriction = 6; + #obstacles = null; + #playerRadius = 0.4; + #deathFloorEyeClearance = 0.3; + // 🖖 Disable built-in touch controls for pieces that implement custom mobile UI + #disableTouchControls = false; + constructor(Camera2, Dolly2, opts) { + this.cam = new Camera2(opts.fov || 80, { + z: opts.z || 0, + y: opts.y * -1 || -0.5, + scale: [1, 1, 1] + }); + this.sensitivity = opts.sensitivity || 25e-5; + this.#dolly = new Dolly2(this.cam); + if (opts.disableTouchControls === true) this.#disableTouchControls = true; + if (opts.gravity !== void 0) { + this.#physicsEnabled = true; + this.#gravity = opts.gravity; + this.#runSpeed = opts.runSpeed ?? 10; + this.#walkSpeed = opts.walkSpeed ?? this.#runSpeed * 0.5; + this.#jumpVelocity = opts.jumpVelocity ?? 8; + this.#groundY = opts.groundY ?? 0; + this.#eyeHeight = opts.eyeHeight ?? 1.5; + this.#crouchEyeHeight = opts.crouchEyeHeight ?? this.#eyeHeight * 0.55; + this.#simHz = opts.simHz ?? 120; + this.#moveDamp = this.#runSpeed / this.#simHz / 10; + if (opts.groundBounds) this.#groundBounds = opts.groundBounds; + if (typeof opts.deathFloorY === "number") this.#deathFloorY = opts.deathFloorY; + if (typeof opts.airAccel === "number") { + this.#airAccel = opts.airAccel; + if (typeof opts.groundAccel === "number") this.#groundAccel = opts.groundAccel; + if (typeof opts.airCapSpeed === "number") this.#airCapSpeed = opts.airCapSpeed; + if (typeof opts.groundFriction === "number") this.#groundFriction = opts.groundFriction; + } + if (Array.isArray(opts.obstacles)) this.#obstacles = opts.obstacles; + if (typeof opts.playerRadius === "number") this.#playerRadius = opts.playerRadius; + this.cam.y = -(this.#groundY + this.#eyeHeight); + } + } + /** Clear every held input state. Called on window defocus / visibility loss + * so keys don't "stick" if the user alt-tabs mid-movement. */ + clearHeldKeys() { + this.#W = this.#S = this.#A = this.#D = false; + this.#SPACE = this.#SHIFT = false; + this.#UP = this.#DOWN = this.#LEFT = this.#RIGHT = false; + this.#BTN_LOOK_UP = this.#BTN_LOOK_DOWN = false; + this.#BTN_LOOK_LEFT = this.#BTN_LOOK_RIGHT = false; + this.#ANALOG.move.x = this.#ANALOG.move.y = this.#ANALOG.move.z = 0; + this.#ANALOG.look.x = this.#ANALOG.look.y = this.#ANALOG.look.z = 0; + } + /** Teleport the camera back to standing on the floor at the configured + * ground position, clearing velocity. Used for respawn. */ + respawn(worldX = 0, worldZ = 0) { + this.cam.x = -worldX; + this.cam.z = -worldZ; + this.cam.y = -(this.#groundY + this.#eyeHeight); + this.#worldYVel = 0; + this.#onGround = true; + this.#frozen = false; + this.#dolly.xVel = this.#dolly.yVel = this.#dolly.zVel = 0; + this.clearHeldKeys(); + } + /** When frozen the physics pass is skipped entirely (e.g. during a death + * animation). Player input is already zeroed via clearHeldKeys. */ + setFrozen(v2) { + this.#frozen = !!v2; + if (v2) this.clearHeldKeys(); + } + /** Enable / disable third-person view. On entry the render offset is + * re-initialised so the camera snaps into position; on exit it instantly + * returns to first person. */ + setThirdPerson(v2, distance4, height2) { + this.#thirdPerson = !!v2; + if (typeof distance4 === "number") this.#thirdPersonDistance = distance4; + if (typeof height2 === "number") this.#thirdPersonHeight = height2; + if (!this.#thirdPerson) this.#tpCurrent = null; + } + /** Flip between 1P and 3P. Use for single-tap / middle-mouse bindings. */ + toggleThirdPerson() { + this.setThirdPerson(!this.#thirdPerson); + } + /** True if third-person mode is currently active. */ + get thirdPerson() { + return this.#thirdPerson; + } + /** Apply a one-shot impulse in world space. Horizontal goes through the + * dolly (decay-driven, so it fades over ~10 frames); vertical adds to + * worldYVel directly. Used by external systems like grenade explosions. */ + applyImpulse({ x = 0, y = 0, z = 0 } = {}) { + const scale7 = this.#airAccel !== null ? 1 / this.#simHz : 1; + if (x) this.#dolly.xVel += -x * scale7; + if (z) this.#dolly.zVel += -z * scale7; + if (y) { + this.#worldYVel += y; + if (y > 0) this.#onGround = false; + } + } + /** Set movement key state directly (for mobile UI buttons). */ + setMovement(dir, pressed) { + if (dir === "forward") this.#W = pressed; + else if (dir === "back") this.#S = pressed; + else if (dir === "left") this.#A = pressed; + else if (dir === "right") this.#D = pressed; + else if (dir === "jump") this.#SPACE = pressed; + else if (dir === "crouch") this.#SHIFT = pressed; + } + /** Live telemetry for HUDs / debug panels. */ + get physics() { + if (!this.#physicsEnabled) return null; + return { + fov: this.cam.fov, + runSpeed: this.#runSpeed, + walkSpeed: this.#walkSpeed, + jumpVelocity: this.#jumpVelocity, + gravity: this.#gravity, + onGround: this.#onGround, + crouch: this.#crouchT, + worldYVel: this.#worldYVel, + thirdPerson: this.#thirdPerson, + // Logical player position in the camera's negated-world coordinate + // system (useful for anchoring body parts at the player, not at the + // offset render camera). + playerCamX: this.cam.x - this.#appliedOffset[0], + playerCamY: this.cam.y - this.#appliedOffset[1], + playerCamZ: this.cam.z - this.#appliedOffset[2] + }; + } + sim() { + if (this.#appliedOffset[0] !== 0 || this.#appliedOffset[1] !== 0 || this.#appliedOffset[2] !== 0) { + this.cam.x -= this.#appliedOffset[0]; + this.cam.y -= this.#appliedOffset[1]; + this.cam.z -= this.#appliedOffset[2]; + this.#appliedOffset[0] = this.#appliedOffset[1] = this.#appliedOffset[2] = 0; + } + let forward = 0, updown = 0, strafe = 0; + if (this.#physicsEnabled) { + const speed = this.#SHIFT ? this.#walkSpeed : this.#runSpeed; + const push = speed / this.#simHz / 10; + if (this.#W) forward = -push; + if (this.#S) forward = push; + if (this.#A) strafe = push; + if (this.#D) strafe = -push; + if ((this.#W || this.#S || this.#A || this.#D) && this.#airAccel === null) { + this.#dolly.push({ x: strafe, y: 0, z: forward }); + } + } else { + if (this.#W) forward = -this.sensitivity; + if (this.#S) forward = this.sensitivity; + if (this.#A) strafe = this.sensitivity; + if (this.#D) strafe = -this.sensitivity; + if (this.#SPACE) updown = -this.sensitivity; + if (this.#SHIFT) updown = this.sensitivity; + if (this.#W || this.#S || this.#A || this.#D || this.#SPACE || this.#SHIFT) { + this.#dolly.push({ x: strafe, y: updown, z: forward }); + } + } + if ((abs4(this.#ANALOG.move.z) > 0 || abs4(this.#ANALOG.move.x) > 0) && this.#airAccel === null) { + this.#dolly.push({ + x: this.#ANALOG.move.x, + y: 0, + z: this.#ANALOG.move.z + }); + } + if (abs4(this.#ANALOG.look.x) > 0 || abs4(this.#ANALOG.look.y) > 0) { + this.cam.rotX += this.#ANALOG.look.x; + this.cam.rotY += this.#ANALOG.look.y; + } + if (this.#BTN_LOOK_UP || this.#BTN_LOOK_DOWN) { + if (this.#BTN_LOOK_UP) this.#buttonLookVelY += this.#buttonLookAccel; + if (this.#BTN_LOOK_DOWN) this.#buttonLookVelY -= this.#buttonLookAccel; + if (this.#buttonLookVelY > this.#buttonLookMaxSpeed) this.#buttonLookVelY = this.#buttonLookMaxSpeed; + if (this.#buttonLookVelY < -this.#buttonLookMaxSpeed) this.#buttonLookVelY = -this.#buttonLookMaxSpeed; + } else { + this.#buttonLookVelY *= this.#buttonLookDecel; + if (abs4(this.#buttonLookVelY) < 0.01) this.#buttonLookVelY = 0; + } + if (this.#BTN_LOOK_LEFT || this.#BTN_LOOK_RIGHT) { + if (this.#BTN_LOOK_RIGHT) this.#buttonLookVelX += this.#buttonLookAccel; + if (this.#BTN_LOOK_LEFT) this.#buttonLookVelX -= this.#buttonLookAccel; + if (this.#buttonLookVelX > this.#buttonLookMaxSpeed) this.#buttonLookVelX = this.#buttonLookMaxSpeed; + if (this.#buttonLookVelX < -this.#buttonLookMaxSpeed) this.#buttonLookVelX = -this.#buttonLookMaxSpeed; + } else { + this.#buttonLookVelX *= this.#buttonLookDecel; + if (abs4(this.#buttonLookVelX) < 0.01) this.#buttonLookVelX = 0; + } + this.cam.rotX += this.#buttonLookVelY; + this.cam.rotY += this.#buttonLookVelX; + if (this.#UP) this.cam.rotX += 1; + if (this.#DOWN) this.cam.rotX -= 1; + if (this.#LEFT) this.cam.rotY -= 1; + if (this.#RIGHT) this.cam.rotY += 1; + const maxPitch = 89; + if (this.cam.rotX > maxPitch) this.cam.rotX = maxPitch; + if (this.cam.rotX < -maxPitch) this.cam.rotX = -maxPitch; + if (this.#physicsEnabled && this.#airAccel !== null) { + this.#dolly.dec = 1; + const dt = 1 / this.#simHz; + let wvx = -this.#dolly.xVel * this.#simHz; + let wvz = -this.#dolly.zVel * this.#simHz; + let ix = 0, iz = 0; + if (this.#D) ix += 1; + if (this.#A) ix -= 1; + if (this.#W) iz += 1; + if (this.#S) iz -= 1; + if (this.#moveDamp > 0) { + const ax = this.#ANALOG.move.x / this.#moveDamp; + const az = this.#ANALOG.move.z / this.#moveDamp; + ix += -ax > 1 ? 1 : -ax < -1 ? -1 : -ax; + iz += -az > 1 ? 1 : -az < -1 ? -1 : -az; + } + const ilen = Math.hypot(ix, iz); + if (ilen > 1) { + ix /= ilen; + iz /= ilen; + } + const speed = this.#SHIFT ? this.#walkSpeed : this.#runSpeed; + const yr = this.cam.rotY * Math.PI / 180; + const sy = Math.sin(yr), cy = Math.cos(yr); + const wx = (ix * cy + iz * sy) * speed; + const wz = (-ix * sy + iz * cy) * speed; + const wishLen = Math.hypot(wx, wz); + const wdx = wishLen > 1e-6 ? wx / wishLen : 0; + const wdz = wishLen > 1e-6 ? wz / wishLen : 0; + if (!this.#frozen && this.#SPACE && this.#onGround) { + this.#worldYVel = this.#jumpVelocity; + this.#onGround = false; + } + if (this.#onGround && !this.#frozen) { + const sp = Math.hypot(wvx, wvz); + if (sp < 1e-4) { + wvx = 0; + wvz = 0; + } else { + const stop = Math.max(sp, this.#runSpeed * 0.5); + const drop = stop * this.#groundFriction * dt; + const k = Math.max(0, sp - drop) / sp; + wvx *= k; + wvz *= k; + } + } + if (wishLen > 1e-6 && !this.#frozen) { + const accel = this.#onGround ? this.#groundAccel : this.#airAccel; + const cap = this.#onGround ? wishLen : Math.min(wishLen, this.#airCapSpeed); + const cur = wvx * wdx + wvz * wdz; + const addspeed = cap - cur; + if (addspeed > 0) { + let mag = accel * dt * cap; + if (mag > addspeed) mag = addspeed; + wvx += wdx * mag; + wvz += wdz * mag; + } + } + this.#dolly.xVel = -wvx / this.#simHz; + this.#dolly.zVel = -wvz / this.#simHz; + } + this.#dolly.sim(); + if (this.#airAccel !== null && this.#obstacles) { + const ws = { + x: -this.cam.x, + z: -this.cam.z, + y: -this.cam.y, + vx: -this.#dolly.xVel * this.#simHz, + vz: -this.#dolly.zVel * this.#simHz + }; + resolveObstacles(ws, { + obstacles: this.#obstacles, + playerRadius: this.#playerRadius, + eyeHeight: this.#eyeHeight + }); + this.cam.x = -ws.x; + this.cam.z = -ws.z; + this.#dolly.xVel = -ws.vx / this.#simHz; + this.#dolly.zVel = -ws.vz / this.#simHz; + } + if (this.#physicsEnabled) { + const dt = 1 / this.#simHz; + if (!this.#frozen && this.#SPACE && this.#onGround) { + this.#worldYVel = this.#jumpVelocity; + this.#onGround = false; + } + if (!this.#onGround || this.#frozen) { + this.#worldYVel -= this.#gravity * dt; + this.cam.y -= this.#worldYVel * dt; + } + const crouchTarget = !this.#frozen && this.#SHIFT ? 1 : 0; + this.#crouchT += (crouchTarget - this.#crouchT) * 0.25; + if (this.#crouchT < 5e-4 && crouchTarget === 0) this.#crouchT = 0; + if (this.#crouchT > 0.9995 && crouchTarget === 1) this.#crouchT = 1; + const effEyeHeight = this.#eyeHeight + (this.#crouchEyeHeight - this.#eyeHeight) * this.#crouchT; + let onSolidGround = true; + if (this.#groundBounds) { + const px = -this.cam.x; + const pz = -this.cam.z; + const b2 = this.#groundBounds; + onSolidGround = px >= b2.xMin && px <= b2.xMax && pz >= b2.zMin && pz <= b2.zMax; + } + const floorCamY = -(this.#groundY + effEyeHeight); + if (onSolidGround && !this.#frozen) { + if (this.cam.y >= floorCamY) { + this.cam.y = floorCamY; + if (this.#worldYVel < 0) this.#worldYVel = 0; + this.#onGround = true; + } else if (this.#onGround) { + this.cam.y = floorCamY; + } + } else { + this.#onGround = false; + } + if (this.#frozen && this.#deathFloorY !== null) { + const lavaCamY = -(this.#deathFloorY + this.#deathFloorEyeClearance); + if (this.cam.y >= lavaCamY) { + this.cam.y = lavaCamY; + this.#worldYVel = 0; + } + } + } + if (this.#thirdPerson) { + const rx = this.cam.rotX * Math.PI / 180; + const ry = this.cam.rotY * Math.PI / 180; + const cp = Math.cos(rx); + const fx = Math.sin(ry) * cp; + const fy = Math.sin(rx); + const fz = Math.cos(ry) * cp; + const d2 = this.#thirdPersonDistance; + const h = this.#thirdPersonHeight; + const targetX = d2 * fx; + const targetZ = d2 * fz; + const targetY = d2 * fy - h; + if (!this.#tpCurrent) { + this.#tpCurrent = [targetX, targetY, targetZ]; + } else { + const k = this.#thirdPersonFollow; + this.#tpCurrent[0] += (targetX - this.#tpCurrent[0]) * k; + this.#tpCurrent[1] += (targetY - this.#tpCurrent[1]) * k; + this.#tpCurrent[2] += (targetZ - this.#tpCurrent[2]) * k; + } + this.#appliedOffset[0] = this.#tpCurrent[0]; + this.#appliedOffset[1] = this.#tpCurrent[1]; + this.#appliedOffset[2] = this.#tpCurrent[2]; + this.cam.x += this.#appliedOffset[0]; + this.cam.y += this.#appliedOffset[1]; + this.cam.z += this.#appliedOffset[2]; + } + } + // TODO: Also add touch controls here. + act(e2) { + if (e2.is("defocus")) { + this.clearHeldKeys(); + return; + } + if (e2.is("keyboard:down:w")) this.#W = true; + if (e2.is("keyboard:down:s")) this.#S = true; + if (e2.is("keyboard:down:a")) this.#A = true; + if (e2.is("keyboard:down:d")) this.#D = true; + if (e2.is("keyboard:up:w")) this.#W = false; + if (e2.is("keyboard:up:s")) this.#S = false; + if (e2.is("keyboard:up:a")) this.#A = false; + if (e2.is("keyboard:up:d")) this.#D = false; + if (e2.is("keyboard:down:space")) this.#SPACE = true; + if (e2.ctrl === false && e2.is("keyboard:down:shift")) this.#SHIFT = true; + if (e2.is("keyboard:up:space")) this.#SPACE = false; + if (e2.is("keyboard:up:shift")) this.#SHIFT = false; + if (e2.is("keyboard:down:arrowup")) this.#UP = true; + if (e2.is("keyboard:down:arrowdown")) this.#DOWN = true; + if (e2.is("keyboard:down:arrowleft")) this.#LEFT = true; + if (e2.is("keyboard:down:arrowright")) this.#RIGHT = true; + if (e2.is("keyboard:up:arrowup")) this.#UP = false; + if (e2.is("keyboard:up:arrowdown")) this.#DOWN = false; + if (e2.is("keyboard:up:arrowleft")) this.#LEFT = false; + if (e2.is("keyboard:up:arrowright")) this.#RIGHT = false; + if (e2.is("pen:locked")) this.#penLocked = true; + if (e2.is("pen:unlocked")) this.#penLocked = false; + if (!this.#penLocked && e2.is("draw") || this.#penLocked && e2.is("move")) { + if (this.cam.type === "perspective") { + this.cam.rotX -= e2.delta.y / 3.5; + this.cam.rotY += e2.delta.x / 3.5; + const maxPitch = 89; + if (this.cam.rotX > maxPitch) this.cam.rotX = maxPitch; + if (this.cam.rotX < -maxPitch) this.cam.rotX = -maxPitch; + } + } + if (!this.#disableTouchControls) { + if (e2.is("touch:2")) this.#W = true; + if (e2.is("lift:2")) this.#W = false; + if (e2.is("touch:3")) this.#S = true; + if (e2.is("lift:3")) this.#S = false; + } + if (e2.is("gamepad")) { + const deadzone = 0.05; + const moveDamp = this.#moveDamp; + const lookDamp = 1; + const gamepadId = e2.gamepadId || "standard"; + const is8BitDoMicro = gamepadId.includes("8BitDo Micro"); + const mapping = GAMEPAD_MAPPINGS[gamepadId] || GAMEPAD_MAPPINGS["standard"]; + if (e2.is("gamepad:0:axis:1")) { + if (abs4(e2.value) < deadzone) { + this.#ANALOG.move.z = 0; + } else { + this.#ANALOG.move.z = e2.value * moveDamp; + } + } + if (e2.is("gamepad:0:axis:0")) { + if (abs4(e2.value) < deadzone) { + this.#ANALOG.move.x = 0; + } else { + this.#ANALOG.move.x = -e2.value * moveDamp; + } + } + if (e2.button !== void 0 && e2.action) { + const btn = e2.button; + const isPressed = e2.action === "push"; + if (is8BitDoMicro) { + if (btn === 0) this.#BTN_LOOK_RIGHT = isPressed; + if (btn === 1) this.#BTN_LOOK_DOWN = isPressed; + if (btn === 3) this.#BTN_LOOK_UP = isPressed; + if (btn === 4) this.#BTN_LOOK_LEFT = isPressed; + } + } + if (!is8BitDoMicro) { + if (e2.is("gamepad:0:axis:3:move")) { + if (abs4(e2.value) < deadzone) { + this.#ANALOG.look.x = 0; + } else { + this.#ANALOG.look.x = -e2.value * lookDamp; + } + } + if (e2.is("gamepad:0:axis:2:move")) { + if (abs4(e2.value) < deadzone) { + this.#ANALOG.look.y = 0; + } else { + this.#ANALOG.look.y = e2.value * lookDamp; + } + } + } + } + } +}; + +// public/aesthetic.computer/disks/common/fonts.mjs +var fonts_exports = {}; +__export(fonts_exports, { + MatrixChunky8: () => MatrixChunky8, + font_1: () => font_1, + microtype: () => microtype, + unifont: () => unifont +}); +var font_1 = { + glyphHeight: 10, + glyphWidth: 6, + proportional: false, + // Monospace font - fixed character width + bdfFallback: "6x10", + // X11 Misc-Fixed 6x10 for Cyrillic, Greek, accented Latin, etc. + 0: "numbers/0 - 2021.12.16.18.28.06", + 1: "numbers/1 - 2021.12.16.17.56.44", + 2: "numbers/2 - 2021.12.16.17.59.01", + 3: "numbers/3 - 2021.12.16.17.59.52", + 4: "numbers/4 - 2021.12.16.18.00.56", + 5: "numbers/5 - 2021.12.16.18.01.27", + 6: "numbers/6 - 2021.12.16.18.02.26", + 7: "numbers/7 - 2021.12.16.18.02.50", + 8: "numbers/8 - 2021.12.16.18.03.31", + 9: "numbers/9 - 2021.12.16.18.04.15", + a: "lowercase/a - 2022.1.11.16.12.07", + b: "lowercase/b - 2022.1.11.16.12.57", + c: "lowercase/c - 2022.1.11.16.14.15", + d: "lowercase/d - 2022.1.11.16.14.53", + e: "lowercase/e - 2022.1.11.16.15.35", + f: "lowercase/f - 2022.1.11.16.18.40", + g: "lowercase/g - 2022.1.11.16.20.34", + h: "lowercase/h - 2022.1.11.16.22.10", + i: "lowercase/i - 2022.1.11.16.23.36", + \u00ED: "lowercase/\xED - 2025.6.25.01.00.00", + j: "lowercase/j - 2022.1.11.16.25.14", + k: "lowercase/k - 2022.1.11.16.29.25", + l: "lowercase/l - 2022.1.11.16.30.34", + m: "lowercase/m - 2022.1.11.16.31.12", + n: "lowercase/n - 2022.1.11.16.31.51", + o: "lowercase/o - 2022.1.11.16.32.30", + p: "lowercase/p - 2022.1.11.16.35.17", + q: "lowercase/q - 2022.1.11.16.36.26", + r: "lowercase/r - 2022.1.11.16.39.47", + s: "lowercase/s - 2022.1.11.16.41.22", + t: "lowercase/t - 2022.1.11.16.42.16", + u: "lowercase/u - 2022.1.11.16.43.31", + v: "lowercase/v - 2022.1.11.16.44.21", + w: "lowercase/w - 2022.1.11.16.45.21", + x: "lowercase/x - 2022.1.11.16.45.58", + y: "lowercase/y - 2022.1.11.16.47.21", + z: "lowercase/z - 2022.1.11.16.48.15", + \u00F8: "lowercase/oslash - 2025.5.9.10.30.15.123", + // Updated Danish lowercase o-slash + A: "uppercase/A - 2022.1.11.18.30.32", + B: "uppercase/B - 2022.1.11.18.13.14", + C: "uppercase/C - 2022.1.11.18.14.00", + D: "uppercase/D - 2022.1.11.18.14.38", + E: "uppercase/E - 2022.1.11.18.15.14", + F: "uppercase/F - 2022.1.11.18.15.47", + G: "uppercase/G - 2022.1.11.18.16.34", + H: "uppercase/H - 2022.1.11.18.17.13", + I: "uppercase/I - 2022.1.11.18.18.01", + J: "uppercase/J - 2022.1.11.18.18.41", + K: "uppercase/K - 2022.1.11.18.19.20", + L: "uppercase/L - 2022.1.11.18.19.53", + M: "uppercase/M - 2022.1.11.18.24.51", + N: "uppercase/N - 2022.1.11.18.31.55", + O: "uppercase/O - 2022.1.11.18.32.33", + P: "uppercase/P - 2022.1.11.18.33.17", + Q: "uppercase/Q - 2022.1.11.18.34.00", + R: "uppercase/R - 2022.1.11.18.35.27", + S: "uppercase/S - 2022.1.11.18.36.12", + T: "uppercase/T - 2022.1.11.18.36.42", + U: "uppercase/U - 2022.1.11.18.37.17", + V: "uppercase/V - 2022.1.11.18.37.54", + W: "uppercase/W - 2022.1.11.18.39.11", + X: "uppercase/X - 2022.1.11.18.50.18", + Y: "uppercase/Y - 2022.1.11.18.52.28", + Z: "uppercase/Z - 2022.1.11.18.53.22", + \u00D8: "uppercase/Oslash - 2025.5.9.10.30.15.123", + // Updated Danish uppercase O-slash + "@": "symbols/at - 2022.1.11.17.09.12", + "&": "symbols/ampersand - 2022.1.11.18.06.40", + "#": "symbols/hash - 2022.1.11.17.04.12", + $: "symbols/dollar - 2022.1.11.16.59.42", + "'": "symbols/apostrophe - 2022.1.11.18.09.59", + "*": "symbols/asterisk - 2022.1.11.17.00.45", + "\\": "symbols/backslash - 2022.1.11.17.11.17", + "^": "symbols/caret - 2022.1.11.17.07.56", + ":": "symbols/colon - 2022.1.11.18.15.47", + ",": "symbols/comma - 2022.1.11.18.12.57", + "=": "symbols/equal - 2022.1.11.17.02.12", + "!": "symbols/exclamation - 2022.1.11.18.10.47", + ">": "symbols/greater than - 2022.1.11.16.58.41", + "{": "symbols/L brace - 2022.1.11.16.53.15", + "[": "symbols/L bracket - 2022.1.11.16.54.53", + "(": "symbols/L paren - 2022.1.11.16.56.12", + "<": "symbols/less than - 2022.1.11.16.58.05", + "-": "symbols/minus - 2022.1.11.17.01.14", + "%": "symbols/percent - 2022.1.11.17.06.43", + ".": "symbols/period - 2022.1.11.18.13.56", + "+": "symbols/plus - 2022.1.11.17.01.43", + "?": "symbols/question mark - 2022.1.11.18.09.24", + '"': "symbols/quotes - 2022.1.11.18.10.19", + "}": "symbols/R brace - 2022.1.11.16.54.15", + "]": "symbols/R bracket - 2022.1.11.16.55.19", + ")": "symbols/R paren - 2022.1.11.16.57.09", + ";": "symbols/semi colon - 2022.1.11.18.14.51", + "/": "symbols/slash - 2022.1.11.17.03.28", + "~": "symbols/tilde - 2022.1.11.18.08.35", + _: "symbols/underscore - 2022.1.11.17.04.46", + "|": "symbols/vertical line - 2022.1.11.18.07.22", + "\u2026": "symbols/ellipsis - 2024.4.27.09.25.16.717" +}; +var unifont = { + glyphHeight: 16, + glyphWidth: 8, + proportional: false, + // Keep as monospace for Latin characters (primary use case) + bdfFont: "unifont-16.0.03" + // Specify the exact BDF file to use +}; +var MatrixChunky8 = { + glyphHeight: 8, + // glyphWidth removed - using character-specific advance widths from BDF DWIDTH + baseline: 0, + // No global baseline - individual BDF offsets handle all positioning + bdfFont: "MatrixChunky8", + // Indicates this uses the BDF endpoint + proportional: true, + // Flag indicating this is a proportional font + // Character advance widths for proportional spacing + advances: { + " ": 2, + "!": 2, + '"': 4, + "#": 6, + "$": 4, + "%": 4, + "&": 5, + "'": 2, + "(": 3, + // Match native BDF DWIDTH (was 4 — overshot kerning) + ")": 3, + // Match native BDF DWIDTH (was 4 — overshot kerning) + "*": 6, + "+": 4, + ",": 2, + "-": 4, + ".": 2, + "/": 4, + "0": 4, + "1": 4, + "2": 4, + "3": 4, + "4": 4, + "5": 4, + "6": 4, + "7": 4, + "8": 4, + "9": 4, + ":": 2, + ";": 2, + "<": 4, + "=": 4, + ">": 4, + "?": 4, + "@": 5, + "A": 4, + "B": 4, + "C": 4, + "D": 4, + "E": 4, + "F": 4, + "G": 5, + "H": 5, + "I": 4, + "J": 4, + "K": 4, + "L": 4, + "M": 6, + "N": 5, + "O": 6, + // Extra breathing room after O (e.g. "OL" in SOLD) + "P": 4, + "Q": 5, + "R": 4, + "S": 4, + "T": 4, + "U": 4, + "V": 4, + "W": 6, + "X": 4, + "Y": 4, + "Z": 4, + "[": 4, + "\\": 4, + "]": 4, + "^": 4, + "_": 4, + "`": 3, + "a": 4, + "b": 4, + "c": 4, + "d": 4, + "e": 4, + "f": 4, + "g": 4, + "h": 4, + "i": 4, + "j": 4, + "k": 4, + "l": 4, + "m": 6, + "n": 4, + "o": 4, + "p": 4, + "q": 4, + "r": 4, + "s": 4, + "t": 4, + "u": 4, + "v": 4, + "w": 6, + "x": 4, + "y": 4, + "z": 4, + "{": 4, + "|": 2, + "}": 4, + "~": 4 + }, + // BDF overrides for character positioning adjustments + bdfOverrides: { + // Star advance width (6) handles right-side spacing. + // No vertical overrides: BDF bbox metrics already carry each glyph's + // baseline offset, so 'y' descends on its own. + } +}; +var microtype = { + glyphHeight: 5, + glyphWidth: 4, + // Increased from 3 to 4 for better spacing + proportional: false, + // Monospace font - fixed character width + // Numbers (0-9) + 0: "microtype/numbers/0", + 1: "microtype/numbers/1", + 2: "microtype/numbers/2", + 3: "microtype/numbers/3", + 4: "microtype/numbers/4", + 5: "microtype/numbers/5", + 6: "microtype/numbers/6", + 7: "microtype/numbers/7", + 8: "microtype/numbers/8", + 9: "microtype/numbers/9", + // Uppercase letters (A-Z) + A: "microtype/uppercase/A", + B: "microtype/uppercase/B", + C: "microtype/uppercase/C", + D: "microtype/uppercase/D", + E: "microtype/uppercase/E", + F: "microtype/uppercase/F", + G: "microtype/uppercase/G", + H: "microtype/uppercase/H", + I: "microtype/uppercase/I", + J: "microtype/uppercase/J", + K: "microtype/uppercase/K", + L: "microtype/uppercase/L", + M: "microtype/uppercase/M", + N: "microtype/uppercase/N", + O: "microtype/uppercase/O", + P: "microtype/uppercase/P", + Q: "microtype/uppercase/Q", + R: "microtype/uppercase/R", + S: "microtype/uppercase/S", + T: "microtype/uppercase/T", + U: "microtype/uppercase/U", + V: "microtype/uppercase/V", + W: "microtype/uppercase/W", + X: "microtype/uppercase/X", + Y: "microtype/uppercase/Y", + Z: "microtype/uppercase/Z", + // Lowercase letters (a-z) + a: "microtype/lowercase/a", + b: "microtype/lowercase/b", + c: "microtype/lowercase/c", + d: "microtype/lowercase/d", + e: "microtype/lowercase/e", + f: "microtype/lowercase/f", + g: "microtype/lowercase/g", + h: "microtype/lowercase/h", + i: "microtype/lowercase/i", + j: "microtype/lowercase/j", + k: "microtype/lowercase/k", + l: "microtype/lowercase/l", + m: "microtype/lowercase/m", + n: "microtype/lowercase/n", + o: "microtype/lowercase/o", + p: "microtype/lowercase/p", + q: "microtype/lowercase/q", + r: "microtype/lowercase/r", + s: "microtype/lowercase/s", + t: "microtype/lowercase/t", + u: "microtype/lowercase/u", + v: "microtype/lowercase/v", + w: "microtype/lowercase/w", + x: "microtype/lowercase/x", + y: "microtype/lowercase/y", + z: "microtype/lowercase/z" +}; + +// public/aesthetic.computer/dep/idb.js +var e; +var t; +var n = /* @__PURE__ */ new WeakMap(); +var r = /* @__PURE__ */ new WeakMap(); +var o = /* @__PURE__ */ new WeakMap(); +var s = /* @__PURE__ */ new WeakMap(); +var a = /* @__PURE__ */ new WeakMap(); +var i = { get(e2, t2, n2) { + if (e2 instanceof IDBTransaction) { + if ("done" === t2) return r.get(e2); + if ("objectStoreNames" === t2) return e2.objectStoreNames || o.get(e2); + if ("store" === t2) return n2.objectStoreNames[1] ? void 0 : n2.objectStore(n2.objectStoreNames[0]); + } + return u(e2[t2]); +}, set: (e2, t2, n2) => (e2[t2] = n2, true), has: (e2, t2) => e2 instanceof IDBTransaction && ("done" === t2 || "store" === t2) || t2 in e2 }; +function c3(e2) { + return e2 !== IDBDatabase.prototype.transaction || "objectStoreNames" in IDBTransaction.prototype ? (t || (t = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey])).includes(e2) ? function(...t2) { + return e2.apply(l(this), t2), u(n.get(this)); + } : function(...t2) { + return u(e2.apply(l(this), t2)); + } : function(t2, ...n2) { + const r2 = e2.call(l(this), t2, ...n2); + return o.set(r2, t2.sort ? t2.sort() : [t2]), u(r2); + }; +} +function d(t2) { + return "function" == typeof t2 ? c3(t2) : (t2 instanceof IDBTransaction && (function(e2) { + if (r.has(e2)) return; + const t3 = new Promise(((t4, n3) => { + const r2 = () => { + e2.removeEventListener("complete", o2), e2.removeEventListener("error", s2), e2.removeEventListener("abort", s2); + }, o2 = () => { + t4(), r2(); + }, s2 = () => { + n3(e2.error || new DOMException("AbortError", "AbortError")), r2(); + }; + e2.addEventListener("complete", o2), e2.addEventListener("error", s2), e2.addEventListener("abort", s2); + })); + r.set(e2, t3); + })(t2), n2 = t2, (e || (e = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction])).some(((e2) => n2 instanceof e2)) ? new Proxy(t2, i) : t2); + var n2; +} +function u(e2) { + if (e2 instanceof IDBRequest) return (function(e3) { + const t3 = new Promise(((t4, n2) => { + const r2 = () => { + e3.removeEventListener("success", o2), e3.removeEventListener("error", s2); + }, o2 = () => { + t4(u(e3.result)), r2(); + }, s2 = () => { + n2(e3.error), r2(); + }; + e3.addEventListener("success", o2), e3.addEventListener("error", s2); + })); + return t3.then(((t4) => { + t4 instanceof IDBCursor && n.set(t4, e3); + })).catch((() => { + })), a.set(t3, e3), t3; + })(e2); + if (s.has(e2)) return s.get(e2); + const t2 = d(e2); + return t2 !== e2 && (s.set(e2, t2), a.set(t2, e2)), t2; +} +var l = (e2) => a.get(e2); +function f(e2, t2, { blocked: n2, upgrade: r2, blocking: o2, terminated: s2 } = {}) { + const a2 = indexedDB.open(e2, t2), i2 = u(a2); + return r2 && a2.addEventListener("upgradeneeded", ((e3) => { + r2(u(a2.result), e3.oldVersion, e3.newVersion, u(a2.transaction), e3); + })), n2 && a2.addEventListener("blocked", ((e3) => n2(e3.oldVersion, e3.newVersion, e3))), i2.then(((e3) => { + s2 && e3.addEventListener("close", (() => s2())), o2 && e3.addEventListener("versionchange", ((e4) => o2(e4.oldVersion, e4.newVersion, e4))); + })).catch((() => { + })), i2; +} +var D = ["get", "getKey", "getAll", "getAllKeys", "count"]; +var v = ["put", "add", "delete", "clear"]; +var b = /* @__PURE__ */ new Map(); +function I(e2, t2) { + if (!(e2 instanceof IDBDatabase) || t2 in e2 || "string" != typeof t2) return; + if (b.get(t2)) return b.get(t2); + const n2 = t2.replace(/FromIndex$/, ""), r2 = t2 !== n2, o2 = v.includes(n2); + if (!(n2 in (r2 ? IDBIndex : IDBObjectStore).prototype) || !o2 && !D.includes(n2)) return; + const s2 = async function(e3, ...t3) { + const s3 = this.transaction(e3, o2 ? "readwrite" : "readonly"); + let a2 = s3.store; + return r2 && (a2 = a2.index(t3.shift())), (await Promise.all([a2[n2](...t3), o2 && s3.done]))[0]; + }; + return b.set(t2, s2), s2; +} +i = ((e2) => ({ ...e2, get: (t2, n2, r2) => I(t2, n2) || e2.get(t2, n2, r2), has: (t2, n2) => !!I(t2, n2) || e2.has(t2, n2) }))(i); + +// public/aesthetic.computer/lib/type.mjs +var glyphDbPromise = null; +var GLYPH_CACHE_VERSION = 2; +var GLYPH_STORE_NAME = "glyphs"; +var META_STORE_NAME = "glyph-meta"; +var glyphMemoryCache = /* @__PURE__ */ new Map(); +var glyphCacheInitialized = false; +var glyphCacheInitPromise = null; +async function initGlyphCache() { + if (glyphCacheInitialized) return glyphDbPromise; + if (glyphCacheInitPromise) return glyphCacheInitPromise; + glyphCacheInitPromise = (async () => { + try { + glyphDbPromise = await f("ac-glyph-cache", GLYPH_CACHE_VERSION, { + upgrade(db, oldVersion, newVersion) { + if (db.objectStoreNames.contains(GLYPH_STORE_NAME)) { + db.deleteObjectStore(GLYPH_STORE_NAME); + } + if (db.objectStoreNames.contains(META_STORE_NAME)) { + db.deleteObjectStore(META_STORE_NAME); + } + db.createObjectStore(GLYPH_STORE_NAME); + db.createObjectStore(META_STORE_NAME); + console.log(`\u{1F524} Glyph cache upgraded: v${oldVersion} \u2192 v${newVersion}`); + } + }); + glyphCacheInitialized = true; + return glyphDbPromise; + } catch (err) { + if (!checkPackMode()) { + console.warn("\u{1F524} Glyph cache unavailable:", err.message); + } + glyphCacheInitialized = true; + return null; + } + })(); + return glyphCacheInitPromise; +} +async function cacheGlyph(fontName, char, glyphData) { + if (!glyphData) return; + const key = `${fontName}:${char}`; + glyphMemoryCache.set(key, glyphData); + const db = await initGlyphCache(); + if (!db) return; + try { + await db.put(GLYPH_STORE_NAME, glyphData, key); + } catch (err) { + } +} +async function cacheGlyphsBulk(fontName, glyphsMap) { + if (!glyphsMap || typeof glyphsMap !== "object") return; + const db = await initGlyphCache(); + for (const [char, data] of Object.entries(glyphsMap)) { + if (data) { + glyphMemoryCache.set(`${fontName}:${char}`, data); + } + } + if (!db) return; + try { + const tx = db.transaction(GLYPH_STORE_NAME, "readwrite"); + const store2 = tx.objectStore(GLYPH_STORE_NAME); + const promises = Object.entries(glyphsMap).map(([char, data]) => { + if (data) { + return store2.put(data, `${fontName}:${char}`); + } + }).filter(Boolean); + await Promise.all([...promises, tx.done]); + } catch (err) { + console.warn("\u{1F524} Bulk glyph cache write failed:", err.message); + } +} +async function preWarmGlyphCache(fontName) { + const db = await initGlyphCache(); + if (!db) return 0; + try { + const tx = db.transaction(GLYPH_STORE_NAME, "readonly"); + const store2 = tx.objectStore(GLYPH_STORE_NAME); + const allKeys = await store2.getAllKeys(); + const fontKeys = allKeys.filter((k) => typeof k === "string" && k.startsWith(`${fontName}:`)); + if (fontKeys.length === 0) return 0; + const loadPromises = fontKeys.map(async (key) => { + const data = await db.get(GLYPH_STORE_NAME, key); + if (data) { + glyphMemoryCache.set(key, data); + } + }); + await Promise.all(loadPromises); + return fontKeys.length; + } catch (err) { + console.warn("\u{1F524} Glyph cache pre-warm failed:", err.message); + return 0; + } +} +async function clearGlyphCache(fontName) { + const db = await initGlyphCache(); + for (const key of glyphMemoryCache.keys()) { + if (!fontName || key.startsWith(`${fontName}:`)) { + glyphMemoryCache.delete(key); + } + } + if (!db) return; + try { + if (fontName) { + const tx = db.transaction(GLYPH_STORE_NAME, "readwrite"); + const store2 = tx.objectStore(GLYPH_STORE_NAME); + const allKeys = await store2.getAllKeys(); + for (const key of allKeys) { + if (typeof key === "string" && key.startsWith(`${fontName}:`)) { + await store2.delete(key); + } + } + await tx.done; + } else { + await db.clear(GLYPH_STORE_NAME); + } + console.log(`\u{1F524} Glyph cache cleared${fontName ? ` for ${fontName}` : ""}`); + } catch (err) { + console.warn("\u{1F524} Glyph cache clear failed:", err.message); + } +} +if (typeof window !== "undefined") { + window.acGlyphCache = { + preWarm: preWarmGlyphCache, + clear: clearGlyphCache, + stats: () => ({ + memorySize: glyphMemoryCache.size, + initialized: glyphCacheInitialized + }) + }; +} +var { floor: floor9, min: min7 } = Math; +var { keys: keys2, entries } = Object; +var undef = void 0; +var Typeface = class { + data; + name; + glyphs = {}; + advanceCache = /* @__PURE__ */ new Map(); + //loaded = false; + constructor(name = "font_1") { + this.name = name; + this.data = fonts_exports[name] || font_1; + } + // Return only the character index from the data. + get glyphData() { + const glyphsOnly = { ...this.data }; + delete glyphsOnly.glyphHeight; + delete glyphsOnly.glyphWidth; + return glyphsOnly; + } + get blockWidth() { + if (this.data.proportional === true || this.data.bdfFont === "MatrixChunky8" || this.name === "MatrixChunky8") { + return 4; + } + return this.data.glyphWidth; + } + get blockHeight() { + return this.data.glyphHeight; + } + async load($preload, needsPaintCallback) { + if (this.name === "font_1") { + const cachedCount = await preWarmGlyphCache(this.name); + if (cachedCount > 0) { + for (const [key, data] of glyphMemoryCache.entries()) { + if (key.startsWith(`${this.name}:`)) { + const char = key.slice(this.name.length + 1); + this.glyphs[char] = data; + } + } + } + const fontMetadataKeys = /* @__PURE__ */ new Set([ + "glyphHeight", + "glyphWidth", + "proportional", + "bdfFallback", + "bdfFont", + "advances", + "baseline" + ]); + const glyphsToLoad = entries(this.data).filter(([g, loc]) => { + if (fontMetadataKeys.has(g)) return false; + return !g.startsWith("glyph") && typeof loc === "string" && loc !== "false" && loc.length > 0; + }); + const glyphsNeedingFetch = glyphsToLoad.filter(([glyph]) => !this.glyphs[glyph]); + if (glyphsNeedingFetch.length > 0) { + console.log(`\u{1F524} font_1 loading ${glyphsNeedingFetch.length}/${glyphsToLoad.length} glyphs (${cachedCount} cached)`); + } + const glyphsToCache = {}; + let resolved = 0, rejected = 0; + const promises = glyphsNeedingFetch.map(([glyph, location2], i2) => { + return $preload( + `aesthetic.computer/disks/drawings/${this.name}/${location2}.json` + ).then((res) => { + this.glyphs[glyph] = res; + glyphsToCache[glyph] = res; + resolved++; + }).catch((err) => { + rejected++; + }); + }); + await Promise.all(promises); + if (Object.keys(glyphsToCache).length > 0) { + cacheGlyphsBulk(this.name, glyphsToCache); + } + if (this.data.bdfFallback) { + const bdfFontName = this.data.bdfFallback; + const handDrawnChars = new Set(Object.keys(this.glyphs)); + const loadingGlyphs = /* @__PURE__ */ new Set(); + const failedGlyphs = /* @__PURE__ */ new Set(); + const batchQueue = []; + let batchTimeout = null; + const BATCH_DELAY = 16; + const MAX_BATCH_SIZE = 50; + const flushBatch = async () => { + if (batchQueue.length === 0) return; + const batch = batchQueue.splice(0, MAX_BATCH_SIZE); + batchTimeout = null; + const codePointStrs = batch.map((item) => item.codePointStr).filter((s2) => s2 && s2.length > 0 && /^[0-9A-F_]+$/i.test(s2)); + if (codePointStrs.length === 0) { + for (const item of batch) { + failedGlyphs.add(item.char); + loadingGlyphs.delete(item.char); + } + return; + } + const charsParam = codePointStrs.join(","); + try { + const apiUrl = typeof window !== "undefined" && window.acSPIDER ? `https://aesthetic.computer/api/bdf-glyph?chars=${charsParam}&font=${bdfFontName}` : `/api/bdf-glyph?chars=${charsParam}&font=${bdfFontName}`; + const response = await fetch(apiUrl); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + const glyphs = data.glyphs || {}; + const glyphsToCache2 = {}; + for (const item of batch) { + const glyphData = glyphs[item.codePointStr]; + if (glyphData) { + item.target[item.char] = glyphData; + glyphsToCache2[item.char] = glyphData; + this.invalidateAdvance(item.char); + if (needsPaintCallback && typeof needsPaintCallback === "function") { + needsPaintCallback(); + } + } else { + failedGlyphs.add(item.char); + } + loadingGlyphs.delete(item.char); + } + if (Object.keys(glyphsToCache2).length > 0) { + cacheGlyphsBulk(bdfFontName, glyphsToCache2); + } + } catch (err) { + for (const item of batch) { + failedGlyphs.add(item.char); + loadingGlyphs.delete(item.char); + } + } + if (batchQueue.length > 0 && !batchTimeout) { + batchTimeout = setTimeout(flushBatch, BATCH_DELAY); + } + }; + const queueGlyphFetch = (char, codePointStr, target) => { + batchQueue.push({ char, codePointStr, target }); + if (batchQueue.length >= MAX_BATCH_SIZE) { + if (batchTimeout) { + clearTimeout(batchTimeout); + batchTimeout = null; + } + flushBatch(); + } else if (!batchTimeout) { + batchTimeout = setTimeout(flushBatch, BATCH_DELAY); + } + }; + preWarmGlyphCache(bdfFontName).then((count) => { + if (count > 0) { + for (const [key, data] of glyphMemoryCache.entries()) { + if (key.startsWith(`${bdfFontName}:`)) { + const char = key.slice(bdfFontName.length + 1); + if (!handDrawnChars.has(char) && !this.glyphs[char]) { + this.glyphs[char] = data; + } + } + } + if (needsPaintCallback && typeof needsPaintCallback === "function") { + needsPaintCallback(); + } + } + }); + this.glyphs = new Proxy(this.glyphs, { + get: (target, char) => { + if (target[char]) return target[char]; + const cacheKey = `${bdfFontName}:${char}`; + if (glyphMemoryCache.has(cacheKey)) { + const cached = glyphMemoryCache.get(cacheKey); + target[char] = cached; + return cached; + } + if (typeof char !== "string" || char.length === 0 || char.trim() === "") return null; + if (failedGlyphs.has(char)) return null; + if (loadingGlyphs.has(char)) return null; + loadingGlyphs.add(char); + const codePoints = []; + try { + for (const singleChar of Array.from(char)) { + const cp = singleChar.codePointAt(0); + if (cp !== void 0 && !(cp >= 55296 && cp <= 57343)) { + codePoints.push( + cp.toString(16).toUpperCase().padStart(cp > 65535 ? 5 : 4, "0") + ); + } + } + } catch (e2) { + codePoints.push("FFFD"); + } + if (codePoints.length === 0) codePoints.push("FFFD"); + queueGlyphFetch(char, codePoints.join("_"), target); + return null; + } + }); + } + console.log("\u{1F524} font_1 load() complete \u2014 BDF Proxy set up"); + } else if (this.name === "microtype") { + const cachedCount = await preWarmGlyphCache(this.name); + if (cachedCount > 0) { + for (const [key, data] of glyphMemoryCache.entries()) { + if (key.startsWith(`${this.name}:`)) { + const char = key.slice(this.name.length + 1); + this.glyphs[char] = data; + } + } + } + const glyphsToLoad = entries(this.data).filter( + ([g, loc]) => !g.startsWith("glyph") && typeof loc === "string" && loc !== "false" && loc.length > 0 + ); + const glyphsNeedingFetch = glyphsToLoad.filter(([glyph]) => !this.glyphs[glyph]); + const glyphsToCache = {}; + const promises = glyphsNeedingFetch.map(([glyph, location2], i2) => { + const path = `aesthetic.computer/disks/drawings/${location2}.json`; + return $preload(path).then((res) => { + this.glyphs[glyph] = res; + glyphsToCache[glyph] = res; + }).catch((err) => { + console.error(`\u274C Couldn't load microtype glyph "${glyph}":`, err); + }); + }); + await Promise.all(promises); + if (Object.keys(glyphsToCache).length > 0) { + cacheGlyphsBulk(this.name, glyphsToCache); + } + } else if (this.name === "unifont" || this.data.bdfFont) { + const fontName = this.data.bdfFont || "unifont"; + const { checkPackMode: checkPackMode2 } = await import("./pack-mode.mjs"); + const isObjktMode = checkPackMode2(); + if (isObjktMode && this.name === "MatrixChunky8") { + this.data.proportional = true; + this.data.bdfFont = "MatrixChunky8"; + this.data.name = "MatrixChunky8"; + const { MatrixChunky8: MatrixChunky82 } = await import("../disks/common/fonts.mjs"); + this.data.advances = MatrixChunky82.advances || {}; + this.data.bdfOverrides = MatrixChunky82.bdfOverrides || {}; + const inlineGlyphMap = typeof globalThis !== "undefined" && globalThis.acOBJKT_MATRIX_CHUNKY_GLYPHS || typeof window !== "undefined" && window.acOBJKT_MATRIX_CHUNKY_GLYPHS || null; + if (inlineGlyphMap && typeof inlineGlyphMap === "object") { + if (!this.data || typeof this.data !== "object") { + this.data = {}; + } + this.inlineMatrixChunkyGlyphMap = inlineGlyphMap; + this.data.inlineMatrixChunkyGlyphMap = inlineGlyphMap; + } + const chars = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + " ", + ".", + ",", + "!", + "?", + ":", + ";", + "-", + "+", + "=", + "<", + ">", + "/", + "\\", + "|", + '"', + "'", + "(", + ")", + "[", + "]", + "{", + "}", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "_", + "~" + ]; + const promises = chars.map(async (char) => { + try { + const charCode = char.charCodeAt(0); + const hexCode = charCode.toString(16).toUpperCase().padStart(4, "0"); + const glyphData = inlineGlyphMap?.[hexCode]; + if (!glyphData) { + return null; + } + this.data[char] = glyphData; + this.invalidateAdvance(char); + return glyphData; + } catch (err) { + return null; + } + }); + await Promise.all(promises); + } + this.glyphs["?"] = { + resolution: [3, 7], + offset: [0, 1], + baselineOffset: [0, 0], + advance: 4, + pixels: [ + [1, 1, 1], + // E0 = 11100000 + [1, 0, 1], + // A0 = 10100000 + [0, 0, 1], + // 20 = 00100000 + [0, 1, 1], + // 60 = 01100000 + [0, 1, 0], + // 40 = 01000000 + [0, 0, 0], + // 00 = 00000000 + [0, 1, 0] + // 40 = 01000000 + ] + }; + this.glyphs["\u263A"] = { + resolution: [6, 9], + pixels: [ + [0, 1, 1, 1, 1, 0], + [1, 0, 0, 0, 0, 1], + [1, 0, 1, 0, 1, 0], + [1, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0] + ] + }; + const loadingGlyphs = /* @__PURE__ */ new Set(); + const failedGlyphs = /* @__PURE__ */ new Set(); + const batchQueue = []; + let batchTimeout = null; + const BATCH_DELAY = 16; + const MAX_BATCH_SIZE = 50; + const flushBatch = async () => { + if (batchQueue.length === 0) return; + const batch = batchQueue.splice(0, MAX_BATCH_SIZE); + batchTimeout = null; + const codePointStrs = batch.map((item) => item.codePointStr).filter((s2) => s2 && s2.length > 0 && /^[0-9A-F_]+$/i.test(s2)); + if (codePointStrs.length === 0) { + console.warn("Batch glyph fetch skipped: no valid code points. Raw batch:", batch.map((b2) => ({ char: b2.char, str: b2.codePointStr }))); + for (const item of batch) { + failedGlyphs.add(item.char); + loadingGlyphs.delete(item.char); + } + return; + } + const charsParam = codePointStrs.join(","); + log.net.verbose(`Batch glyph fetch: ${codePointStrs.length} chars, font=${this.name}`); + const batchStart = performance.now(); + try { + const apiUrl = typeof window !== "undefined" && window.acSPIDER ? `https://aesthetic.computer/api/bdf-glyph?chars=${charsParam}&font=${this.name}` : `/api/bdf-glyph?chars=${charsParam}&font=${this.name}`; + const response = await fetch(apiUrl); + if (!response.ok) { + const errorText = await response.text(); + console.error(`\u{1F524} Batch glyph API error (${response.status}):`, errorText, `URL: ${apiUrl}`); + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + const glyphs = data.glyphs || {}; + const batchDuration = performance.now() - batchStart; + const glyphsToCache = {}; + for (const item of batch) { + const glyphData = glyphs[item.codePointStr]; + if (glyphData) { + item.target[item.char] = glyphData; + glyphsToCache[item.char] = glyphData; + this.invalidateAdvance(item.char); + if (needsPaintCallback && typeof needsPaintCallback === "function") { + needsPaintCallback(); + } + } else { + failedGlyphs.add(item.char); + } + loadingGlyphs.delete(item.char); + } + if (Object.keys(glyphsToCache).length > 0) { + cacheGlyphsBulk(this.name, glyphsToCache); + } + } catch (err) { + console.warn(`Batch glyph fetch failed:`, err); + for (const item of batch) { + failedGlyphs.add(item.char); + loadingGlyphs.delete(item.char); + } + } + if (batchQueue.length > 0 && !batchTimeout) { + batchTimeout = setTimeout(flushBatch, BATCH_DELAY); + } + }; + const queueGlyphFetch = (char, codePointStr, target) => { + batchQueue.push({ char, codePointStr, target }); + if (batchQueue.length >= MAX_BATCH_SIZE) { + if (batchTimeout) { + clearTimeout(batchTimeout); + batchTimeout = null; + } + flushBatch(); + } else if (!batchTimeout) { + batchTimeout = setTimeout(flushBatch, BATCH_DELAY); + } + }; + const font1LoadingGlyphs = /* @__PURE__ */ new Set(); + const font1FailedGlyphs = /* @__PURE__ */ new Set(); + const getFont1CachedGlyph = (char) => { + const direct = glyphMemoryCache.get(`font_1:${char}`); + if (direct) return direct; + return glyphMemoryCache.get(`font_1:?`) || null; + }; + const queueFont1FallbackFetch = (char, target) => { + if (font1LoadingGlyphs.has(char) || font1FailedGlyphs.has(char)) return; + const font1Path = fonts_exports?.font_1?.[char] || fonts_exports?.font_1?.["?"]; + if (typeof font1Path !== "string" || font1Path.length === 0 || font1Path === "false") { + font1FailedGlyphs.add(char); + return; + } + font1LoadingGlyphs.add(char); + $preload(`aesthetic.computer/disks/drawings/font_1/${font1Path}.json`).then((glyph) => { + if (!glyph) { + font1FailedGlyphs.add(char); + return; + } + target[char] = glyph; + this.invalidateAdvance(char); + cacheGlyph("font_1", char, glyph); + if (needsPaintCallback && typeof needsPaintCallback === "function") { + needsPaintCallback(); + } + }).catch(() => { + font1FailedGlyphs.add(char); + }).finally(() => { + font1LoadingGlyphs.delete(char); + }); + }; + preWarmGlyphCache(this.name).then((count) => { + if (count > 0) { + for (const [key, data] of glyphMemoryCache.entries()) { + if (key.startsWith(`${this.name}:`)) { + const char = key.slice(this.name.length + 1); + if (!this.glyphs[char]) { + Object.defineProperty(this.glyphs, char, { + value: data, + writable: true, + enumerable: true, + configurable: true + }); + } + } + } + if (needsPaintCallback && typeof needsPaintCallback === "function") { + needsPaintCallback(); + } + } + }); + this.glyphs = new Proxy(this.glyphs, { + get: (target, char) => { + if (target[char]) { + return target[char]; + } + const cacheKey = `${this.name}:${char}`; + if (glyphMemoryCache.has(cacheKey)) { + const cached = glyphMemoryCache.get(cacheKey); + target[char] = cached; + return cached; + } + if (failedGlyphs.has(char)) { + return this.getEmojiFallback(char, target); + } + if (loadingGlyphs.has(char)) { + return this.getEmojiFallback(char, target); + } + if (typeof char !== "string" || char.length === 0 || char.trim() === "") { + return null; + } + loadingGlyphs.add(char); + const codePoints = []; + try { + const characters = Array.from(char); + for (const singleChar of characters) { + const codePoint = singleChar.codePointAt(0); + if (codePoint !== void 0) { + if (codePoint >= 55296 && codePoint <= 57343) { + console.warn( + `Invalid lone surrogate detected: U+${codePoint.toString(16).toUpperCase()} in char "${char}"` + ); + if (char.length >= 2) { + console.warn( + `Original char length: ${char.length}, char codes:`, + Array.from(char).map( + (c4) => `U+${c4.codePointAt(0).toString(16).toUpperCase()}` + ) + ); + } + codePoints.push("FFFD"); + continue; + } + const hexValue = codePoint.toString(16).toUpperCase().padStart(codePoint > 65535 ? 5 : 4, "0"); + codePoints.push(hexValue); + } + } + } catch (error) { + console.warn(`Error processing character "${char}":`, error); + codePoints.push("FFFD"); + } + if (codePoints.length === 0) { + codePoints.push("FFFD"); + } + const codePointStr = codePoints.join("_"); + const isObjktMode2 = checkPackMode2(); + if (isObjktMode2 && this.name === "MatrixChunky8") { + if (target[char] && target[char].pixels && target[char].resolution) { + loadingGlyphs.delete(char); + return target[char]; + } + if (this.data && this.data[char]) { + target[char] = this.data[char]; + loadingGlyphs.delete(char); + return this.data[char]; + } + const inlineGlyphMap = this.inlineMatrixChunkyGlyphMap || this.data?.inlineMatrixChunkyGlyphMap || typeof globalThis !== "undefined" && globalThis.acOBJKT_MATRIX_CHUNKY_GLYPHS || typeof window !== "undefined" && window.acOBJKT_MATRIX_CHUNKY_GLYPHS || null; + if (inlineGlyphMap && codePoints.length === 1) { + const hexKey = codePoints[0]; + const glyphData = inlineGlyphMap[hexKey]; + if (glyphData) { + target[char] = glyphData; + this.data[char] = glyphData; + this.invalidateAdvance(char); + loadingGlyphs.delete(char); + return glyphData; + } + } + loadingGlyphs.delete(char); + failedGlyphs.add(char); + return this.getEmojiFallback(char, target); + } + if (!isObjktMode2) { + queueGlyphFetch(char, codePointStr, target); + } else { + const cachedFont1Glyph = getFont1CachedGlyph(char); + if (cachedFont1Glyph) { + target[char] = cachedFont1Glyph; + loadingGlyphs.delete(char); + return cachedFont1Glyph; + } + queueFont1FallbackFetch(char, target); + const simpleFallback = { + resolution: [6, 8], + pixels: [ + [0, 1, 1, 1, 1, 0], + [1, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 0, 1], + [1, 0, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0] + ] + }; + target[char] = simpleFallback; + loadingGlyphs.delete(char); + return simpleFallback; + } + if (isObjktMode2 && target[char]) return target[char]; + return this.getEmojiFallback(char, target); + } + }); + } + return this; + } + // Helper method to get appropriate fallback for different character types + getEmojiFallback(char, target) { + if (!char || char.length === 0) { + return this.getLoadingPlaceholder(); + } + if (this.name === "MatrixChunky8") { + return this.getLoadingPlaceholder(4, 8); + } + if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03" || this.data?.bdfFont) { + return this.getLoadingPlaceholder(8, 16); + } + const codePoint = char.codePointAt(0); + if (codePoint >= 128512 && codePoint <= 128591) { + return target["\u263A"] || target["?"] || null; + } else if (codePoint >= 127744 && codePoint <= 128511) { + return target["?"] || null; + } else if (codePoint >= 128640 && codePoint <= 128767) { + return target["?"] || null; + } else if (codePoint >= 9728 && codePoint <= 9983) { + return target["?"] || null; + } else { + return target["?"] || null; + } + } + // Create an animated loading placeholder for characters being fetched + getLoadingPlaceholder(width2 = 8, height2 = 16) { + const time = typeof performance !== "undefined" ? performance.now() : Date.now(); + const frame = Math.floor(time / 100) % 4; + const pixels2 = []; + for (let y = 0; y < height2; y++) { + const row = []; + for (let x = 0; x < width2; x++) { + const pattern = (x + y + frame) % 4; + row.push(pattern < 2 ? 1 : 0); + } + pixels2.push(row); + } + return { + resolution: [width2, height2], + pixels: pixels2, + advance: width2, + isPlaceholder: true + // Mark as placeholder so we know to repaint + }; + } + // Create a colored block placeholder for unifont characters that haven't loaded yet + createColoredBlockPlaceholder() { + const colors = []; + const numColors = Math.floor(Math.random() * 3) + 2; + for (let i2 = 0; i2 < numColors; i2++) { + colors.push([ + Math.floor(Math.random() * 200) + 55, + // R: 55-255 + Math.floor(Math.random() * 200) + 55, + // G: 55-255 + Math.floor(Math.random() * 200) + 55 + // B: 55-255 + ]); + } + return { + resolution: [8, 16], + // Standard unifont size + pixels: [], + // Empty pixels - will be drawn as box command + commands: [ + // Fill the entire 8x16 block with random colored pixels + ...Array.from( + { length: 16 }, + (_, y) => Array.from({ length: 8 }, (_2, x) => ({ + name: "point", + args: [x, y], + color: colors[Math.floor(Math.random() * colors.length)] + })) + ).flat() + ], + advance: 8 + // Standard unifont advance width + }; + } + // Get a glyph for a specific character + getGlyph(char) { + try { + return this.glyphs[char]; + } catch (err) { + console.warn(`Failed to get glyph for "${char}":`, err.message); + return null; + } + } + getAdvance(char) { + if (!char) return this.blockWidth || 4; + if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") { + try { + const glyph2 = this.glyphs?.[char]; + if (glyph2 && typeof glyph2.advance === "number") { + return glyph2.advance; + } + } catch (err) { + } + return 8; + } + if (this.advanceCache.has(char)) { + return this.advanceCache.get(char); + } + let advance; + let glyph; + if (this.data?.advances && this.data.advances[char] !== void 0) { + advance = this.data.advances[char]; + } + if (advance === void 0) { + try { + glyph = this.glyphs?.[char]; + if (glyph && typeof glyph.advance === "number") { + advance = glyph.advance; + } + } catch (err) { + } + } + if (advance === void 0) { + const glyphData = this.data?.[char]; + if (glyphData && typeof glyphData.advance === "number") { + advance = glyphData.advance; + } + } + if (advance === void 0 && glyph?.dwidth?.x) { + advance = glyph.dwidth.x; + } + if (advance === void 0 && glyph?.resolution?.[0]) { + advance = glyph.resolution[0]; + } + if (advance === void 0 && typeof this.data?.glyphWidth === "number") { + advance = this.data.glyphWidth; + } + if (advance === void 0) { + advance = this.blockWidth || 4; + } + this.advanceCache.set(char, advance); + return advance; + } + invalidateAdvance(char) { + if (!char) { + this.advanceCache.clear?.(); + } else { + this.advanceCache.delete?.(char); + } + } + // 📓 tf.print + print($, pos = { x: undef, y: undef, size: 1, thickness: 1, rotation: 0 }, lineNumber, text, bg = null, charColors = null) { + const font = this.glyphs; + const fallbackFont = $.typeface?.glyphs || null; + const size = pos.size || 1; + const blockMargin = 1; + const inferredBlockWidth = this.data?.glyphWidth ?? this.blockWidth ?? this.data?.width ?? 6; + const blockHeight = ((this.blockHeight || 10) + blockMargin) * size; + const blockWidth = inferredBlockWidth; + const thickness = pos.thickness || 1; + const rotation = pos.rotation || 0; + const fullWidth = blockWidth * size * text.length; + if (Array.isArray(pos)) { + pos = { x: pos[0], y: pos[1] }; + } + const width2 = $.screen.width; + const height2 = $.screen.height; + let w; + const isProportional = this.data?.proportional === true || this.data?.bdfFont === "MatrixChunky8" || this.name === "MatrixChunky8"; + if (isProportional) { + w = 0; + const chars = [...text.toString()]; + for (const char of chars) { + if (char === "\n") { + continue; + } + const charAdvance = this.getAdvance(char) || inferredBlockWidth; + w += charAdvance * size; + } + } else { + w = text.length * blockWidth * size; + } + if (pos.center === void 0) { + if (pos.right !== void 0) { + pos.x = width2 - w - pos.right; + } else if (pos.left !== void 0) { + pos.x = pos.left; + } else if (pos.x === void 0) { + pos.x = $.num.randIntRange(-fullWidth / 2, width2 + fullWidth / 2); + } + if (pos.bottom !== void 0) { + pos.y = height2 - blockHeight - pos.bottom; + } else if (pos.top !== void 0) { + pos.y = pos.top; + } else if (pos.y === void 0) { + pos.y = $.num.randIntRange(-blockHeight / 2, height2 + blockHeight / 2); + } + } + let x = Math.floor(pos.x || 0), y = Math.floor(pos.y || 0); + pos.center = pos.center || ""; + if (pos.center.includes("x")) { + const hw = w / 2; + x = pos.x === undef ? width2 / 2 - hw : x - hw; + } + if (pos.center.includes("y")) { + const hh = Math.floor(blockHeight / 2); + y = pos.y === undef ? height2 / 2 - hh : y - hh; + } + y += lineNumber * blockHeight; + const rn = $.inkrn(); + if (bg !== null && bg !== false) $.ink(bg).box(x, y, fullWidth, blockHeight); + const baselineAdjustment = this.data.baseline || 0; + y += baselineAdjustment; + const rotRad = rotation * Math.PI / 180; + const cosR = Math.cos(rotRad); + const sinR = Math.sin(rotRad); + if (charColors && charColors.length > 0) { + let currentX = 0; + for (let i2 = 0; i2 < text.length; i2++) { + const char = text[i2]; + const charColor = charColors[i2]; + if (charColor) { + if (typeof charColor === "object" && charColor.foreground !== void 0 && charColor.background !== void 0) { + if (charColor.background) { + const charAdvance2 = this.getAdvance(char); + const charWidth = charAdvance2 * size; + if (Array.isArray(charColor.background)) { + $.ink(...charColor.background); + } else { + $.ink(charColor.background); + } + const rotatedX2 = x + (currentX * cosR - 0 * sinR); + const rotatedY2 = y + (currentX * sinR + 0 * cosR); + $.box(rotatedX2, rotatedY2, charWidth, blockHeight); + } + if (charColor.foreground) { + if (Array.isArray(charColor.foreground)) { + $.ink(...charColor.foreground); + } else { + $.ink(charColor.foreground); + } + } else { + $.ink(...rn); + } + } else if (Array.isArray(charColor)) { + $.ink(...charColor); + } else { + $.ink(charColor); + } + } else { + $.ink(...rn); + } + const rotatedX = Math.round(x + (currentX * cosR - 0 * sinR)); + const rotatedY = Math.round(y + (currentX * sinR + 0 * cosR)); + $.printLine( + char, + font, + rotatedX, + rotatedY, + blockWidth, + size, + 0, + thickness, + rotation, + this.data, + // Pass font metadata to avoid BDF proxy issues + fallbackFont + ); + const charAdvance = this.getAdvance(char); + currentX += charAdvance * size; + } + } else { + if (rotation !== 0) { + let currentX = 0; + for (let i2 = 0; i2 < text.length; i2++) { + const char = text[i2]; + const rotatedX = Math.round(x + (currentX * cosR - 0 * sinR)); + const rotatedY = Math.round(y + (currentX * sinR + 0 * cosR)); + $.ink(...rn).printLine( + char, + font, + rotatedX, + rotatedY, + blockWidth, + size, + 0, + thickness, + rotation, + this.data, + fallbackFont + ); + const charAdvance = this.getAdvance(char); + currentX += charAdvance * size; + } + } else { + $.ink(...rn).printLine( + text, + font, + x, + y, + blockWidth, + size, + 0, + thickness, + rotation, + this.data, + // Pass font metadata to avoid BDF proxy issues + fallbackFont + ); + } + } + } +}; +var TextInput = class { + $; + // a reference to the api. + #text; + // text content + #lastPrintedText = ""; + // a place to cache a previous reply. + #lastUserText = ""; + // cache the user's in-progress edited text. + submittedText = ""; + // cache the user's submitted text. + mute = false; + // Whether to prevent sounds from playing. + shifting = false; + // Whether we are emoving the cursor or not. + #renderSpaces = false; + // Whether to render invisible space characters. " " + // For debugging purposes. + blink; + // block cursor blink timer + showBlink = true; + cursor = "blink"; + // Buttons + enter; + // A button for replying or inputting text. + copy; + // A button for copying to the clipboard, that shows up conditionally. + paste; + // Similar to copy. + canType = false; + //#autolock = true; + #lock = false; + #lockTimeout; + #showSpinner = false; + #prompt; + hideGutter = false; + #gutterMax; + #activatingPress = false; + #deactivatingPress = false; + // Set on touch when active, deferred to lift for VSCode curtain close + #edgeCancelled = false; + #manuallyDeactivated = false; + #manualDeactivationTime = 0; + #manuallyActivated = false; + #manualActivationTime = 0; + #preventDeactivation = false; + // Flag to prevent unwanted deactivation during command execution + typeface; + pal; + // color palette + scheme; + #processCommand; + // text processing callback + // #processingCommand = false; + historyDepth = -1; + #prehistory; + #scrubCache = null; + // snapshot of the history stack during a rolodex drag + //inputStarted = false; // Flipped when the TextInput is first activated. + // (To clear any starting text.) + #moveThreshold = 6; + // Drag threshold. + #moveDeltaX = 0; + #recentlyShifting = false; + // Track if we just finished character sliding + runnable = false; + // Whether a command can be tried. + didReset; + // Callback for blank reset. + key; + copiedCallback; + // When the "Copy" button is pressed, for designing wrappers. + #copyPasteTimeout; + // UI Timer for clipboard copy response. + #copyPasteScheme; + // An override for the Copy button's color. + #coatedCopy; + // Stores a version of the current text output that could be + // decorated. (With a URL, for example.) + activate; + // Hook to `activate` inside of act. + activated; + // Optional callback for when the the text input becomes + // activated via pushing the Enter button or typing a key. + activatedOnce = false; + backdropTouchOff = false; + // Determines whether to activate the input + // after tapping the backdrop. + commandSentOnce = false; + // 🏴 + closeOnEmptyEnter = false; + // Add support for loading from preloaded system typeface. + constructor($, text = "", processCommand, options = { + palette: void 0, + font: "font_1", + // fonts.font_1, + //autolock: true, + wrap: "char" + }) { + this.key = `${$.slug}:history`; + this.noHistory = options.history === false; + this.$ = $; + this.closeOnEmptyEnter = options.closeOnEmptyEnter || false; + this.hideGutter = options.hideGutter || false; + this.poe = options.poe || false; + this.copiedCallback = options.copied; + if (!options.font) options.font = "font_1"; + this._needsRepaint = false; + if ($.typeface?.data !== options.font) { + this.typeface = new Typeface(options.font); + this.#moveThreshold = this.typeface.blockWidth; + this.typeface.load($.net.preload, () => { + this._needsRepaint = true; + }); + } else { + this.typeface = $.typeface; + } + this.activated = options.activated; + this.didReset = options.didReset; + const blockWidth = this.typeface.blockWidth; + this.#gutterMax = options.gutterMax || 48; + this.#prompt = new Prompt( + 6, + // blockWidth, + 6, + // blockWidth, + options.wrap || "char", + // "char" or "word" + $.store["gutter:lock"] || Math.min(this.#gutterMax, floor9($.screen.width / blockWidth) - 2), + options.lineSpacing, + this.typeface + ); + this.print(text); + this.startingInput = this.text; + this.scheme = options.scheme || { + dark: { + text: 255, + background: 0, + block: 255, + highlight: 0, + guideline: 255 + }, + light: { + text: 0, + background: 255, + block: 0, + highlight: 255, + guideline: 0 + } + }; + const { + ui: { TextButton: TB } + } = $; + this.enter = new TB(this.scheme.buttons?.enter || "Enter"); + this.enter.stickyScrubbing = true; + this.enter.btn.stickyScrubbing = true; + this.enter.btn.noEdgeDetection = true; + this.copy = new TB(this.scheme.buttons?.copy.label || "Copy"); + this.paste = new TB(this.scheme.buttons?.paste?.label || "Paste"); + this.copy.btn.disabled = true; + this.paste.btn.disabled = true; + if (this.text.length === 0) { + this.enter.btn.disabled = true; + } + this.#processCommand = processCommand; + $.send({ type: "keyboard:enabled" }); + } + // 🔤 Change the font dynamically + async setFont(fontName) { + if (!fontName || fontName === this.typeface?.name) return; + const newTypeface = new Typeface(fontName); + await newTypeface.load(this.$.net.preload, () => { + this._needsRepaint = true; + }); + this.typeface = newTypeface; + this.#moveThreshold = newTypeface.blockWidth; + const blockWidth = newTypeface.blockWidth; + this.#prompt.typeface = newTypeface; + this.#prompt.letterWidth = blockWidth; + this.#prompt.letterHeight = newTypeface.blockHeight; + this._needsRepaint = true; + } + // Stretches the gutter to be the screen width minus two slots. + fullGutter($) { + this.gutter = Math.min( + this.#gutterMax, + floor9($.screen.width / this.#prompt.letterWidth) - 2 + ); + } + set lock(bool) { + this.#lock = bool; + if (bool) { + this.#lockTimeout = setTimeout(() => { + this.#showSpinner = true; + }, 100); + } else { + clearTimeout(this.#lockTimeout); + this.#showSpinner = false; + } + } + get lock() { + return this.#lock; + } + get recentlyShifting() { + return this.#recentlyShifting; + } + // Adjust the gutter width for text wrapping. + set gutter(n2) { + this.#prompt.colWidth = n2; + this.#prompt.gutter = this.#prompt.colWidth * this.#prompt.letterWidth; + } + // Alias for the setter above, returned in columns. + get columns() { + return this.#prompt.colWidth; + } + // Reset the user text after a message is complete. + clearUserText() { + this.#lastUserText = ""; + } + addUserText(txt) { + this.#lastUserText = txt; + } + // Snap cursor to the end of text. + snap() { + this.#prompt.snapTo(this.text); + this.$.send({ + type: "keyboard:cursor", + content: { cursor: this.#text.length } + }); + } + // Run a command + async run(store2) { + this.snap(); + this.submittedText = ""; + await this.#execute(store2); + } + // Set the text and reflow it. + set text(str7) { + if (str7 === this.#text) { + return; + } + this.#text = str7; + this.flow(); + } + // Return the prompt. + get prompt() { + return this.#prompt; + } + print(text) { + this.text = text; + this.bakePrintedText(); + } + bakePrintedText() { + this.#lastPrintedText = this.text; + } + latentFirstPrint(text) { + if (!this.activatedOnce) this.text = text; + } + // Reflow the input text. + flow() { + this.#prompt.mapTo(this.text); + } + // Return the text contents of the input. + get text() { + return this.#text; + } + #coatCopy(text) { + return this.copiedCallback?.(text) || text; + } + // Paint the TextInput, with an optional `frame` for placement. + paint($, clear2 = false, frame = { x: 0, y: 0, width: $.screen.width, height: $.screen.height }) { + this.pal = this.scheme[$.dark ? "dark" : "light"] || this.scheme["dark"] || this.scheme; + const pen = $.pen; + const isHovering = pen && pen.x >= frame.x && pen.x < frame.x + frame.width && pen.y >= frame.y && pen.y < frame.y + frame.height; + if (!clear2 && this.pal.background !== void 0) { + $.ink(this.pal.background).box(frame); + if (isHovering && this.canType && !this.poe) { + const glowColor = this.pal.promptHover || ($.dark ? [100, 80, 150, 30] : [180, 150, 220, 40]); + $.ink(glowColor).box(frame.x, frame.y, frame.width, frame.height); + const borderColor = this.pal.promptHoverBorder || ($.dark ? [150, 100, 200, 80] : [140, 100, 180, 60]); + $.ink(borderColor).box(frame.x, frame.y, frame.width, frame.height, "inline"); + } + } + const ti = this; + const prompt = this.#prompt; + let charColorMap = null; + if ($.system?.prompt?.kidlispMode && this.text) { + try { + const tokens = tokenize(this.text); + const kidlispInstance = new KidLisp(); + kidlispInstance.isEditMode = true; + kidlispInstance.initializeSyntaxHighlighting(this.text); + charColorMap = /* @__PURE__ */ new Map(); + let sourceIndex = 0; + for (let i2 = 0; i2 < tokens.length; i2++) { + const token = tokens[i2]; + const fastMathMatch = token.match(/^(\w+)\s*([+\-*/%])\s*(\w+|\d+(?:\.\d+)?)$/); + if (fastMathMatch) { + const [, left, op, right] = fastMathMatch; + const tokenIndex = this.text.indexOf(token, sourceIndex); + if (tokenIndex !== -1) { + const leftColor = kidlispInstance.getTokenColor(left, [left, op, right], 0); + const opColor = kidlispInstance.getTokenColor(op, [left, op, right], 1); + const rightColor = kidlispInstance.getTokenColor(right, [left, op, right], 2); + for (let j = 0; j < left.length; j++) { + charColorMap.set(tokenIndex + j, leftColor); + } + charColorMap.set(tokenIndex + left.length, opColor); + for (let j = 0; j < right.length; j++) { + charColorMap.set(tokenIndex + left.length + 1 + j, rightColor); + } + sourceIndex = tokenIndex + token.length; + } + continue; + } + if (token.startsWith("fade:")) { + const colorName = kidlispInstance.getTokenColor(token, tokens, i2); + const coloredFadeString = kidlispInstance.colorFadeExpression(token); + const colorCodeRegex = /\\([^\\]+)\\([^\\]*)/g; + let match; + let charOffset = 0; + const tokenIndex = this.text.indexOf(token, sourceIndex); + while ((match = colorCodeRegex.exec(coloredFadeString)) !== null) { + const color3 = match[1]; + const text = match[2]; + for (let j = 0; j < text.length; j++) { + if (tokenIndex >= 0 && charOffset < token.length) { + charColorMap.set(tokenIndex + charOffset, color3); + charOffset++; + } + } + } + if (tokenIndex !== -1) { + sourceIndex = tokenIndex + token.length; + } + } else { + const colorName = kidlispInstance.getTokenColor(token, tokens, i2); + const tokenIndex = this.text.indexOf(token, sourceIndex); + if (tokenIndex !== -1) { + for (let charOffset = 0; charOffset < token.length; charOffset++) { + charColorMap.set(tokenIndex + charOffset, colorName); + } + sourceIndex = tokenIndex + token.length; + } + } + } + } catch (error) { + console.warn("Error building KidLisp syntax highlight map:", error); + charColorMap = null; + } + } + function paintBlockLetter(char, pos, alt = false, charIndex = -1) { + if (char.charCodeAt(0) === 10 && ti.#renderSpaces) { + $.ink([255, 0, 0, 127]).box(pos.x, pos.y, 4); + } else if (char !== " " && char.charCodeAt(0) !== 10) { + const pic = ti.typeface.glyphs[char] || ti.typeface.glyphs["?"]; + let drawColor; + if (charColorMap && charIndex >= 0 && charColorMap.has(charIndex)) { + const colorName = charColorMap.get(charIndex); + if (colorName && colorName.includes(",")) { + const parts = colorName.split(",").map((n2) => parseInt(n2.trim())); + drawColor = parts.length >= 3 ? parts : null; + } else if (colorName && colorName.startsWith("COMPOUND:")) { + const parts = colorName.split(":"); + const actualColorName = parts[1] || "white"; + if (cssColors2[actualColorName]) { + drawColor = cssColors2[actualColorName]; + } else { + drawColor = [255, 255, 255]; + } + } else if (colorName === "RAINBOW" || colorName === "ZEBRA") { + drawColor = !alt ? ti.pal.text : ti.pal.prompt || ti.pal.text; + } else if (cssColors2[colorName]) { + drawColor = cssColors2[colorName]; + } else if (colorName === "gray" || colorName === "grey") { + drawColor = [128, 128, 128]; + } else if (colorName === "orange") { + drawColor = [255, 165, 0]; + } else if (colorName === "lime") { + drawColor = [0, 255, 0]; + } else if (colorName === "pink") { + drawColor = [255, 192, 203]; + } else if (colorName === "darkred") { + drawColor = [139, 0, 0]; + } else if (colorName === "limegreen") { + drawColor = [50, 205, 50]; + } else if (colorName === "hotpink") { + drawColor = [255, 105, 180]; + } else if (colorName === "mediumseagreen") { + drawColor = [60, 179, 113]; + } else { + drawColor = !alt ? ti.pal.text : ti.pal.prompt || ti.pal.text; + } + if (!drawColor) { + drawColor = !alt ? ti.pal.text : ti.pal.prompt || ti.pal.text; + } + } else { + drawColor = !alt ? ti.pal.text : ti.pal.prompt || ti.pal.text; + } + const isLightMode = !$.dark; + if (isLightMode && charColorMap && charIndex >= 0 && charColorMap.has(charIndex)) { + const [r2, g, b2] = Array.isArray(drawColor) ? drawColor : [200, 200, 200]; + const luminance = 0.299 * r2 + 0.587 * g + 0.114 * b2; + if (luminance > 120) { + $.ink([30, 20, 50, 180]).draw( + pic, + { x: pos.x + 1, y: pos.y + 1 }, + prompt.scale + ); + } + } + $.ink(drawColor).draw( + pic, + pos, + prompt.scale + ); + } else if (ti.#renderSpaces) { + $.ink([0, 255, 0, 127]).box(pos.x, pos.y, 3); + } + } + if (frame.x || frame.y) $.pan(frame.x, frame.y); + if (!this.#lock && this.selection && this.canType) { + for (let i2 = this.selection[0]; i2 < this.selection[1]; i2 += 1) { + const c4 = prompt.textToCursorMap[i2]; + const p = prompt.pos(c4, true); + $.ink(this.pal.selection || [255, 255, 0, 64]).box(p); + } + } + let submittedIndex = 0; + Object.keys(prompt.cursorToTextMap).forEach((key) => { + const [x, y] = key.split(":").map((c4) => parseInt(c4)); + const charIndex = prompt.cursorToTextMap[key]; + const char = this.text[charIndex]; + let fromSubmitted = false; + if (!this.canType && submittedIndex < this.submittedText.length) { + if (char === this.submittedText[submittedIndex]) fromSubmitted = true; + submittedIndex += 1; + } + paintBlockLetter(char, prompt.pos({ x, y }), fromSubmitted, charIndex); + }); + if (this.canType && !this.poe) { + if (!this.hideGutter) { + $.ink(this.pal.guideline).line( + prompt.gutter, + 0, + prompt.gutter, + frame.height - 1 + ); + } + $.ink($.dark ? 127 : "teal").box( + 0, + 0, + frame.width, + frame.height, + "inline" + ); + } + if (this.#lock) { + if (this.#showSpinner) { + const center = $.geo.Box.from(prompt.pos()).center; + const distance4 = 2; + const topL = [center.x - distance4, center.y - distance4]; + const topR = [center.x + distance4, center.y - distance4]; + const bottomL = [center.x - distance4, center.y + distance4]; + const bottomR = [center.x + distance4, center.y + distance4]; + const middleL = [center.x - distance4, center.y]; + const middleR = [center.x + distance4, center.y]; + $.ink(this.pal.block); + if ($.paintCount % 60 < 20) { + $.line(...topR, ...bottomL); + } else if ($.paintCount % 60 < 40) { + $.line(...middleL, ...middleR); + } else { + $.line(...topL, ...bottomR); + } + } + } else { + if (this.cursor === "blink" && this.showBlink && this.canType) { + const cursorColor = $.system?.prompt?.actualKidlisp ? $.dark ? [100, 255, 100] : [0, 150, 0] : this.pal.block; + if (this.poe) { + const cp = prompt.pos(void 0, true); + $.ink(this.pal.cursor || cursorColor).box(cp.x, cp.y, 2, cp.h); + } else { + $.ink(cursorColor).box(prompt.pos(void 0, true)); + const char = this.text[this.#prompt.textPos()]; + if (char !== void 0 && char !== "") { + const pic = this.typeface.glyphs[char]; + if (pic) + $.ink(this.pal.highlight).draw(pic, prompt.pos(void 0, true)); + } + } + } + } + if (this.cursor === "stop" && !this.canType) { + const pos = prompt.pos(); + const pulse = 0.5 + 0.5 * Math.sin($.paintCount / 8); + const alpha = Math.floor(110 + pulse * 145); + $.ink(100, 180, 255, alpha).box(pos.x + 1, pos.y + 3, 3); + } + let btnScheme, btnHvrScheme, btnRolloverScheme; + const pal = this.pal; + if (pal.btn && pal.btnTxt) + btnScheme = [pal.btn, pal.btnTxt, pal.btnTxt, pal.btn]; + if (pal.btnHvr && pal.btnHvrTxt) + btnHvrScheme = [pal.btnHvr, pal.btnHvrTxt, pal.btnHvrTxt, pal.btnHvr]; + if (pal.btnRollover && pal.btnRolloverTxt) + btnRolloverScheme = [pal.btnRollover, pal.btnRolloverTxt, pal.btnRolloverTxt, pal.btnRollover]; + if (!this.enter.btn.disabled && !this.poe) { + if (this.#activatingPress) { + const color3 = pal.focusOutline || (Array.isArray(pal.text) ? [...pal.text.slice(0, 3), 128] : [255, 0, 200, 64]); + $.ink(color3).box(0, 0, frame.width, frame.height, "inline"); + } + } + if (frame.x || frame.y) $.unpan(); + if (!this.enter.btn.disabled && !this.poe) { + this.enter.reposition({ right: 6, bottom: 6, screen: frame }); + $.layer(2); + this.enter.paint($, btnScheme, btnHvrScheme, void 0, btnRolloverScheme); + $.layer(1); + } + if (!this.copy.btn.disabled && !this.poe) { + this.copy.reposition({ left: 6, bottom: 6, screen: frame }); + this.copy.btn.publishToDom($, "copy", this.#coatedCopy); + $.layer(2); + this.copy.paint( + { ink: $.ink }, + this.#copyPasteScheme || btnScheme, + btnHvrScheme, + void 0, + btnRolloverScheme + ); + $.layer(1); + } + if (!this.paste.btn.disabled && !this.poe) { + this.paste.reposition({ left: 6, bottom: 6, screen: frame }); + this.paste.btn.publishToDom($, "paste"); + $.layer(2); + this.paste.paint( + { ink: $.ink }, + this.#copyPasteScheme || btnScheme, + btnHvrScheme, + void 0, + btnRolloverScheme + ); + $.layer(1); + } + return !(keys2(this.typeface.glyphs).length === keys2(this.typeface.glyphData).length); + } + // Simulate anything necessary. + sim({ seconds, needsPaint, gizmo: { Hourglass: Hourglass2 } }) { + this.blink = this.blink || new Hourglass2(seconds(0.75), { + flipped: (count, showBlinkOverride) => { + if (showBlinkOverride !== void 0) + this.showBlink = showBlinkOverride; + else this.showBlink = !this.showBlink; + needsPaint(); + }, + autoFlip: true + }); + if (this.#lock) needsPaint(); + if (this.canType) this.blink.step(); + if (this._needsRepaint) { + this._needsRepaint = false; + needsPaint(); + } + } + // Helper method to ensure blink is initialized before use + #ensureBlink() { + if (!this.blink) { + const Hourglass2 = this.$.gizmo?.Hourglass; + if (Hourglass2) { + this.blink = new Hourglass2(45, { + // 0.75 seconds at 60fps + flipped: (count, showBlinkOverride) => { + if (showBlinkOverride !== void 0) + this.showBlink = showBlinkOverride; + else this.showBlink = !this.showBlink; + this.$.needsPaint?.(); + }, + autoFlip: true + }); + } + } + } + showButton($, { nocopy, nopaste } = { nocopy: false, nopaste: false }) { + this.enter.btn.disabled = false; + if (!nocopy && this.text.length > 0) { + this.#coatedCopy = this.#coatCopy(this.text); + this.copy.btn.disabled = false; + this.paste.btn.disabled = true; + this.paste.btn.removeFromDom($, "paste"); + } else if (nopaste) { + this.paste.btn.disabled = true; + this.paste.btn.removeFromDom($, "paste"); + } else { + this.paste.btn.disabled = false; + } + } + // Run a command. + async #execute(store2) { + if (!this.noHistory) { + store2[this.key] = store2[this.key] || []; + if (store2[this.key][0] !== this.text && !this.text.startsWith("prompt~")) { + store2[this.key].unshift(this.text); + } + } + if (!this.noHistory) store2.persist(this.key); + await this.#processCommand?.(this.text); + this.commandSentOnce = true; + } + // Clear the TextInput object and flip the cursor to ON. + blank(cursor2) { + if (cursor2) this.cursor = cursor2; + this.text = ""; + this.#prompt.cursor = { x: 0, y: 0 }; + this.blink?.flip(true); + this.$.send({ type: "keyboard:text:replace", content: { text: "" } }); + } + // Set the UI state to be that of a completed reply. + replied($) { + this.runnable = false; + this.canType = false; + this.clearUserText(); + this.showButton($); + } + #buildCopyPasteScheme() { + let scheme = [64, 127, 127, 64]; + if (this.pal.btnReply && this.pal.btnReplyTxt) + scheme = [ + this.pal.btnReply, + this.pal.btnReplyTxt, + this.pal.btnReplyTxt, + this.pal.btnReply + ]; + return scheme; + } + // 🎞️ Rolodex history scrubbing. Drives the SAME state as the ArrowUp/ + // ArrowDown handlers, but continuously, for a touch/drag gesture. The + // stack is snapshotted at gesture start so a drag doesn't hammer the + // store; depth -1 == the un-submitted draft (#prehistory); on release + // the input simply stays wherever it landed (the rolodex "snap"). + // These are additive — existing arrow-key history is untouched. + async beginHistoryScrub() { + const store2 = this.$?.store; + this.#scrubCache = !this.noHistory && store2 && await store2.retrieve(this.key) || [""]; + if (this.#prehistory === void 0) this.#prehistory = this.text; + return this.#scrubCache.length; + } + scrubHistoryTo(depth) { + if (!this.#scrubCache) return; + const len5 = this.#scrubCache.length; + let d2 = Math.round(depth); + if (d2 < -1) d2 = -1; + if (d2 > len5 - 1) d2 = len5 - 1; + this.historyDepth = d2; + this.text = d2 === -1 ? this.#prehistory ?? "" : this.#scrubCache[d2] || ""; + this.snap(); + this.$?.send?.({ + type: "keyboard:text:replace", + content: { text: this.text } + }); + this.selection = null; + } + endHistoryScrub() { + this.#scrubCache = null; + } + isHistoryScrubbing() { + return this.#scrubCache !== null; + } + // Handle user input. + async act($) { + const { debug: debug4, event: e2, store: store2, needsPaint, sound: sound2 } = $; + if (e2.is("ui:cancel-interactions")) { + this.#edgeCancelled = true; + this.#activatingPress = false; + this.backdropTouchOff = false; + this.enter.btn.act(e2); + this.copy.btn.act(e2); + this.paste.btn.act(e2); + needsPaint(); + return; + } + if (e2.is("reframed")) { + if (!$.store["gutter:lock"]) this.fullGutter($); + this.flow(); + needsPaint(); + } + if (e2.is("keyboard:down") && this.#lock === false && !this.enter.btn.down) { + if (e2.key.length === 1 && e2.ctrl === false && e2.key !== "`") { + this.#edgeCancelled = false; + } + if (e2.key.length === 1 && e2.ctrl === false && e2.key !== "`") { + if (!this.canType && !this.#edgeCancelled) { + this.#manuallyActivated = true; + this.#manualActivationTime = Date.now(); + activate(this); + } + let insert = e2.key.replace(/[“”]/g, '"').replace(/[‘’]/g, "'"); + let index = this.#prompt.textPos(); + const char = this.text[index]; + const newLine = char?.charCodeAt(0) === 10; + const underCursor = index !== void 0; + if (index === void 0 && this.#prompt.cursor.x === 0 && this.#prompt.cursor.y === 0) { + index = 0; + this.text = insert + this.text; + this.#prompt.forward(this.#prompt.cursor, insert.length); + this.#ensureBlink(); + this.blink?.flip(true); + this.showBlink = true; + return; + } + if (newLine && underCursor) { + this.text = this.text.slice(0, index + 1) + insert + this.text.slice(index + 1); + this.#prompt.forward(); + this.#ensureBlink(); + this.blink?.flip(true); + this.showBlink = true; + return; + } + while (index === void 0) { + index = this.#prompt.textPos( + this.#prompt.backward({ ...this.#prompt.cursor }) + ); + } + const sliceIndex = underCursor ? index : index + 1; + if (this.#prompt.wrapped(sliceIndex) && insert === " ") { + const lastCursor = this.#prompt.textToCursorMap[sliceIndex - 1]; + const thisCursor = this.#prompt.cursor; + let spaces = 0; + spaces = this.#prompt.colWidth - lastCursor.x + thisCursor.x; + insert = " ".repeat(spaces - 1); + } + this.text = this.text.slice(0, sliceIndex) + insert + this.text.slice(sliceIndex); + if (!underCursor || index === 0) { + let skipForward = false; + const newIndex = this.#prompt.textPos(); + const mapped = this.#prompt.textToCursorMap[newIndex]; + if (mapped) { + this.#prompt.cursor = { ...mapped }; + } else { + skipForward = true; + } + if (newIndex <= index && index > 0) { + this.#prompt.forward(this.#prompt.cursor, index - newIndex + 2); + } else if (!skipForward) this.#prompt.forward(); + } else { + let newCursor = this.#prompt.textToCursorMap[sliceIndex + insert.length]; + if (!newCursor) + newCursor = this.#prompt.textToCursorMap[sliceIndex + insert.length + 1]; + if (newCursor) this.#prompt.cursor = { ...newCursor }; + } + } else { + if (e2.key === "Backspace") { + const prompt = this.#prompt; + const currentTextIndex = prompt.textPos(); + const onNewline = this.text[currentTextIndex]?.charCodeAt(0) === 10; + if (onNewline) { + this.text = this.text.slice(0, currentTextIndex) + this.text.slice(currentTextIndex + 1); + if (this.text.length === currentTextIndex) { + this.snap(); + } else { + prompt.crawlBackward(); + if (prompt.posHasVisibleCharacter()) prompt.forward(); + } + } else { + const back = prompt.backward({ ...prompt.cursor }); + const key = `${back.x}:${back.y}`; + const cursorTextIndex = prompt.cursorToTextMap[key]; + const hasNewLine = prompt.cursorToTextMap[key + ":\\n"] !== void 0; + const currentPosition = prompt.textPos(); + if (prompt.wrapped(currentPosition)) { + let movablePosition = prompt.textPos(); + let char = this.text[movablePosition - 1]; + let len5 = 0; + while (char === " ") { + movablePosition -= 1; + char = this.text[movablePosition - 1]; + len5 += 1; + } + if (len5 > 0) { + this.text = this.text.slice(0, currentPosition - len5) + this.text.slice(currentPosition); + prompt.cursor = { + ...prompt.textToCursorMap[currentPosition - len5] + }; + } + this.#ensureBlink(); + this.blink?.flip(true); + this.showBlink = true; + return; + } + if (currentTextIndex === 0) return; + if (cursorTextIndex === void 0 && currentTextIndex > 0) { + this.text = this.text.slice(0, currentTextIndex - 1) + this.text.slice(currentTextIndex); + prompt.cursor = { + ...prompt.textToCursorMap[currentTextIndex - 1] + }; + } + if (cursorTextIndex >= 0) { + this.text = this.text.slice(0, cursorTextIndex) + this.text.slice(cursorTextIndex + 1); + let cursor2 = prompt.textToCursorMap[cursorTextIndex - 1]; + if (!cursor2) { + if (prompt.posHasVisibleCharacter()) { + cursor2 = prompt.textToCursorMap[cursorTextIndex]; + } else { + cursor2 = { x: 0, y: 0 }; + } + } + if (cursor2) { + prompt.cursor = { ...cursor2 }; + if (cursorTextIndex > 0 && !hasNewLine) { + if (!prompt.wrapped(cursorTextIndex)) prompt.forward(); + } + } else { + prompt.crawlBackward(); + } + } + } + } + if (e2.key === "Escape") { + this.activate(this); + $.send({ type: "keyboard:open" }); + this.text = ""; + $.send({ type: "keyboard:text:replace", content: { text: "" } }); + this.#prompt.cursor = { x: 0, y: 0 }; + } + if (e2.key === "ArrowUp" && !this.skipHistory && !this.noHistory) { + const history = await store2.retrieve(this.key) || [""]; + if (this.#prehistory === void 0) this.#prehistory = this.text; + this.historyDepth += 1; + if (this.historyDepth === history.length) { + this.historyDepth = -1; + } + if (this.historyDepth === -1) { + this.text = this.#prehistory; + } else { + this.text = history[this.historyDepth] || ""; + } + this.snap(); + $.send({ + type: "keyboard:text:replace", + content: { text: this.text } + }); + this.selection = null; + } + if (e2.key === "ArrowDown" && !this.skipHistory && !this.noHistory) { + const history = await store2.retrieve(this.key) || [""]; + if (this.#prehistory === void 0) this.#prehistory = this.text; + this.historyDepth -= 1; + if (this.historyDepth < -1) this.historyDepth = history.length - 1; + if (this.historyDepth === -1) { + this.text = this.#prehistory; + } else { + this.text = history[this.historyDepth] || ""; + } + this.snap(); + $.send({ + type: "keyboard:text:replace", + content: { text: this.text } + }); + this.selection = null; + } + } + if (e2.key !== "Enter" && e2.key !== "`") { + if (this.canType) { + if (this.text.length > 0) { + this.enter.btn.disabled = false; + this.runnable = true; + } else { + this.enter.btn.disabled = true; + this.runnable = false; + } + } + } + if (e2.key === "Enter" && !this.skipEnter) { + if (e2.shift) { + const pos = this.#prompt.textPos(); + const char = this.text[pos]; + if (pos === void 0 || char?.charCodeAt(0) === 10 && pos === this.text.length - 1) { + this.text += ` +`; + this.#prompt.newLine(); + this.snap(); + } else { + const hasVis = this.#prompt.posHasVisibleCharacter(); + let insert = "\n"; + let wrapped = false; + if (this.#prompt.wrapped(pos)) { + wrapped = true; + insert += "\n"; + } + this.text = this.text.slice(0, pos) + insert + this.text.slice(pos); + if (hasVis && !wrapped) { + this.#prompt.cursor = { ...this.#prompt.textToCursorMap[pos] }; + } else { + this.#prompt.cursor.y += 1; + } + } + $.send({ + type: "keyboard:text:replace", + content: { + text: this.text + /*cursor: this.#prompt.textPos()*/ + } + }); + } else if (this.runnable) { + if (this.text.trim().length > 0) { + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 850, + attack: 0.1, + decay: 0.96, + volume: 0.65, + duration: 5e-3 + }); + } + await this.run(store2); + deactivate(this); + } + } else if (!this.canType && !this.#edgeCancelled) { + activate(this); + } + } + this.#ensureBlink(); + this.blink?.flip(true); + this.showBlink = true; + } + if (e2.is("keyboard:open") && !this.#lock && !this.#edgeCancelled) { + const timeSinceManualDeactivation = Date.now() - this.#manualDeactivationTime; + const timeSinceManualActivation = Date.now() - this.#manualActivationTime; + if (!this.#manuallyDeactivated || this.#activatingPress || timeSinceManualDeactivation > 100) { + activate(this); + } + } + if (e2.is("keyboard:close") && !this.#lock) { + if (!this.canType) { + return; + } + if (this.#preventDeactivation || this._preventDeactivation) { + return; + } + const timeSinceManualDeactivation = Date.now() - this.#manualDeactivationTime; + const timeSinceManualActivation = Date.now() - this.#manualActivationTime; + if (this._enterButtonActivation) { + return; + } + if ((!this.#manuallyDeactivated || timeSinceManualDeactivation > 100) && (!this.#manuallyActivated || timeSinceManualActivation > 500)) { + deactivate(this); + } + } + if (e2.is("touch") && !this.#lock && //!this.inputStarted && + !this.canType && !this.backdropTouchOff && (this.copy.btn.disabled === true || !this.copy.btn.box.contains(e2)) && (this.paste.btn.disabled === true || !this.paste.btn.box.contains(e2))) { + this.#activatingPress = true; + this.#edgeCancelled = false; + $.send({ type: "keyboard:unlock" }); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 300, + attack: 0.1, + decay: 0.96, + volume: 0.5, + duration: 0.01 + }); + } + } + if (e2.is("touch") && !this.#lock && this.canType && !this.backdropTouchOff && (this.copy.btn.disabled === true || !this.copy.btn.box.contains(e2)) && (this.paste.btn.disabled === true || !this.paste.btn.box.contains(e2)) && (this.enter.btn.disabled === true || !this.enter.btn.box.contains(e2))) { + this.#deactivatingPress = true; + } + function activate(ti) { + ti.activatedOnce = true; + if (ti.canType) { + return; + } + ti.activated?.($, true); + ti.#activatingPress = false; + if (ti.text.length > 0) { + ti.copy.btn.disabled = true; + ti.copy.btn.removeFromDom($, "copy"); + } + ti.canType = true; + if (ti.#lastUserText.length > 0) { + ti.text = ti.#lastUserText; + ti.runnable = true; + ti.paste.btn.disabled = false; + } else { + if (ti.#lastPrintedText) { + ti.blank("blink"); + } + ti.runnable = false; + ti.paste.btn.disabled = false; + } + $.act("text-input:editable"); + $.send({ type: "keyboard:unlock" }); + $.send({ type: "keyboard:open" }); + if (!ti.mute) { + sound2.synth({ + type: "sine", + tone: 300, + attack: 0.1, + decay: 0.96, + volume: 0.5, + duration: 0.01 + }); + } + } + this.activate = activate; + function deactivate(ti) { + if (ti.#preventDeactivation || ti._preventDeactivation) { + return; + } + if (ti.canType === false) { + return; + } + ti.activated?.($, false); + ti.enter.btn.disabled = false; + ti.paste.btn.disabled = false; + ti.canType = false; + ti.runnable = false; + ti.#lastUserText = ti.text; + ti.backdropTouchOff = false; + ti.text = ti.#lastPrintedText || ti.text; + if (ti.#lastPrintedText.length > 0 && ti.commandSentOnce) { + ti.copy.btn.disabled = false; + ti.#coatedCopy = ti.#coatCopy(ti.text); + ti.paste.btn.disabled = true; + ti.paste.btn.removeFromDom($, "paste"); + } + $.act("text-input:uneditable"); + needsPaint(); + if (!ti.mute) { + sound2.synth({ + type: "sine", + tone: 250, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 1e-3 + }); + } + ti.mute = false; + } + if (e2.is("touch") && (this.enter.btn.disabled === false && this.enter.btn.box.contains(e2) || this.copy.btn.disabled === false && this.copy.btn.box.contains(e2) || this.paste.btn.disabled === false && this.paste.btn.box.contains(e2))) { + this.backdropTouchOff = true; + } + if (e2.is("lift")) { + if (this.shifting) { + this.#moveDeltaX = 0; + this.shifting = false; + this.#recentlyShifting = true; + $.send({ type: "keyboard:unlock" }); + setTimeout(() => { + this.#recentlyShifting = false; + }, 50); + } + const shouldPreventActivation = this.backdropTouchOff; + this.backdropTouchOff = false; + if (!this.#lock && !shouldPreventActivation) { + $.send({ type: "keyboard:unlock" }); + } + if (!this.canType) { + if (this.#activatingPress) { + if (!this.#edgeCancelled && !shouldPreventActivation) { + this.#manuallyActivated = true; + this.#manualActivationTime = Date.now(); + activate(this); + } + this.#activatingPress = false; + } + } else if (!shouldPreventActivation) { + const isOverEnterButton = this.enter.btn.disabled === false && this.enter.btn.box.contains(e2); + const enterButtonIsDown = this.enter.btn.down; + const isOverInteractive = this.copy.btn.disabled === false && this.copy.btn.box.contains(e2) || this.paste.btn.disabled === false && this.paste.btn.box.contains(e2) || isOverEnterButton; + if (!isOverInteractive || isOverEnterButton && enterButtonIsDown) { + if (!isOverInteractive && !this.#recentlyShifting) { + this.#manuallyDeactivated = true; + this.#manualDeactivationTime = Date.now(); + if (this.#deactivatingPress) { + $.send({ type: "keyboard:lock" }); + $.send({ type: "keyboard:close" }); + } + deactivate(this); + } + } + } + this.#deactivatingPress = false; + } + if (!this.#lock) { + if (e2.is("draw") && this.enter.btn.disabled === false && this.enter.btn.box.contains(e2) && !this.enter.btn.down) { + $.send({ type: "keyboard:lock" }); + } + if ((e2.is("draw") || e2.is("touch")) && this.copy.btn.disabled === false && this.copy.btn.box.contains(e2)) { + $.send({ type: "keyboard:lock" }); + } + if ((e2.is("draw") || e2.is("touch")) && this.paste.btn.disabled === false && this.paste.btn.box.contains(e2)) { + $.send({ type: "keyboard:lock" }); + } + this.enter.btn.act(e2, { + down: () => { + $.send({ type: "keyboard:unlock" }); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 600, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 1e-3 + }); + } + needsPaint(); + }, + scrub: () => { + }, + push: async () => { + if (this.#lock) { + return; + } + if (!this.canType && this.text.trim().length > 0) { + this.#manuallyActivated = true; + this.#manualActivationTime = Date.now(); + activate(this); + if (this.runnable && this.text.trim().length > 0) { + await this.run(store2); + deactivate(this); + } + } else if (this.runnable && this.text.trim().length > 0) { + this.#manuallyActivated = true; + this.#manualActivationTime = Date.now(); + await this.run(store2); + this._enterHandledMessage = true; + deactivate(this); + setTimeout(() => { + this._enterHandledMessage = false; + }, 200); + } else if (this.runnable && this.text.trim().length === 0 && this.closeOnEmptyEnter) { + deactivate(this); + } else { + this.#edgeCancelled = false; + this.#manuallyActivated = true; + this.#manualActivationTime = Date.now(); + activate(this); + } + }, + cancel: () => { + $.send({ type: "keyboard:lock" }); + needsPaint(); + }, + rollover: (btn) => { + if (btn) $.send({ type: "keyboard:unlock" }); + needsPaint(); + }, + rollout: () => { + $.send({ type: "keyboard:lock" }); + needsPaint(); + } + }); + this.copy.btn.act(e2, { + down: () => { + needsPaint(); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 600, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 1e-3 + }); + } + }, + push: () => { + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 800, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 5e-3 + }); + } + needsPaint(); + }, + cancel: () => needsPaint() + }); + this.paste.btn.act(e2, { + down: () => { + needsPaint(); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 600, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 1e-3 + }); + } + }, + push: () => { + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 800, + attack: 0.1, + decay: 0.99, + volume: 0.75, + duration: 5e-3 + }); + } + needsPaint(); + }, + cancel: () => needsPaint() + }); + } + if (e2.name?.startsWith("clipboard:copy")) { + const copied = e2.is("clipboard:copy:copied"); + if (debug4) { + copied ? "\u{1F4CB} Copy: Copied \u{1F643}" : console.warn("\u{1F4CB} Copy: Failed \u26A0\uFE0F"); + } + this.copy.txt = copied ? this.scheme.buttons?.copy?.copied || "Copied" : this.scheme.buttons?.copy?.failed || "Failed"; + this.#copyPasteScheme = this.#buildCopyPasteScheme(); + needsPaint(); + clearTimeout(this.#copyPasteTimeout); + this.#copyPasteTimeout = setTimeout(() => { + this.copy.btn.disabled = false; + this.copy.txt = this.scheme.buttons?.copy?.label || "Copy"; + this.#copyPasteScheme = void 0; + needsPaint(); + }, 500); + } + if (e2.name?.startsWith("clipboard:paste")) { + let label; + if (e2.is("clipboard:paste:pasted")) { + if (debug4) "\u{1F4CB} Paste: Pasted \u{1F643}"; + label = this.scheme.buttons?.paste?.pasted || "Pasted"; + } else if (e2.is("clipboard:paste:pasted:empty")) { + if (debug4) console.warn("\u{1F4CB} Paste: Empty \u{1F450}\uFE0F"); + label = this.scheme.buttons?.paste?.empty || "Empty"; + } else { + if (debug4) console.warn("\u{1F4CB} Paste: Failed \u26A0\uFE0F"); + label = this.scheme.buttons?.paste?.failed || "Failed"; + } + this.paste.txt = label; + this.#copyPasteScheme = this.#buildCopyPasteScheme(); + needsPaint(); + clearTimeout(this.#copyPasteTimeout); + this.#copyPasteTimeout = setTimeout(() => { + this.paste.btn.disabled = false; + this.paste.txt = this.scheme.buttons?.paste?.label || "Paste"; + this.#copyPasteScheme = void 0; + needsPaint(); + }, 500); + } + if (e2.is("prompt:text:replace") && (!this.activatedOnce || this.#lock === false)) { + this.text = e2.text; + this.#lastUserText = e2.text; + this.#prompt.snapTo(this.text.slice(0, e2.cursor)); + this.#ensureBlink(); + this.blink?.flip(true); + this.selection = null; + if (this.text.length > 0) { + this.enter.btn.disabled = false; + this.runnable = true; + } else { + this.enter.btn.disabled = true; + this.runnable = false; + } + if (this.#prehistory !== void 0) this.#prehistory = this.text; + } + if (e2.is("prompt:text:cursor") && this.#lock === false) { + if (e2.cursor === this.text.length) { + this.#prompt.snapTo(this.text); + } else if (this.text[e2.cursor]?.charCodeAt(0) === 10) { + if (e2.cursor > 0 && this.#prompt.textToCursorMap[e2.cursor - 1]) { + const prevPos = this.#prompt.textToCursorMap[e2.cursor - 1]; + this.#prompt.cursor = { ...prevPos }; + if (this.text[e2.cursor - 1]?.charCodeAt(0) !== 10) { + this.#prompt.forward(); + } + } else { + this.#prompt.cursor = { x: 0, y: 0 }; + } + } else { + this.#prompt.cursor = { ...this.#prompt.textToCursorMap[e2.cursor] }; + } + if (e2.start !== void 0 && e2.end !== void 0) { + this.selection = [e2.start, e2.end]; + } else { + this.selection = null; + } + this.#ensureBlink(); + this.blink?.flip(true); + } + if (e2.is("touch") && !this.#lock) { + this.#ensureBlink(); + this.blink?.flip(true); + this.#recentlyShifting = false; + } + if (e2.is("draw") && !this.#lock && this.canType && !this.enter.btn.down && !this.paste.btn.down) { + if (!this.shifting) { + $.send({ type: "keyboard:lock" }); + this.shifting = true; + this.backdropTouchOff = true; + this.#deactivatingPress = false; + } + if (this.#moveDeltaX > 0 && e2.delta.x < 0 || this.#moveDeltaX < 0 && e2.delta.x > 0) { + this.#moveDeltaX = 0; + } + this.#moveDeltaX += e2.delta.x; + while (this.#moveDeltaX <= -this.#moveThreshold) { + this.#moveDeltaX += this.#moveThreshold; + this.#prompt.crawlBackward(); + this.selection = null; + $.send({ type: "keyboard:cursor", content: -1 }); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 800, + attack: 0.01, + decay: 0.95, + volume: 0.25, + duration: 8e-3 + }); + } + } + while (this.#moveDeltaX >= this.#moveThreshold) { + this.#moveDeltaX -= this.#moveThreshold; + this.#prompt.crawlForward(); + this.selection = null; + if (this.prompt.textPos() === void 0) { + $.act("textinput:shift-right:empty"); + } + $.send({ type: "keyboard:cursor", content: 1 }); + if (!this.mute) { + sound2.synth({ + type: "sine", + tone: 800, + attack: 0.01, + decay: 0.95, + volume: 0.25, + duration: 8e-3 + }); + } + } + this.#ensureBlink(); + this.blink?.flip(true); + } + } +}; +var Prompt = class { + top = 0; + left = 0; + wrap = "char"; + // auto-wrap setting, could also be "word". + scale = 1; + letterWidth; + // Taken from the typeface's block sizing. + letterHeight; + typeface; + // Reference to the typeface for proportional spacing + colWidth = 48; + // Maximum character width of each line before wrapping. + cursor = { x: 0, y: 0 }; + gutter; + // A y-position at the end of the colWidth. + lineBreaks = []; + // Legacy? + cursorToTextMap = {}; + // Keep track of text data in relationship to whitespace. + textToCursorMap = []; + wrappedWordIndices = []; + // Keep track of word wrapped indices after + // each mapping. + #mappedTo = ""; + // Text that has been mapped. + constructor(top = 0, left = 0, wrap2, colWidth = 48, lineSpacing = 0, typeface) { + this.typeface = typeface; + this.letterWidth = typeface.blockWidth * this.scale; + this.letterHeight = typeface.blockHeight * this.scale + lineSpacing; + this.top = top; + this.left = left; + this.wrap = wrap2; + this.colWidth = colWidth; + this.gutter = this.colWidth * this.letterWidth; + } + // Snap the cursor to the end of a text. + snapTo(text) { + if (text[text.length - 1]) { + this.cursor = { ...this.textToCursorMap[text.length - 1] }; + if (text[text.length - 1].charCodeAt(0) !== 10) this.forward(); + } else { + this.cursor = { x: 0, y: 0 }; + } + } + // Generate text map for rendering and UI operations. + mapTo(text) { + this.#mappedTo = text; + this.cursorToTextMap = {}; + this.textToCursorMap = []; + this.wrappedWordIndices = []; + const cursor2 = { x: 0, y: 0 }; + if (this.wrap === "char") { + let textIndex = 0; + let brokeLine = false; + const characters = Array.from(text); + for (let c4 = 0; c4 < characters.length; c4 += 1) { + const char = characters[c4]; + const newLine = char.charCodeAt(0) === 10; + if (c4 === 0) { + if (newLine) { + this.newLine(cursor2); + brokeLine = true; + } + this.#updateMaps(text, textIndex, cursor2); + continue; + } + if (newLine) { + this.newLine(cursor2); + brokeLine = true; + } else { + !brokeLine ? this.forward(cursor2) : brokeLine = false; + } + textIndex += 1; + this.#updateMaps(text, textIndex, cursor2); + } + } else if (this.wrap === "word") { + let textIndex = 0; + let brokeLine = false; + let wordStart = false; + let wordCount = 0; + const characters = Array.from(text); + for (let c4 = 0; c4 < characters.length; c4 += 1) { + const char = characters[c4]; + let newLine = char.charCodeAt(0) === 10; + if (c4 === 0) { + if (newLine) { + this.newLine(cursor2); + brokeLine = true; + } + this.#updateMaps(text, textIndex, cursor2); + if (!newLine && char !== " ") { + wordStart = true; + wordCount += 1; + } + continue; + } + if (!newLine && char !== " ") { + if (!wordStart) { + wordStart = true; + wordCount += 1; + let len5 = 0; + for (let i2 = c4; i2 < characters.length; i2 += 1) { + const char2 = characters[i2]; + if (char2 !== " " && char2.charCodeAt(0) !== 10) { + len5 += 1; + } else { + break; + } + } + if (cursor2.x + len5 >= this.colWidth - 1) { + if (!this.posHasNewLine(cursor2)) { + this.newLine(cursor2); + brokeLine = true; + this.wrappedWordIndices.push(c4); + } + } + } + } else { + wordStart = false; + } + if (char === " " && cursor2.x + 1 === this.colWidth - 1 && characters[textIndex] !== " ") { + newLine = true; + } + if (newLine) { + this.newLine(cursor2); + brokeLine = true; + } else { + !brokeLine ? this.forward(cursor2) : brokeLine = false; + } + textIndex += 1; + this.#updateMaps(text, textIndex, cursor2); + } + } + } + // Lookup to check if the word at the beginning of this index was + // word-wrapped. + wrapped(index) { + if (this.wrap !== "word") return false; + return this.wrappedWordIndices.includes(index); + } + #updateMaps(text, textIndex, cursor2 = this.cursor) { + const char = text[textIndex]; + const newLine = char.charCodeAt(0) === 10; + this.textToCursorMap[textIndex] = { ...cursor2 }; + let key = `${cursor2.x}:${cursor2.y}`; + if (newLine) key = key + ":\\n"; + this.cursorToTextMap[key] = textIndex; + } + // Get the current text index given a cursor position. + textPos(cursor2 = this.cursor) { + if (this.textToCursorMap.length === 0) { + return 0; + } else { + const key = `${cursor2.x}:${cursor2.y}`; + let pos = this.cursorToTextMap[key]; + if (pos === void 0) pos = this.cursorToTextMap[key + ":\\n"]; + return pos; + } + } + // Determine whether a cursor has a visible character in the map. + posHasVisibleCharacter(cursor2 = this.cursor) { + return this.cursorToTextMap[`${cursor2.x}:${cursor2.y}`] !== void 0; + } + // Determine whether there is an invisible new line character + // under the cursor in the map. + posHasNewLine(cursor2 = this.cursor) { + return this.cursorToTextMap[`${cursor2.x}:${cursor2.y}:\\n`] !== void 0; + } + // Flatten the coordinates of the cursor to return a linear value. + // (Does not necessarily match text, due to line breaks, etc.) + get index() { + const x = this.cursor.x; + const y = this.cursor.y; + const cols = this.colWidth; + const lineBreaks = y; + return y * (cols + 1) + x - lineBreaks; + } + // Caluclate the screen x, y position of the top left of the cursor. + // (Also include the width and height of the block.) + pos(cursor2 = this.cursor, bh = false) { + const y = this.left + cursor2.y * this.letterHeight; + const isProportional = this.typeface?.data?.proportional === true || this.typeface?.data?.bdfFont === "MatrixChunky8" || this.typeface?.name === "MatrixChunky8" || !!this.typeface?.data?.advances; + let x; + let w; + if (isProportional && this.#mappedTo && typeof this.typeface?.getAdvance === "function") { + x = this.top; + let charsOnLine = 0; + for (const [key, textIdx] of Object.entries(this.cursorToTextMap)) { + const [kx, ky] = key.split(":").map((v2) => parseInt(v2)); + if (ky === cursor2.y && kx < cursor2.x && !key.includes("\\n")) { + charsOnLine++; + } + } + for (let i2 = 0; i2 < this.#mappedTo.length; i2++) { + const mappedPos = this.textToCursorMap[i2]; + if (mappedPos && mappedPos.y === cursor2.y && mappedPos.x < cursor2.x) { + const char = this.#mappedTo[i2]; + if (char && char.charCodeAt(0) !== 10) { + const advance = this.typeface.getAdvance(char); + x += (typeof advance === "number" ? advance : this.letterWidth) * this.scale; + } + } + } + const cursorTextIdx = this.textPos(cursor2); + if (cursorTextIdx !== void 0 && this.#mappedTo[cursorTextIdx]) { + const curChar = this.#mappedTo[cursorTextIdx]; + const advance = this.typeface.getAdvance(curChar); + w = (typeof advance === "number" ? advance : this.letterWidth) * this.scale; + } else { + w = this.letterWidth; + } + } else { + x = this.top + cursor2.x * this.letterWidth; + w = this.letterWidth; + } + return { + x, + y, + w, + h: this.letterHeight + }; + } + // Move the cursor forward, optionally input an override cursor. + forward(cursor2 = this.cursor, amount = 1) { + repeat(amount, () => { + cursor2.x = (cursor2.x + 1) % (this.colWidth - 1); + if (cursor2.x === 0) cursor2.y += 1; + }); + return cursor2; + } + // Move the cursor forward only by the mapped text. + crawlForward() { + if (this.#mappedTo.length === 0) return; + const back = this.backward({ ...this.cursor }); + const backIndex = this.textPos(back); + const startIndex = this.textPos(); + if (backIndex === this.#mappedTo.length || startIndex === this.#mappedTo.length - 1 && !this.posHasVisibleCharacter()) { + return; + } + if (backIndex !== this.#mappedTo.length - 1) { + if (this.#mappedTo[startIndex + 1]?.charCodeAt(0) === 10 && this.posHasVisibleCharacter()) { + this.cursor.x += 1; + } else { + this.forward(); + if (startIndex !== this.#mappedTo.length - 1) { + while (this.textPos() === void 0) { + this.forward(); + } + } + } + } else if (startIndex === 0) this.forward(); + } + // Move the cursor backward only by the mapped text. + crawlBackward() { + const back = this.backward({ ...this.cursor }); + let backIndex = this.textPos(back); + const currentIndex = this.textPos(); + if (backIndex === void 0) { + if (this.posHasNewLine() && currentIndex <= 1 && this.#mappedTo[currentIndex].charCodeAt(0) === 10) { + this.cursor.y -= 1; + return; + } else { + if (this.posHasNewLine()) { + const backupAmount = this.posHasVisibleCharacter() ? 2 : 1; + this.cursor = { + ...this.textToCursorMap[currentIndex - backupAmount] + }; + if (this.posHasVisibleCharacter()) this.forward(); + return; + } else { + while (backIndex === void 0) { + this.backward(); + backIndex = this.textPos(this.backward(back)); + } + } + } + } + this.backward(); + } + // Move cursor backward, with optional override cursor. + backward(cursor2 = this.cursor) { + if (cursor2.x === 0) { + if (cursor2.y > 0) { + cursor2.x = this.colWidth - 2; + cursor2.y -= 1; + } + } else { + cursor2.x -= 1; + } + return cursor2; + } + // Create and track a cursor line break. + newLine(cursor2 = this.cursor) { + cursor2.y += 1; + cursor2.x = 0; + } +}; +var TextFields = class { + values = {}; + focused = 0; + specs; + input; + rows = []; + // hit boxes, rebuilt every paint + #submit; + #pending = null; + // a value waiting for the keyboard to be ready for it + constructor($, specs, submit, options = {}) { + this.specs = specs.map((spec) => ({ lines: 1, ...spec })); + this.specs.forEach((spec) => this.values[spec.name] = ""); + this.#submit = submit; + this.input = new TextInput($, "", () => this.advance($), { + ...options, + poe: true, + history: false, + closeOnEmptyEnter: false + }); + } + get current() { + return this.specs[this.focused]; + } + // Pull the live buffer back into the focused field. + sync() { + this.values[this.current.name] = this.input.text; + } + focus(index, $) { + this.sync(); + this.focused = Math.max(0, Math.min(this.specs.length - 1, index)); + this.#pending = this.values[this.current.name] ?? ""; + $.send({ type: "keyboard:open" }); + } + // Enter walks down the stack, and sends from the last field. + advance($) { + this.sync(); + if (this.focused < this.specs.length - 1) return this.focus(this.focused + 1, $); + this.#submit({ ...this.values }); + } + reset($) { + this.specs.forEach((spec) => this.values[spec.name] = ""); + this.focused = 0; + this.input.text = ""; + this.#pending = ""; + $.send({ type: "keyboard:text:replace", content: { text: "" } }); + } + sim($) { + if (this.#pending !== null && this.input.canType) { + this.input.text = this.#pending; + $.send({ + type: "keyboard:text:replace", + content: { text: this.#pending } + }); + this.input.snap(); + this.#pending = null; + } + this.input.sim($); + } + #labelWidth($) { + const widest = this.specs.reduce( + (wide, spec) => Math.max(wide, $.text.box(spec.label, void 0, void 0, 1, false, "MatrixChunky8").box.width), + 0 + ); + return Math.min(widest + 6, Math.round($.screen.width * 0.34)); + } + // Single-line rows keep their height and the taller fields split whatever is + // left, so one call fits a phone and a desktop. + paint($, frame) { + const { ink: ink3 } = $; + const labelW = this.#labelWidth($); + const gap = 3; + const rowH = 14; + const grows = this.specs.filter((spec) => spec.lines > 1).length; + const singles = this.specs.length - grows; + const spare = frame.height - singles * (rowH + gap) - grows * gap; + const grown = Math.max(30, Math.floor(spare / Math.max(grows, 1))); + this.rows = []; + let y = frame.y; + this.specs.forEach((spec, i2) => { + const h = spec.lines > 1 ? grown : rowH; + const box2 = { x: frame.x, y, width: frame.width, height: h }; + this.rows.push(box2); + const on = i2 === this.focused; + ink3(on ? [120, 210, 255] : [92, 88, 110]).write( + spec.label, + { x: box2.x + 2, y: y + 4 }, + void 0, + void 0, + false, + "MatrixChunky8" + ); + const field = { + x: box2.x + labelW, + y, + width: box2.width - labelW, + height: h + }; + if (on) { + ink3(26, 34, 50).box(field.x, field.y, field.width, field.height); + this.input.paint($, false, field); + ink3(110, 180, 255).box( + field.x, + field.y, + field.width, + field.height, + "outline" + ); + ink3(120, 210, 255).box(field.x - 2, field.y + 1, 2, field.height - 2); + } else { + const value = this.values[spec.name]; + ink3(20, 17, 26).box(field.x, field.y, field.width, field.height); + ink3(46, 42, 58).box(field.x, field.y, field.width, field.height, "outline"); + ink3(value ? 185 : [74, 70, 90]).write( + value || spec.placeholder || "", + { x: field.x + 3, y: y + 4 }, + void 0, + field.width - 6 + ); + } + y += h + gap; + }); + } + // Returns true when the focus moved, so the caller can stop there. + act($) { + const e2 = $.event; + if (e2.is("keyboard:down:arrowup") && this.focused > 0) { + this.focus(this.focused - 1, $); + return true; + } + if (e2.is("keyboard:down:arrowdown") && this.focused < this.specs.length - 1) { + this.focus(this.focused + 1, $); + return true; + } + if (e2.is("touch")) { + const hit = this.rows.findIndex( + (b2) => e2.x >= b2.x && e2.x < b2.x + b2.width && e2.y >= b2.y && e2.y < b2.y + b2.height + ); + if (hit >= 0) { + if (hit !== this.focused) { + this.focus(hit, $); + return true; + } + } + } + this.input.act($); + return false; + } +}; + +// public/aesthetic.computer/lib/p5-worker.mjs +var p5LoadPromise = null; +var stubsInstalled = false; +var P5_URL = "/aesthetic.computer/dep/p5/p5.min.js"; +var CanvasShim = class _CanvasShim { + constructor(w = 100, h = 100) { + this._oc = new OffscreenCanvas(w, h); + this._ctx2d = this._oc.getContext("2d", { willReadFrequently: true, alpha: true }); + this.style = new Proxy({}, { get: () => "", set: () => true }); + this.dataset = {}; + this.attributes = []; + this.id = ""; + this.className = ""; + this.classList = { + add() { + }, + remove() { + }, + toggle() { + }, + contains() { + return false; + }, + replace() { + }, + item() { + return null; + }, + length: 0 + }; + this.tagName = "CANVAS"; + this.nodeName = "CANVAS"; + this.nodeType = 1; + this.children = []; + this.childNodes = []; + this.innerHTML = ""; + this.textContent = ""; + this.parentNode = null; + this.parentElement = null; + this.ownerDocument = null; + } + get width() { + return this._oc.width; + } + set width(v2) { + this._oc.width = v2; + } + get height() { + return this._oc.height; + } + set height(v2) { + this._oc.height = v2; + } + getContext(type, opts) { + if (type === "2d" && this._ctx2d) return this._ctx2d; + return this._oc.getContext(type, opts); + } + getBoundingClientRect() { + return { + x: 0, + y: 0, + left: 0, + top: 0, + right: this._oc.width, + bottom: this._oc.height, + width: this._oc.width, + height: this._oc.height + }; + } + toDataURL() { + return ""; + } + async toBlob(cb) { + cb && cb(await this._oc.convertToBlob()); + } + addEventListener() { + } + removeEventListener() { + } + setAttribute() { + } + getAttribute() { + return null; + } + removeAttribute() { + } + appendChild(c4) { + return c4; + } + removeChild(c4) { + return c4; + } + insertBefore(c4) { + return c4; + } + cloneNode() { + return new _CanvasShim(this._oc.width, this._oc.height); + } + contains() { + return false; + } + focus() { + } + blur() { + } + click() { + } + matches() { + return false; + } + closest() { + return null; + } + // Convenience for AC: get the underlying OffscreenCanvas + get offscreen() { + return this._oc; + } +}; +function makeElementStub(tag) { + const t2 = (tag || "div").toLowerCase(); + return { + tagName: t2.toUpperCase(), + nodeName: t2.toUpperCase(), + style: new Proxy({}, { get: () => "", set: () => true }), + children: [], + childNodes: [], + classList: { + add() { + }, + remove() { + }, + toggle() { + }, + contains() { + return false; + }, + replace() { + } + }, + dataset: {}, + attributes: [], + parentNode: null, + parentElement: null, + ownerDocument: null, + innerHTML: "", + textContent: "", + setAttribute() { + }, + getAttribute() { + return null; + }, + removeAttribute() { + }, + hasAttribute() { + return false; + }, + appendChild(c4) { + return c4; + }, + removeChild(c4) { + return c4; + }, + insertBefore(c4) { + return c4; + }, + replaceChild(c4) { + return c4; + }, + cloneNode() { + return makeElementStub(t2); + }, + addEventListener() { + }, + removeEventListener() { + }, + dispatchEvent() { + return true; + }, + getBoundingClientRect() { + return { x: 0, y: 0, left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }; + }, + contains() { + return false; + }, + focus() { + }, + blur() { + }, + click() { + }, + matches() { + return false; + }, + closest() { + return null; + }, + querySelector() { + return null; + }, + querySelectorAll() { + return []; + } + }; +} +function installDomStubs() { + if (stubsInstalled) return; + stubsInstalled = true; + if (typeof self.window === "undefined") self.window = self; + self.window.innerWidth = 256; + self.window.innerHeight = 256; + self.window.devicePixelRatio = 1; + self.window.scrollX = 0; + self.window.scrollY = 0; + self.window.pageXOffset = 0; + self.window.pageYOffset = 0; + if (!self.window.screen) self.window.screen = { width: 256, height: 256, availWidth: 256, availHeight: 256 }; + if (!self.window.navigator) { + self.window.navigator = self.navigator || { + userAgent: "aesthetic-computer", + platform: "AC", + language: "en-US", + languages: ["en-US"], + onLine: true + }; + } + if (!self.window.location) { + self.window.location = self.location || { href: "https://aesthetic.computer/", hostname: "aesthetic.computer", protocol: "https:" }; + } + if (typeof self.requestAnimationFrame !== "function") { + self.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 16); + self.cancelAnimationFrame = (id) => clearTimeout(id); + } + const fakeBody = makeElementStub("body"); + const fakeHead = makeElementStub("head"); + const fakeHtml = makeElementStub("html"); + self.__acP5CreatedCanvases = []; + self.document = { + readyState: "complete", + visibilityState: "visible", + hidden: false, + title: "aesthetic.computer", + body: fakeBody, + head: fakeHead, + documentElement: fakeHtml, + location: self.window.location, + cookie: "", + createElement(tag) { + if (typeof tag === "string" && tag.toLowerCase() === "canvas") { + const c4 = new CanvasShim(); + self.__acP5CreatedCanvases.push(c4); + return c4; + } + return makeElementStub(tag); + }, + createElementNS(_ns, tag) { + return this.createElement(tag); + }, + createTextNode(s2) { + return { nodeValue: String(s2), textContent: String(s2), nodeType: 3 }; + }, + getElementById() { + return null; + }, + getElementsByTagName(tag) { + const t2 = String(tag || "").toLowerCase(); + if (t2 === "head") return [fakeHead]; + if (t2 === "body" || t2 === "html") return [fakeBody]; + return [fakeBody]; + }, + getElementsByClassName() { + return [fakeBody]; + }, + querySelector() { + return fakeBody; + }, + querySelectorAll() { + return [fakeBody]; + }, + addEventListener() { + }, + removeEventListener() { + }, + dispatchEvent() { + return true; + }, + hasFocus() { + return true; + }, + exitFullscreen() { + return Promise.resolve(); + }, + contains() { + return false; + } + }; + self.HTMLCanvasElement = CanvasShim; + if (typeof self.HTMLElement === "undefined") self.HTMLElement = function HTMLElement() { + }; + if (typeof self.Node === "undefined") self.Node = function Node() { + }; + if (typeof self.Element === "undefined") self.Element = function Element() { + }; + if (typeof self.Image === "undefined") { + self.Image = class { + constructor() { + this.src = ""; + this.crossOrigin = ""; + this.complete = false; + } + addEventListener() { + } + removeEventListener() { + } + }; + } + if (typeof self.Audio === "undefined") { + self.Audio = class { + constructor() { + } + play() { + return Promise.resolve(); + } + pause() { + } + addEventListener() { + } + }; + } + if (typeof self.getComputedStyle === "undefined") { + self.getComputedStyle = () => new Proxy({}, { get: () => "" }); + } +} +async function ensureP5Loaded() { + if (p5LoadPromise) return p5LoadPromise; + p5LoadPromise = (async () => { + installDomStubs(); + const res = await fetch(P5_URL, { cache: "force-cache" }); + if (!res.ok) throw new Error(`failed to fetch p5: ${res.status}`); + const code2 = await res.text(); + (0, eval)(code2); + if (typeof self.p5 !== "function") { + throw new Error("p5 did not attach to global after eval"); + } + })(); + return p5LoadPromise; +} +function setViewport(w, h) { + if (typeof self.window === "undefined") return; + self.window.innerWidth = w; + self.window.innerHeight = h; + self.window.windowWidth = w; + self.window.windowHeight = h; + self.window.displayWidth = w; + self.window.displayHeight = h; + if (self.window.screen) { + self.window.screen.width = w; + self.window.screen.height = h; + self.window.screen.availWidth = w; + self.window.screen.availHeight = h; + } +} +var NAMED_KEYS = { + arrowup: 38, + arrowdown: 40, + arrowleft: 37, + arrowright: 39, + enter: 13, + escape: 27, + tab: 9, + backspace: 8, + delete: 46, + shift: 16, + control: 17, + alt: 18, + meta: 91, + capslock: 20, + space: 32, + " ": 32, + pageup: 33, + pagedown: 34, + home: 36, + end: 35, + insert: 45, + f1: 112, + f2: 113, + f3: 114, + f4: 115, + f5: 116, + f6: 117, + f7: 118, + f8: 119, + f9: 120, + f10: 121, + f11: 122, + f12: 123 +}; +function keyToKeyCode(key) { + if (!key) return 0; + const lower = String(key).toLowerCase(); + if (NAMED_KEYS[lower] !== void 0) return NAMED_KEYS[lower]; + if (key.length === 1) return key.toUpperCase().charCodeAt(0); + return 0; +} +function normalizeKey(key) { + if (!key) return ""; + return key; +} +var SKETCH_GLOBALS = [ + "setup", + "draw", + "preload", + "mousePressed", + "mouseReleased", + "mouseClicked", + "mouseMoved", + "mouseDragged", + "mouseWheel", + "doubleClicked", + "keyPressed", + "keyReleased", + "keyTyped", + "touchStarted", + "touchMoved", + "touchEnded", + "windowResized", + "deviceMoved", + "deviceTurned", + "deviceShaken" +]; +function clearSketchGlobals() { + for (const k of SKETCH_GLOBALS) { + try { + delete self[k]; + } catch { + } + } +} +async function makeP5WorkerModule({ slug, source }) { + await ensureP5Loaded(); + let p5Instance = null; + let canvasShim = null; + let mouseDown = false; + let bootError = null; + let lastWidth = 0; + let lastHeight = 0; + let paintCount2 = 0; + let lastPaintTime2 = 0; + let drainTimeTotal = 0; + let blitTimeTotal = 0; + let dtTotal = 0; + let drainCountTotal = 0; + let maxDt = 0; + let maxBlit = 0; + let maxDrain = 0; + let p5Looping = true; + let cachedCtx = null; + const blitPixelsToScreen = (screen2) => { + if (!canvasShim) return; + const oc = canvasShim.offscreen; + if (!cachedCtx) cachedCtx = canvasShim.getContext("2d"); + if (oc.width !== screen2.width || oc.height !== screen2.height) { + const w = Math.min(oc.width, screen2.width); + const h = Math.min(oc.height, screen2.height); + const img = cachedCtx.getImageData(0, 0, w, h); + const dst = screen2.pixels; + const stride = screen2.width * 4; + const srcStride = w * 4; + for (let row = 0; row < h; row++) { + dst.set(img.data.subarray(row * srcStride, (row + 1) * srcStride), row * stride); + } + } else { + const img = cachedCtx.getImageData(0, 0, oc.width, oc.height); + screen2.pixels.set(img.data); + } + }; + return { + boot: async ({ screen: screen2 }) => { + console.log(`[p5-worker] \u{1F7E2} BOOT START v4 screen=${screen2.width}x${screen2.height} slug=${slug}`); + try { + clearSketchGlobals(); + if (self.__acP5CreatedCanvases) self.__acP5CreatedCanvases.length = 0; + setViewport(screen2.width, screen2.height); + lastWidth = screen2.width; + lastHeight = screen2.height; + try { + (0, eval)(source); + } catch (err) { + bootError = `sketch eval: ${err && err.message || err}`; + console.error("[p5-worker]", bootError, err); + return; + } + try { + p5Instance = new self.p5(void 0, void 0); + } catch (err) { + bootError = `new p5(): ${err && err.message || err}`; + console.error("[p5-worker]", bootError, err); + return; + } + const tryCanvas = () => { + const candidates = [ + p5Instance?._renderer?.canvas, + p5Instance?.canvas, + p5Instance?._renderer?.elt, + ...self.__acP5CreatedCanvases || [] + ]; + for (const c4 of candidates) { + if (c4 instanceof CanvasShim) { + canvasShim = c4; + return true; + } + if (c4 && c4.offscreen instanceof OffscreenCanvas) { + canvasShim = c4; + return true; + } + } + return false; + }; + const start = performance.now(); + while ((!tryCanvas() || !p5Instance?._setupDone) && performance.now() - start < 1e3) { + await new Promise((r2) => setTimeout(r2, 10)); + } + if (!canvasShim) { + bootError = "could not locate sketch canvas after 1000ms \u2014 did setup() call createCanvas()?"; + console.warn("[p5-worker]", bootError); + } else { + try { + p5Instance.noLoop(); + p5Looping = false; + } catch (err) { + console.warn("[p5-worker] noLoop failed:", err); + } + console.log(`[p5-worker] \u{1F7E2} canvas=${canvasShim.offscreen.width}x${canvasShim.offscreen.height} setupDone=${p5Instance?._setupDone} looping=${p5Looping}`); + } + } catch (err) { + bootError = String(err); + console.error("[p5-worker] boot crash:", err); + } + }, + paint: ({ screen: screen2, ink: ink3, wipe }) => { + if (bootError) { + wipe(20, 0, 0); + ink3(255, 120, 120).write(`p5 boot error:`, { x: 8, y: 12 }); + ink3(255, 200, 200).write(bootError.slice(0, 200), { x: 8, y: 28 }); + return true; + } + const t0 = performance.now(); + const dt = lastPaintTime2 ? t0 - lastPaintTime2 : 0; + lastPaintTime2 = t0; + if (p5Instance && p5Instance._setupDone) { + try { + p5Instance.redraw(); + } catch (err) { + console.warn("[p5-worker] redraw:", err); + } + } + const t1 = performance.now(); + blitPixelsToScreen(screen2); + const t2 = performance.now(); + const drawMs = t1 - t0; + const blitMs = t2 - t1; + drainTimeTotal += drawMs; + blitTimeTotal += blitMs; + dtTotal += dt; + if (dt > maxDt) maxDt = dt; + if (blitMs > maxBlit) maxBlit = blitMs; + if (drawMs > maxDrain) maxDrain = drawMs; + paintCount2++; + const shouldLog = paintCount2 <= 3 || paintCount2 % 30 === 0; + if (shouldLog) { + const N = paintCount2 <= 3 ? 1 : 30; + const fps = dtTotal > 0 ? N * 1e3 / dtTotal : 0; + const cw = canvasShim && canvasShim.offscreen ? canvasShim.offscreen.width : "?"; + const ch = canvasShim && canvasShim.offscreen ? canvasShim.offscreen.height : "?"; + console.log( + `[p5-worker] frame=${paintCount2} fps=${fps.toFixed(1)} dt=${(dtTotal / N).toFixed(1)}ms(max ${maxDt.toFixed(1)}) draw=${(drainTimeTotal / N).toFixed(2)}ms(max ${maxDrain.toFixed(2)}) blit=${(blitTimeTotal / N).toFixed(2)}ms(max ${maxBlit.toFixed(2)}) canvas=${cw}x${ch} screen=${screen2.width}x${screen2.height}` + ); + drainTimeTotal = blitTimeTotal = dtTotal = 0; + maxDt = maxBlit = maxDrain = 0; + } + return true; + }, + // sim is intentionally empty — reframe is handled via the `reframed` + // event in act() (AC's canonical channel for screen-size changes). + act: ({ event: e2, jump: jump2, handle: handle2, notice: notice2 }) => { + if (!self.ac && jump2) { + self.ac = { + jump: (path, opts) => jump2(path, opts), + handle: () => typeof handle2 === "function" ? handle2() : null, + notice: (msg, color3) => notice2 && notice2(msg, color3) + }; + } + if (!p5Instance) return; + if (e2.is("reframed")) { + const w = e2.width ?? e2.content?.width ?? lastWidth; + const h = e2.height ?? e2.content?.height ?? lastHeight; + lastWidth = w; + lastHeight = h; + setViewport(w, h); + try { + p5Instance.resizeCanvas?.(w, h); + if (canvasShim && (canvasShim.offscreen.width !== w || canvasShim.offscreen.height !== h)) { + canvasShim._oc.width = w; + canvasShim._oc.height = h; + } + } catch (err) { + console.warn("[p5-worker] resizeCanvas:", err); + } + if (typeof self.windowResized === "function") { + try { + self.windowResized(); + } catch (err) { + console.warn("[p5-worker] windowResized:", err); + } + } + console.log(`[p5-worker] \u{1F4D0} reframed \u2192 ${w}x${h}`); + return; + } + const updateMouse = (x, y) => { + const cw = canvasShim?.offscreen.width || lastWidth || 1; + const ch = canvasShim?.offscreen.height || lastHeight || 1; + const sx = cw / (lastWidth || cw); + const sy = ch / (lastHeight || ch); + const mx = x * sx; + const my = y * sy; + self.window.pmouseX = self.window.mouseX ?? mx; + self.window.pmouseY = self.window.mouseY ?? my; + self.window.mouseX = mx; + self.window.mouseY = my; + self.window.winMouseX = mx; + self.window.winMouseY = my; + self.window.pwinMouseX = self.window.winMouseX; + self.window.pwinMouseY = self.window.winMouseY; + if (p5Instance) { + p5Instance.pmouseX = p5Instance.mouseX; + p5Instance.pmouseY = p5Instance.mouseY; + p5Instance.mouseX = mx; + p5Instance.mouseY = my; + p5Instance.winMouseX = mx; + p5Instance.winMouseY = my; + } + }; + if (e2.is("move") || e2.is("draw")) { + updateMouse(e2.x, e2.y); + const fn = mouseDown ? self.mouseDragged : self.mouseMoved; + if (typeof fn === "function") { + try { + fn(); + } catch (err) { + console.warn(err); + } + } + } else if (e2.is("touch")) { + mouseDown = true; + self.window.mouseIsPressed = true; + self.window.mouseButton = self.LEFT || "left"; + if (p5Instance) { + p5Instance.mouseIsPressed = true; + p5Instance.mouseButton = p5Instance.LEFT; + } + updateMouse(e2.x, e2.y); + if (typeof self.mousePressed === "function") { + try { + self.mousePressed(); + } catch (err) { + console.warn(err); + } + } + if (typeof self.touchStarted === "function") { + try { + self.touchStarted(); + } catch (err) { + console.warn(err); + } + } + } else if (e2.is("lift")) { + mouseDown = false; + self.window.mouseIsPressed = false; + if (p5Instance) p5Instance.mouseIsPressed = false; + if (typeof self.mouseReleased === "function") { + try { + self.mouseReleased(); + } catch (err) { + console.warn(err); + } + } + if (typeof self.mouseClicked === "function") { + try { + self.mouseClicked(); + } catch (err) { + console.warn(err); + } + } + if (typeof self.touchEnded === "function") { + try { + self.touchEnded(); + } catch (err) { + console.warn(err); + } + } + } else if (e2.device === "wheel") { + if (typeof self.mouseWheel === "function") { + try { + self.mouseWheel({ delta: e2.y ?? e2.deltaY ?? 0, deltaX: e2.x ?? 0, deltaY: e2.y ?? 0 }); + } catch (err) { + console.warn(err); + } + } + } else if (e2.is("keyboard:down")) { + const k = e2.key || ""; + self.window.key = normalizeKey(k); + self.window.keyCode = keyToKeyCode(k); + self.window.keyIsPressed = true; + if (p5Instance) { + p5Instance.key = self.window.key; + p5Instance.keyCode = self.window.keyCode; + p5Instance.keyIsPressed = true; + } + if (typeof self.keyPressed === "function") { + try { + self.keyPressed(); + } catch (err) { + console.warn(err); + } + } + if (k.length === 1 && typeof self.keyTyped === "function") { + try { + self.keyTyped(); + } catch (err) { + console.warn(err); + } + } + } else if (e2.is("keyboard:up")) { + self.window.keyIsPressed = false; + if (p5Instance) p5Instance.keyIsPressed = false; + if (typeof self.keyReleased === "function") { + try { + self.keyReleased(); + } catch (err) { + console.warn(err); + } + } + } + }, + leave: () => { + try { + p5Instance?.remove?.(); + } catch { + } + p5Instance = null; + canvasShim = null; + cachedCtx = null; + clearSketchGlobals(); + } + }; +} + +// public/aesthetic.computer/lib/l5.mjs +var WASM_URL; +function getWasmUrl() { + if (!WASM_URL) { + try { + WASM_URL = new URL("../dep/wasmoon/glue.wasm", import.meta.url).href; + } catch { + WASM_URL = null; + } + } + return WASM_URL; +} +var factoryPromise; +var LuaFactoryCtor; +async function ensureLuaFactoryCtor() { + if (!LuaFactoryCtor) { + const wasmoon = await import("../dep/wasmoon/index.mjs"); + LuaFactoryCtor = wasmoon.LuaFactory; + } + return LuaFactoryCtor; +} +async function ensureFactory() { + if (!factoryPromise) { + const LuaFactory = await ensureLuaFactoryCtor(); + factoryPromise = Promise.resolve(new LuaFactory(getWasmUrl())); + } + return factoryPromise; +} +function clampByte(value) { + if (typeof value !== "number" || !Number.isFinite(value)) return 0; + if (value < 0) return 0; + if (value > 255) return 255; + return Math.round(value); +} +function asNumber(value, fallback = 0) { + if (typeof value === "number" && Number.isFinite(value)) return value; + return fallback; +} +function parseColorArgs(args, fallback = [255, 255, 255, 255]) { + if (!Array.isArray(args) || args.length === 0) return [...fallback]; + if (Array.isArray(args[0])) { + return parseColorArgs(args[0], fallback); + } + if (args.length === 1) { + const gray = clampByte(args[0]); + return [gray, gray, gray, 255]; + } + const r2 = clampByte(args[0]); + const g = clampByte(args[1]); + const b2 = clampByte(args[2]); + const a2 = clampByte(args.length >= 4 ? args[3] : 255); + return [r2, g, b2, a2]; +} +function applyInk($, color3) { + $.ink(color3[0], color3[1], color3[2], color3[3]); +} +function safeGetGlobalFn(engine, name) { + try { + const fn = engine.global.get(name); + return typeof fn === "function" ? fn : null; + } catch { + return null; + } +} +function updateInputGlobals(engine, $, state2) { + const mouseX = asNumber($.pen?.x, 0); + const mouseY = asNumber($.pen?.y, 0); + state2.pmouseX = state2.mouseX; + state2.pmouseY = state2.mouseY; + state2.mouseX = mouseX; + state2.mouseY = mouseY; + engine.global.set("mouseX", mouseX); + engine.global.set("mouseY", mouseY); + engine.global.set("pmouseX", state2.pmouseX); + engine.global.set("pmouseY", state2.pmouseY); + engine.global.set("movedX", mouseX - state2.pmouseX); + engine.global.set("movedY", mouseY - state2.pmouseY); + engine.global.set("mouseIsPressed", !!state2.mouseIsPressed); + engine.global.set("width", asNumber($.screen?.width, 128)); + engine.global.set("height", asNumber($.screen?.height, 128)); + engine.global.set("frameCount", state2.frameCount); + engine.global.set("deltaTime", state2.deltaTime); + engine.global.set("focused", true); + engine.global.set("key", state2.key); + engine.global.set("keyCode", state2.keyCode); + engine.global.set("keyIsPressed", !!state2.keyIsPressed); +} +function makeErrorModule(message) { + return { + boot() { + }, + sim() { + }, + act() { + }, + leave() { + }, + paint($) { + $.wipe(20, 0, 0); + $.ink(255, 80, 80).write("L5 COMPILE ERROR", { x: 4, y: 4 }); + $.ink(255, 220, 220).write(String(message || "unknown error"), { + x: 4, + y: 18 + }); + return true; + } + }; +} +async function module2(source) { + const factory = await ensureFactory(); + const engine = await factory.createEngine({ + injectObjects: false, + enableProxy: true, + openStandardLibs: true + }); + let activeApi = null; + let runtimeError = null; + let closed = false; + const state2 = { + fillEnabled: true, + strokeEnabled: true, + fillColor: [255, 255, 255, 255], + strokeColor: [0, 0, 0, 255], + strokeWeight: 1, + textSize: 1, + sizeWasCalled: false, + pendingResizePaints: 0, + looping: true, + redrawRequested: false, + hasDrawnOnce: false, + frameCount: 0, + deltaTime: 16, + lastFrameMs: 0, + mouseX: 0, + mouseY: 0, + pmouseX: 0, + pmouseY: 0, + mouseIsPressed: false, + key: "", + keyCode: 0, + keyIsPressed: false + }; + const withApi = (fn) => (...args) => { + if (!activeApi) return void 0; + try { + return fn(activeApi, ...args); + } catch (error) { + runtimeError = error; + console.error("L5 runtime API error:", error); + return void 0; + } + }; + const drawRect = ($, x, y, w, h) => { + if (state2.fillEnabled) { + applyInk($, state2.fillColor); + $.box(x, y, w, h, "fill"); + } + if (state2.strokeEnabled) { + applyInk($, state2.strokeColor); + $.box(x, y, w, h, "outline"); + } + }; + const drawCircle = ($, x, y, diameter) => { + const radius = asNumber(diameter, 0) / 2; + if (state2.fillEnabled) { + applyInk($, state2.fillColor); + $.circle(x, y, radius, true); + } + if (state2.strokeEnabled) { + applyInk($, state2.strokeColor); + $.circle(x, y, radius, false, Math.max(1, asNumber(state2.strokeWeight, 1))); + } + }; + const drawEllipse = ($, x, y, w, h) => { + const rx = asNumber(w, 0) / 2; + const ry = asNumber(h, 0) / 2; + if (state2.fillEnabled) { + applyInk($, state2.fillColor); + $.oval(x, y, rx, ry, true); + } + if (state2.strokeEnabled) { + applyInk($, state2.strokeColor); + $.oval(x, y, rx, ry, false, Math.max(1, asNumber(state2.strokeWeight, 1))); + } + }; + const drawTriangle = ($, x1, y1, x2, y2, x3, y3) => { + if (state2.fillEnabled) { + applyInk($, state2.fillColor); + $.tri(x1, y1, x2, y2, x3, y3, "fill"); + } + if (state2.strokeEnabled) { + applyInk($, state2.strokeColor); + $.tri(x1, y1, x2, y2, x3, y3, "outline"); + } + }; + const drawQuad = ($, x1, y1, x2, y2, x3, y3, x4, y4) => { + if (state2.fillEnabled) { + applyInk($, state2.fillColor); + $.shape({ points: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]], filled: true }); + } + if (state2.strokeEnabled) { + applyInk($, state2.strokeColor); + $.poly([[x1, y1], [x2, y2], [x3, y3], [x4, y4], [x1, y1]]); + } + }; + engine.global.set("background", withApi(($, ...args) => { + const color3 = parseColorArgs(args, [0, 0, 0, 255]); + $.wipe(color3[0], color3[1], color3[2], color3[3]); + })); + engine.global.set("clear", withApi(($) => { + $.wipe(0, 0, 0, 0); + })); + engine.global.set("size", withApi(($, w, h) => { + state2.sizeWasCalled = true; + state2.pendingResizePaints = Math.max(state2.pendingResizePaints, 3); + $.resolution(asNumber(w, 128), asNumber(h, asNumber(w, 128))); + $.needsPaint?.(); + })); + engine.global.set("fill", (...args) => { + state2.fillEnabled = true; + state2.fillColor = parseColorArgs(args, state2.fillColor); + }); + engine.global.set("noFill", () => { + state2.fillEnabled = false; + }); + engine.global.set("stroke", (...args) => { + state2.strokeEnabled = true; + state2.strokeColor = parseColorArgs(args, state2.strokeColor); + }); + engine.global.set("noStroke", () => { + state2.strokeEnabled = false; + }); + engine.global.set("strokeWeight", (value) => { + state2.strokeWeight = Math.max(1, asNumber(value, 1)); + }); + engine.global.set("point", withApi(($, x, y) => { + if (!state2.strokeEnabled) return; + applyInk($, state2.strokeColor); + $.plot(asNumber(x), asNumber(y)); + })); + engine.global.set("line", withApi(($, x1, y1, x2, y2) => { + if (!state2.strokeEnabled) return; + applyInk($, state2.strokeColor); + $.line(asNumber(x1), asNumber(y1), asNumber(x2), asNumber(y2)); + })); + engine.global.set("rect", withApi(($, x, y, w, h) => { + drawRect($, asNumber(x), asNumber(y), asNumber(w), asNumber(h)); + })); + engine.global.set("square", withApi(($, x, y, size) => { + const side = asNumber(size); + drawRect($, asNumber(x), asNumber(y), side, side); + })); + engine.global.set("circle", withApi(($, x, y, diameter) => { + drawCircle($, asNumber(x), asNumber(y), asNumber(diameter)); + })); + engine.global.set("ellipse", withApi(($, x, y, w, h) => { + drawEllipse($, asNumber(x), asNumber(y), asNumber(w), asNumber(h)); + })); + engine.global.set("triangle", withApi(($, x1, y1, x2, y2, x3, y3) => { + drawTriangle($, asNumber(x1), asNumber(y1), asNumber(x2), asNumber(y2), asNumber(x3), asNumber(y3)); + })); + engine.global.set("quad", withApi(($, x1, y1, x2, y2, x3, y3, x4, y4) => { + drawQuad( + $, + asNumber(x1), + asNumber(y1), + asNumber(x2), + asNumber(y2), + asNumber(x3), + asNumber(y3), + asNumber(x4), + asNumber(y4) + ); + })); + engine.global.set("text", withApi(($, str7, x, y) => { + if (!state2.fillEnabled) return; + applyInk($, state2.fillColor); + $.write(String(str7 ?? ""), { + x: asNumber(x), + y: asNumber(y), + size: Math.max(1, asNumber(state2.textSize, 1) / 8) + }); + })); + engine.global.set("textSize", (value) => { + state2.textSize = Math.max(1, asNumber(value, 8)); + }); + engine.global.set("textWidth", withApi(($, value) => { + return $.text.width(String(value ?? "")); + })); + engine.global.set("frameRate", withApi(($, value) => { + $.fps?.(asNumber(value)); + })); + engine.global.set("noLoop", () => { + state2.looping = false; + }); + engine.global.set("loop", withApi(($) => { + state2.looping = true; + state2.redrawRequested = true; + $.needsPaint?.(); + })); + engine.global.set("isLooping", () => state2.looping); + engine.global.set("redraw", withApi(($) => { + state2.redrawRequested = true; + $.needsPaint?.(); + })); + engine.global.set("millis", () => performance.now()); + engine.global.set("print", (...args) => console.log(...args)); + engine.global.set("println", (...args) => console.log(...args)); + engine.global.set("random", (a2, b2) => { + if (a2 === void 0) return Math.random(); + const min10 = b2 === void 0 ? 0 : asNumber(a2); + const max9 = b2 === void 0 ? asNumber(a2, 1) : asNumber(b2, 1); + return min10 + Math.random() * (max9 - min10); + }); + engine.global.set("radians", (deg) => asNumber(deg) * Math.PI / 180); + engine.global.set("degrees", (rad) => asNumber(rad) * 180 / Math.PI); + engine.global.set("constrain", (v2, lo, hi) => Math.max(asNumber(lo), Math.min(asNumber(hi), asNumber(v2)))); + engine.global.set("lerp", (a2, b2, t2) => asNumber(a2) + (asNumber(b2) - asNumber(a2)) * asNumber(t2)); + engine.global.set("dist", (x1, y1, x2, y2) => { + const dx = asNumber(x2) - asNumber(x1); + const dy = asNumber(y2) - asNumber(y1); + return Math.sqrt(dx * dx + dy * dy); + }); + engine.global.set("map", (v2, inMin, inMax, outMin, outMax) => { + const n2 = asNumber(v2); + const a2 = asNumber(inMin); + const b2 = asNumber(inMax); + if (b2 === a2) return asNumber(outMin); + const t2 = (n2 - a2) / (b2 - a2); + return asNumber(outMin) + t2 * (asNumber(outMax) - asNumber(outMin)); + }); + engine.global.set("PI", Math.PI); + engine.global.set("HALF_PI", Math.PI / 2); + engine.global.set("QUARTER_PI", Math.PI / 4); + engine.global.set("TWO_PI", Math.PI * 2); + engine.global.set("TAU", Math.PI * 2); + engine.global.set("LEFT", "LEFT"); + engine.global.set("RIGHT", "RIGHT"); + engine.global.set("CENTER", "CENTER"); + engine.global.set("TOP", "TOP"); + engine.global.set("BOTTOM", "BOTTOM"); + engine.global.set("CORNER", "CORNER"); + engine.global.set("CORNERS", "CORNERS"); + engine.global.set("RADIUS", "RADIUS"); + engine.global.set("CLOSE", "CLOSE"); + engine.global.set("RGB", "RGB"); + engine.global.set("HSB", "HSB"); + engine.global.set("HSL", "HSL"); + try { + await engine.doString(source); + } catch (error) { + console.error("L5 compile error:", error); + try { + engine.global.close(); + } catch { + } + return makeErrorModule(error?.message || String(error)); + } + const setup = safeGetGlobalFn(engine, "setup"); + const draw2 = safeGetGlobalFn(engine, "draw"); + const simFn = safeGetGlobalFn(engine, "sim"); + const actFn = safeGetGlobalFn(engine, "act"); + const mousePressed = safeGetGlobalFn(engine, "mousePressed"); + const mouseReleased = safeGetGlobalFn(engine, "mouseReleased"); + const mouseMoved = safeGetGlobalFn(engine, "mouseMoved"); + const mouseDragged = safeGetGlobalFn(engine, "mouseDragged"); + const keyPressed = safeGetGlobalFn(engine, "keyPressed"); + const keyReleased = safeGetGlobalFn(engine, "keyReleased"); + const safeCall = (name, fn, ...args) => { + if (typeof fn !== "function") return; + try { + fn(...args); + } catch (error) { + runtimeError = error; + console.error(`L5 runtime error in ${name}:`, error); + } + }; + const maybePaintRuntimeError = ($) => { + if (!runtimeError) return false; + $.ink(255, 60, 60).box(0, 0, $.screen.width, 14); + $.ink(255, 240, 240).write(`L5: ${String(runtimeError.message || runtimeError)}`, { + x: 2, + y: 2 + }); + return true; + }; + const runSetup = async ($) => { + activeApi = $; + updateInputGlobals(engine, $, state2); + safeCall("setup", setup); + if (!state2.sizeWasCalled) { + const w = $.screen?.width || 128; + const h = $.screen?.height || 128; + $.resolution(w, h, 0); + } + }; + const runPaint = ($) => { + activeApi = $; + const now = performance.now(); + if (state2.lastFrameMs === 0) { + state2.deltaTime = 16; + } else { + state2.deltaTime = Math.max(0, now - state2.lastFrameMs); + } + state2.lastFrameMs = now; + updateInputGlobals(engine, $, state2); + const shouldDraw = state2.looping || !state2.hasDrawnOnce || state2.redrawRequested || state2.pendingResizePaints > 0; + if (!shouldDraw) { + maybePaintRuntimeError($); + return false; + } + state2.redrawRequested = false; + state2.frameCount += 1; + engine.global.set("frameCount", state2.frameCount); + safeCall("draw", draw2); + state2.hasDrawnOnce = true; + if (state2.pendingResizePaints > 0) { + state2.pendingResizePaints -= 1; + if (state2.pendingResizePaints > 0) { + $.needsPaint?.(); + } + } + maybePaintRuntimeError($); + return true; + }; + const runSim = ($) => { + activeApi = $; + updateInputGlobals(engine, $, state2); + safeCall("sim", simFn); + }; + const runAct = ($) => { + activeApi = $; + updateInputGlobals(engine, $, state2); + const event = $.event; + if (event) { + const eventName = event.name || ""; + if (eventName.startsWith("keyboard:down")) { + state2.keyIsPressed = true; + const maybeKey = event.key || event.char || eventName.split(":").pop() || ""; + state2.key = String(maybeKey || ""); + state2.keyCode = state2.key.length ? state2.key.charCodeAt(0) : 0; + engine.global.set("key", state2.key); + engine.global.set("keyCode", state2.keyCode); + engine.global.set("keyIsPressed", true); + safeCall("keyPressed", keyPressed); + } else if (eventName.startsWith("keyboard:up")) { + state2.keyIsPressed = false; + engine.global.set("keyIsPressed", false); + safeCall("keyReleased", keyReleased); + } + const isEvent = (name) => { + if (typeof event.is === "function") return event.is(name); + return eventName === name; + }; + if (isEvent("touch")) { + state2.mouseIsPressed = true; + engine.global.set("mouseIsPressed", true); + safeCall("mousePressed", mousePressed); + } else if (isEvent("lift")) { + state2.mouseIsPressed = false; + engine.global.set("mouseIsPressed", false); + safeCall("mouseReleased", mouseReleased); + } else if (isEvent("move")) { + safeCall("mouseMoved", mouseMoved); + } else if (isEvent("draw")) { + state2.mouseIsPressed = true; + engine.global.set("mouseIsPressed", true); + safeCall("mouseDragged", mouseDragged); + } + } + safeCall("act", actFn); + }; + const runLeave = () => { + if (closed) return; + closed = true; + try { + engine.global.close(); + } catch (error) { + console.warn("L5 leave cleanup error:", error); + } + }; + return { + boot: runSetup, + paint: runPaint, + sim: runSim, + act: runAct, + leave: runLeave + }; +} + +// public/aesthetic.computer/disks/common/tape-player.mjs +var { floor: floor10, sin: sin4, max: max7, min: min8 } = Math; +function calculateVHSEffects(x, animFrame) { + const scanLine = sin4(animFrame * 0.5 + x * 0.6) * 0.15 + 0.85; + const glowPhase = (animFrame * 0.15 + x * 0.12) % (Math.PI * 2); + const analogGlow = sin4(glowPhase) * 0.2 + 0.8; + const tracking = sin4(animFrame * 0.08 + x * 0.03) * 0.1 + 0.9; + const secondaryGlow = sin4(animFrame * 0.25 + x * 0.2) * 0.1 + 0.9; + return { scanLine, analogGlow, tracking, secondaryGlow }; +} +function blendColorWithVHS(sampledColor, vhsR, vhsG, vhsB, blendFactor = 0.55) { + return { + r: floor10(sampledColor.r * blendFactor + vhsR * (1 - blendFactor)), + g: floor10(sampledColor.g * blendFactor + vhsG * (1 - blendFactor)), + b: floor10(sampledColor.b * blendFactor + vhsB * (1 - blendFactor)) + }; +} + +// public/aesthetic.computer/lib/disk.mjs +var diskTimings = { + sessionStarted: null, + preambleComplete: null, + loadStarted: null, + fetchComplete: null, + compileComplete: null, + bootStarted: null, + bootComplete: null, + firstPaint: null, + firstRenderSent: null +}; +var diskTimingStart = performance.now(); +var _importStart = performance.now(); +var { pow: pow3, abs: abs5, round: round7, sin: sin5, random: random7, min: min9, max: max8, floor: floor11, cos: cos4 } = Math; +var { keys: keys3 } = Object; +var _importEnd = performance.now(); +if (_importEnd - _importStart > 50) { + console.log(`\u{1F4E6} [DISK] Imports: ${(_importEnd - _importStart).toFixed(0)}ms`); +} +function matrixDebugEnabled2() { + if (typeof window !== "undefined" && window?.acMatrixDebug) return true; + if (typeof globalThis !== "undefined" && globalThis?.acMatrixDebug) + return true; + return false; +} +function inkFloodLoggingEnabled2() { + if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_COLORS) return true; + if (typeof process !== "undefined" && process.env?.AC_LOG_INK_COLORS === "1") return true; + return false; +} +function inkFloodLogPrefix2() { + let label = null; + if (typeof process !== "undefined" && process.env?.AC_LOG_INK_LABEL) { + label = process.env.AC_LOG_INK_LABEL; + } else if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_LABEL) { + label = globalThis.AC_LOG_INK_LABEL; + } + return label ? `[${label}] ` : ""; +} +function cloneColorForLog2(color3) { + if (Array.isArray(color3)) return Array.from(color3); + return color3; +} +function cloneArgsForLog(args) { + return Array.from(args).map((arg) => { + if (Array.isArray(arg)) return Array.from(arg); + if (arg && typeof arg === "object") { + try { + return JSON.parse(JSON.stringify(arg)); + } catch (err) { + return String(arg); + } + } + return arg; + }); +} +if (typeof globalThis !== "undefined") { + if (globalThis.acMatrixDebug === void 0) { + globalThis.acMatrixDebug = true; + } + if (typeof window !== "undefined" && window.acMatrixDebug === void 0) { + window.acMatrixDebug = globalThis.acMatrixDebug; + } + if (typeof window !== "undefined") { + window.acClearImageCache = () => { + imageCache.clear(); + console.log("\u{1F5D1}\uFE0F Image cache cleared"); + }; + window.acImageCacheStats = () => { + console.log("\u{1F5BC}\uFE0F Image cache stats:", { + memorySize: imageCache.memory.size, + urls: Array.from(imageCache.memory.keys()) + }); + }; + } +} +async function fetchPieceMetadata(pieceCode) { + try { + const { protocol, hostname } = getSafeUrlParts(); + const apiUrl = `${protocol}//${hostname}/api/piece-metadata?code=${encodeURIComponent(pieceCode)}`; + const response = await fetch(apiUrl); + if (!response.ok) { + console.warn(`\u26A0\uFE0F Failed to fetch metadata for piece: ${pieceCode}`); + return { code: pieceCode, trustLevel: "untrusted", anonymous: true }; + } + const metadata2 = await response.json(); + return metadata2; + } catch (error) { + console.warn(`\u26A0\uFE0F Error fetching piece metadata:`, error); + return { code: pieceCode, trustLevel: "untrusted", anonymous: true }; + } +} +function isSandboxed() { + try { + if (typeof window !== "undefined" && window.acSPIDER) { + return true; + } + if (typeof window !== "undefined") { + return window.origin === "null"; + } else if (typeof self !== "undefined" && self.origin) { + return self.origin === "null"; + } else if (typeof location !== "undefined" && location.origin) { + return location.origin === "null"; + } else { + return false; + } + } catch (err) { + return false; + } +} +function getSafeUrlParts() { + try { + const sandboxed = isSandboxed(); + if (sandboxed) { + return { + protocol: "https:", + hostname: "aesthetic.computer", + host: "aesthetic.computer" + }; + } else { + let loc = null; + if (typeof location !== "undefined") { + loc = location; + } else if (typeof self !== "undefined" && self.location) { + loc = self.location; + } else if (typeof window !== "undefined" && window.location) { + loc = window.location; + } + if (loc) { + return { + protocol: loc.protocol, + hostname: loc.hostname || loc.host, + // `host` keeps the port (localhost:8888) — hostname drops it, + // which breaks root-relative preloads on port-carrying dev hosts. + host: loc.host || loc.hostname + }; + } else { + return { + protocol: "https:", + hostname: "aesthetic.computer", + host: "aesthetic.computer" + }; + } + } + } catch (err) { + return { + protocol: "https:", + hostname: "aesthetic.computer", + host: "aesthetic.computer" + }; + } +} +var SAME_ORIGIN_BUILT_IN_PIECE_HOSTS = /* @__PURE__ */ new Set([ + "aesthetic.computer", + "www.aesthetic.computer", + "kidlisp.com", + "www.kidlisp.com", + "notepat.com", + "www.notepat.com", + "nopaint.art", + "www.nopaint.art", + "oskiewar.com", + "www.oskiewar.com", + "laklok.com", + "www.laklok.com", + "p5.aesthetic.computer", + "sitemap.aesthetic.computer" +]); +function isLocalDevelopmentHost(hostname) { + return hostname === "localhost" || hostname === "127.0.0.1" || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname); +} +function getBuiltInPieceBaseUrl() { + const { protocol, hostname } = getSafeUrlParts(); + if (typeof window !== "undefined" && window.acSPIDER) { + return "https://aesthetic.computer"; + } + if (isLocalDevelopmentHost(hostname) && typeof location !== "undefined" && location.port) { + return `${protocol}//${hostname}:${location.port}`; + } + if (SAME_ORIGIN_BUILT_IN_PIECE_HOSTS.has(hostname)) { + return `${protocol}//${hostname}`; + } + return "https://aesthetic.computer"; +} +var tf; +var typefaceCache = /* @__PURE__ */ new Map(); +async function clearFontCaches() { + console.log("\u{1F524} Clearing font caches..."); + const clearedTypefaces = typefaceCache.size; + for (const [name, typeface] of typefaceCache) { + if (typeface.advanceCache) { + typeface.advanceCache.clear(); + } + } + typefaceCache.clear(); + try { + const { clearGlyphCache: clearGlyphCache2 } = await import("./type.mjs"); + if (clearGlyphCache2) { + await clearGlyphCache2(); + console.log("\u{1F524} IndexedDB glyph cache cleared"); + } + } catch (e2) { + console.log("\u{1F524} IndexedDB cache clear skipped:", e2.message); + } + try { + const timestamp2 = Date.now(); + const fontsUrl = `../disks/common/fonts.mjs?t=${timestamp2}`; + const fontsModule = await import(fontsUrl); + console.log("\u{1F524} fonts.mjs reloaded, MatrixChunky8 * advance:", fontsModule.MatrixChunky8?.advances?.["*"]); + } catch (e2) { + console.log("\u{1F524} fonts.mjs reload skipped:", e2.message); + } + console.log(`\u{1F524} Cleared ${clearedTypefaces} typeface(s). Refresh page or load a piece to see changes.`); + return true; +} +if (typeof window !== "undefined") { + window.clearFontCaches = clearFontCaches; + const isDevMode = location.host === "localhost:8888" || location.host === "aesthetic.local:8888" || location.host === "local.aesthetic.computer"; + if (isDevMode) { + (async () => { + try { + const { clearGlyphCache: clearGlyphCache2 } = await import("./type.mjs"); + if (clearGlyphCache2) { + await clearGlyphCache2(); + console.log("\u{1F524} Dev mode: IndexedDB glyph cache auto-cleared"); + } + } catch (e2) { + } + })(); + } +} +var DEFAULT_TYPEFACE_BLOCK_WIDTH = 6; +var DEFAULT_TYPEFACE_BLOCK_HEIGHT = 10; +var HUD_LABEL_TEXT_MARGIN = 0; +var HUD_DRIFT_X = 1.5; +var HUD_DRIFT_Y = 2; +var HUD_DRIFT_EASE_IN_MS = 220; +var HUD_DRIFT_EASE_OUT_MS = 380; +var HUD_DRIFT_PHASES = Float32Array.from( + { length: 64 }, + (_, i2) => i2 * 0.6180339887 % 1 * Math.PI * 2 +); +function hudDriftEase(amount) { + return amount * amount * (3 - 2 * amount); +} +function hudDriftX(i2, t2, ease) { + const p = HUD_DRIFT_PHASES[i2 & 63]; + return HUD_DRIFT_X * ease * (0.65 * Math.sin(t2 * 21e-4 + p) + 0.35 * Math.sin(t2 * 37e-4 + p * 1.7)); +} +function hudDriftY(i2, t2, ease) { + const p = HUD_DRIFT_PHASES[i2 + 17 & 63]; + return -HUD_DRIFT_Y * ease * Math.abs(Math.sin(t2 * 28e-4 + p)); +} +function hudCodeColor(code2, base) { + if (!code2) return base; + if (Array.isArray(code2)) return code2; + const s2 = code2.trim(); + const lower = s2.toLowerCase(); + if (lower === "reset" || lower === "default" || lower === "base") return base; + if (s2.includes(",")) { + const parts = s2.split(",").map((n2) => parseInt(n2.trim(), 10) || 0); + while (parts.length < 3) parts.push(0); + return parts.slice(0, 4); + } + return findColor2(s2) ?? base; +} +function resolveTypefaceInstance(typefaceRef) { + if (!typefaceRef) return void 0; + if (typefaceRef instanceof Typeface) return typefaceRef; + if (typeof typefaceRef === "string") { + if (typefaceCache.has(typefaceRef)) { + return typefaceCache.get(typefaceRef); + } + const instance = new Typeface(typefaceRef); + typefaceCache.set(typefaceRef, instance); + return instance; + } + if (typeof typefaceRef === "object" && typeof typefaceRef?.getGlyph === "function") { + return typefaceRef; + } + return void 0; +} +function ensureTypefaceLoaded(typeface) { + if (!typeface || typeof typeface.load !== "function") return; + const requiresLoad = typeface.data?.bdfFont || typeface.name === "unifont" || typeface.name === "MatrixChunky8"; + if (!requiresLoad) return; + if (!typeface.__loadPromise) { + typeface.__loadPromise = typeface.load($commonApi.net.preload, () => { + if (typeof window !== "undefined" && window.$activePaintApi?.needsPaint) { + window.$activePaintApi.needsPaint(); + } + }); + } + return typeface.__loadPromise; +} +function getTypefaceForMeasurement(typefaceName) { + if (!typefaceName) return tf; + const resolved = resolveTypefaceInstance(typefaceName); + return resolved || tf; +} +var _isRenderingShadow = false; +function writeHudLabelText($, text, { + x = 0, + y = 0, + typefaceName, + preserveColors = true, + bounds, + wordWrap +} = {}) { + if (text === void 0 || text === null || text === "") return; + const content = preserveColors ? text : stripColorCodes4(text); + const effectiveBounds = typeof bounds === "number" && bounds > 0 ? bounds : void 0; + const shouldWrap = wordWrap === void 0 ? effectiveBounds !== void 0 : wordWrap; + $.write( + content, + { x, y }, + void 0, + effectiveBounds, + shouldWrap, + typefaceName + ); + return $; +} +function isColorDark(colorStr) { + let rgb; + if (typeof colorStr === "string" && colorStr.includes(",")) { + const parts = colorStr.split(",").map((n2) => parseInt(n2.trim(), 10)); + rgb = parts; + } else if (typeof colorStr === "string") { + const lower = colorStr.toLowerCase(); + const resolved = findColor2(lower); + if (Array.isArray(resolved)) { + rgb = resolved; + } else { + return false; + } + } else if (Array.isArray(colorStr)) { + rgb = colorStr; + } + if (!rgb || rgb.length < 3) return false; + const [r2, g, b2] = rgb; + const luminance = (0.299 * r2 + 0.587 * g + 0.114 * b2) / 255; + return luminance < 0.5; +} +var BRIGHT_SHADOW_RGB = "220,220,220"; +var DARK_SHADOW_RGB = "30,20,50"; +function getShadowColorForText(colorStr) { + if (!colorStr) return "64,64,64"; + const isLightMode = $commonApi && !$commonApi.dark; + let rgb; + let normalizedCommand = null; + if (typeof colorStr === "string") { + normalizedCommand = colorStr.trim().toLowerCase(); + if (/[:(]/.test(normalizedCommand) || normalizedCommand.includes("?")) { + return isLightMode ? DARK_SHADOW_RGB : BRIGHT_SHADOW_RGB; + } + } + if (typeof colorStr === "string" && colorStr.includes(",")) { + const parts = colorStr.split(",").map((n2) => parseInt(n2.trim(), 10)); + rgb = parts; + } else if (typeof colorStr === "string") { + const lower = colorStr.toLowerCase(); + const resolved = findColor2(lower); + if (Array.isArray(resolved)) { + rgb = resolved; + } else { + return "64,64,64"; + } + } else if (Array.isArray(colorStr)) { + rgb = colorStr; + } + if (!rgb || rgb.length < 3) return "64,64,64"; + const [r2, g, b2] = rgb; + if (r2 <= 24 && g <= 24 && b2 <= 24) { + return BRIGHT_SHADOW_RGB; + } + const luminance = 0.299 * r2 + 0.587 * g + 0.114 * b2; + if (isLightMode && luminance > 140) { + return DARK_SHADOW_RGB; + } + if (r2 >= 0 && g === 0 && b2 === 0) { + const shadowR2 = Math.max(32, Math.round(r2 * 0.4)); + return `${shadowR2},0,0`; + } + if (r2 === 0 && g >= 0 && b2 === 0) { + const shadowG2 = Math.max(32, Math.round(g * 0.4)); + return `0,${shadowG2},0`; + } + if (r2 === 0 && b2 >= 0 && g >= 0) { + const expectedG = Math.round(b2 * 0.75); + if (Math.abs(g - expectedG) <= 1) { + const shadowG2 = Math.max(24, Math.round(g * 0.4)); + const shadowB2 = Math.max(32, Math.round(b2 * 0.4)); + return `0,${shadowG2},${shadowB2}`; + } + } + if (isColorDark(colorStr)) { + if (r2 <= 24 && g <= 24 && b2 <= 24) { + return "255,255,255"; + } + const factor2 = 0.85; + const shadowR2 = Math.round(r2 + (255 - r2) * factor2); + const shadowG2 = Math.round(g + (255 - g) * factor2); + const shadowB2 = Math.round(b2 + (255 - b2) * factor2); + return `${shadowR2},${shadowG2},${shadowB2}`; + } + const factor = 0.6; + const shadowR = Math.round(r2 * (1 - factor)); + const shadowG = Math.round(g * (1 - factor)); + const shadowB = Math.round(b2 * (1 - factor)); + return `${shadowR},${shadowG},${shadowB}`; +} +function replaceColorCodesWithShadows(text, defaultTextColor = "white") { + if (!text || !textContainsColorCodes(text)) return text; + let currentTextColor = defaultTextColor; + return mapColorCodes(text, (colorStr) => { + if (!colorStr) return colorStr; + const normalized = colorStr.trim(); + const lower = normalized.toLowerCase(); + if (lower === "reset" || lower === "default" || lower === "base") { + currentTextColor = defaultTextColor; + } else { + currentTextColor = normalized; + } + return getShadowColorForText(currentTextColor); + }); +} +function drawHudLabelText($, text, { + x = 0, + y = 0, + typefaceName, + textColor = "white", + shadowColor, + shadowOffsetX = 1, + shadowOffsetY = 1, + preserveColors = true, + bounds, + wordWrap +} = {}) { + if (!text) return; + const containsColorCodes = textContainsColorCodes(text); + const shouldPreserveColors = preserveColors || (typefaceName === "MatrixChunky8" || typefaceName === "unifont") && containsColorCodes; + const effectiveBounds = typeof bounds === "number" && bounds > 0 ? bounds : void 0; + const shouldWrap = wordWrap === void 0 ? effectiveBounds !== void 0 : wordWrap; + let effectiveShadowColor = shadowColor; + if (!effectiveShadowColor && textColor) { + effectiveShadowColor = getShadowColorForText(textColor); + } + if (!effectiveShadowColor) { + effectiveShadowColor = "black"; + } + const shouldRenderShadow = effectiveShadowColor && !((typefaceName === "MatrixChunky8" || typefaceName === "unifont") && matrixDebugEnabled2()); + if (shouldRenderShadow) { + _isRenderingShadow = true; + if (shouldPreserveColors && containsColorCodes) { + const shadowText = replaceColorCodesWithShadows(text, textColor); + writeHudLabelText($, shadowText, { + x: x + shadowOffsetX, + y: y + shadowOffsetY, + typefaceName, + preserveColors: true, + // Keep the shadow color codes + bounds: effectiveBounds, + wordWrap: shouldWrap + }); + } else { + $.ink(effectiveShadowColor); + writeHudLabelText($, text, { + x: x + shadowOffsetX, + y: y + shadowOffsetY, + typefaceName, + preserveColors: false, + bounds: effectiveBounds, + wordWrap: shouldWrap + }); + } + _isRenderingShadow = false; + } + if (textColor) { + $.ink(textColor); + } + const content = shouldPreserveColors ? text : stripColorCodes4(text); + $.write( + content, + { x, y }, + void 0, + effectiveBounds, + shouldWrap, + typefaceName + ); +} +function drawHudLabelDrift($, text, { x, y, typefaceName, typeface, textColor, lineStep, t: t2, ease }) { + if (!text) return; + const parts = splitColorCodes(text); + const perCodeShadows = parts.length > 1; + const blockWidth = typeface?.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; + for (let pass = 0; pass < 2; pass++) { + const shadow = pass === 0; + let color3 = textColor; + let cx = x, cy = y, i2 = 0; + for (let p = 0; p < parts.length; p++) { + if (p % 2 === 1) { + color3 = hudCodeColor(parts[p], textColor); + continue; + } + const run = parts[p]; + for (let j = 0; j < run.length; j++) { + const ch = run[j]; + if (ch === "\n") { + cx = x; + cy += lineStep; + continue; + } + if (ch !== " ") { + const dx = Math.round(cx + hudDriftX(i2, t2, ease)); + const dy = Math.round(cy + hudDriftY(i2, t2, ease)); + if (shadow) { + $.ink( + perCodeShadows ? hudCodeColor(getShadowColorForText(color3), "black") : "black" + ); + $.write(ch, { x: dx + 1, y: dy + 1 }, void 0, void 0, false, typefaceName); + } else { + $.ink(color3); + $.write(ch, { x: dx, y: dy }, void 0, void 0, false, typefaceName); + } + } + cx += typeface?.getAdvance?.(ch) ?? blockWidth; + i2++; + } + } + } +} +var noWorker = { onMessage: void 0, postMessage: void 0 }; +var ROOT_PIECE = "prompt"; +var USER; +var sessionStarted = false; +var LAN_HOST; +var SHARE_SUPPORTED; +var PREVIEW_OR_ICON; +var VSCODE; +var TV_MODE = false; +var DEVICE_MODE = false; +var SOLO_MODE = false; +var HIGHLIGHT_MODE = false; +var HIGHLIGHT_COLOR = "64,64,64"; +var PERF_MODE = false; +var AUTO_SCALE_MODE = false; +var SPOOF_AUDIO_MODE = false; +var NOGAP_MODE = false; +var AUDIO_SAMPLE_RATE = 0; +var loopPaused = false; +var debug3 = false; +var nopaintPerf = false; +var visible = true; +var pieceBackground = false; +var _trueScreenPixels = null; +var _trueScreenWidth = 0; +var _trueScreenHeight = 0; +var _mainScreenObject = null; +var globalKidLispInstance2 = null; +var persistentFirstLineColor = null; +function storePersistentFirstLineColor(color3) { + persistentFirstLineColor = color3; + if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { + window.setPersistentFirstLineColor(color3); + } +} +function getPersistentFirstLineColor() { + return persistentFirstLineColor; +} +function createGamepadAPI(gamepadEvents) { + if (!gamepadEvents) return []; + return gamepadEvents.map((events, index) => { + if (!events || events.length === 0) return null; + return { + index, + events, + // Helper to check if button was pushed/released + button(buttonIndex) { + const pushEvent = events.find( + (e2) => e2.button === buttonIndex && e2.action === "push" + ); + const releaseEvent = events.find( + (e2) => e2.button === buttonIndex && e2.action === "release" + ); + return { + pushed: !!pushEvent, + released: !!releaseEvent, + event: pushEvent || releaseEvent + }; + }, + // Helper to get axis value (returns most recent value in frame) + axis(axisIndex) { + const axisEvent = events.findLast((e2) => e2.axis === axisIndex); + return axisEvent ? axisEvent.value : 0; + }, + // Check if this gamepad is connected (has events) + connected() { + return events && events.length > 0; + }, + // Get the gamepad device ID/name + get id() { + return events[0]?.gamepadId || null; + } + }; + }); +} +if (typeof globalThis !== "undefined") { + globalThis.storePersistentFirstLineColor = storePersistentFirstLineColor; + globalThis.getPersistentFirstLineColor = getPersistentFirstLineColor; +} +if (typeof window !== "undefined") { + window.setPersistentFirstLineColor = function(color3) { + persistentFirstLineColor = color3; + }; + window.getPersistentFirstLineColor = function() { + return persistentFirstLineColor; + }; +} +function resolveBackgroundFillSpec(colorLike) { + if (colorLike === void 0 || colorLike === null) return null; + let resolved; + try { + resolved = findColor2(colorLike); + } catch (err) { + return null; + } + if (!Array.isArray(resolved) || resolved.length === 0) { + return null; + } + const first = resolved[0]; + if (typeof first === "string" && first.startsWith("fade:")) { + const fadeInfo = parseFadeColor(resolved); + if (fadeInfo) { + return { + type: "fade", + fadeInfo + }; + } + return null; + } + const clampChannel = (value, fallback = 0) => { + if (typeof value !== "number" || !Number.isFinite(value)) return fallback; + return Math.max(0, Math.min(255, Math.round(value))); + }; + const r2 = clampChannel(resolved[0], 0); + const g = clampChannel(resolved[1], r2); + const b2 = clampChannel(resolved[2], r2); + const a2 = clampChannel(resolved[3], 255); + return { + type: "solid", + rgba: [r2, g, b2, a2] + }; +} +function computeFadePositionForPixel(x, y, width2, height2, fadeInfo) { + if (!fadeInfo || !width2 || !height2) return 0; + const maxX = Math.max(width2 - 1, 0); + const maxY = Math.max(height2 - 1, 0); + const direction = fadeInfo.direction; + if (typeof direction === "number" && Number.isFinite(direction)) { + return calculateAngleFadePosition( + x, + y, + 0, + 0, + maxX, + maxY, + direction + ); + } + const numeric = direction !== void 0 ? parseFloat(direction) : NaN; + if (!Number.isNaN(numeric) && Number.isFinite(numeric)) { + return calculateAngleFadePosition( + x, + y, + 0, + 0, + maxX, + maxY, + numeric + ); + } + const safeDiv = (value, denom) => denom <= 0 ? 0 : value / denom; + switch (direction) { + case "horizontal-reverse": + return safeDiv(maxX - x, maxX); + case "vertical": + return safeDiv(y, maxY); + case "vertical-reverse": + return safeDiv(maxY - y, maxY); + case "diagonal": { + const dx = safeDiv(x, maxX); + const dy = safeDiv(y, maxY); + return (dx + dy) / 2; + } + case "diagonal-reverse": { + const dx = safeDiv(maxX - x, maxX); + const dy = safeDiv(maxY - y, maxY); + return (dx + dy) / 2; + } + case "horizontal": + default: + return safeDiv(x, maxX); + } +} +function fillExpandedWithSolidPixels(screen2, width2, height2, oldWidth, oldHeight, rgba) { + const [r2, g, b2] = rgba; + const applyPixel = (x, y) => { + const i2 = (y * width2 + x) * 4; + screen2.pixels[i2] = r2; + screen2.pixels[i2 + 1] = g; + screen2.pixels[i2 + 2] = b2; + screen2.pixels[i2 + 3] = 255; + }; + if (width2 > oldWidth) { + for (let y = 0; y < height2; y++) { + for (let x = oldWidth; x < width2; x++) { + applyPixel(x, y); + } + } + } + if (height2 > oldHeight) { + const bottomLimit = width2 > oldWidth ? oldWidth : width2; + for (let y = oldHeight; y < height2; y++) { + for (let x = 0; x < bottomLimit; x++) { + applyPixel(x, y); + } + } + } +} +function fillExpandedWithFadePixels(screen2, width2, height2, oldWidth, oldHeight, fadeInfo) { + const applyPixel = (x, y) => { + const t2 = computeFadePositionForPixel(x, y, width2, height2, fadeInfo); + const [r2 = 0, g = 0, b2 = 0, a2 = 255] = getLocalFadeColor(t2, x, y, fadeInfo); + const i2 = (y * width2 + x) * 4; + screen2.pixels[i2] = Math.max(0, Math.min(255, Math.round(r2))); + screen2.pixels[i2 + 1] = Math.max(0, Math.min(255, Math.round(g))); + screen2.pixels[i2 + 2] = Math.max(0, Math.min(255, Math.round(b2))); + screen2.pixels[i2 + 3] = Math.max(0, Math.min(255, Math.round(a2 || 255))); + }; + if (width2 > oldWidth) { + for (let y = 0; y < height2; y++) { + for (let x = oldWidth; x < width2; x++) { + applyPixel(x, y); + } + } + } + if (height2 > oldHeight) { + const bottomLimit = width2 > oldWidth ? oldWidth : width2; + for (let y = oldHeight; y < height2; y++) { + for (let x = 0; x < bottomLimit; x++) { + applyPixel(x, y); + } + } + } +} +var projectionMode = location.search.indexOf("nolabel") > -1; +var shellHTMLMode = location.search.indexOf("shellhtml") > -1; +var shellPromptLast = null; +function shellPromptSync($) { + const input3 = $.system?.prompt?.input; + if (!input3) return; + const thinking2 = !!$.system.prompt.thinking; + const state2 = (input3.text || "") + "\0" + thinking2; + if (state2 === shellPromptLast) return; + shellPromptLast = state2; + send({ + type: "prompt:text:shell", + content: { text: input3.text || "", thinking: thinking2 } + }); +} +var pieceRuns = (() => { + let current = null; + const startPerf = performance.now(); + const origConsole = { + log: console.log.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + info: console.info.bind(console) + }; + function serialize(v2) { + if (v2 === void 0) return "undefined"; + if (v2 === null) return "null"; + if (typeof v2 === "string") return v2; + if (typeof v2 === "number" || typeof v2 === "boolean") return String(v2); + if (v2 instanceof Error) return `${v2.name}: ${v2.message} +${v2.stack || ""}`; + try { + return JSON.stringify(v2); + } catch { + return String(v2); + } + } + function post(phase, pieceId, body = {}) { + try { + fetch("/api/piece-log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pieceId, phase, ...body }), + keepalive: true + }).catch(() => { + }); + } catch { + } + } + function patch(level) { + console[level] = function(...args) { + origConsole[level](...args); + if (!current) return; + current.events.push({ + level, + at: Date.now(), + elapsed: Math.round(performance.now() - current.startPerf), + message: args.map(serialize).join(" ") + }); + if (current.events.length >= 25) current.flush(); + else if (!current.flushTimer) current.flushTimer = setTimeout(current.flush, 2e3); + }; + } + ["log", "warn", "error", "info"].forEach(patch); + function complete(run, summary = {}) { + if (!run) return; + if (run.flushTimer) { + clearTimeout(run.flushTimer); + run.flushTimer = null; + } + if (run.events.length) { + const events = run.events.splice(0, run.events.length); + post("log", run.pieceId, { data: { events } }); + } + post("complete", run.pieceId, { + data: { duration: Date.now() - run.startedAt, ...summary } + }); + } + return { + start({ slug, params, colon, host, user }) { + const prev = current; + const pieceId = typeof crypto !== "undefined" && crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`; + const run = { + pieceId, + events: [], + startPerf: performance.now(), + startedAt: Date.now(), + flushTimer: null, + flush: null + }; + run.flush = () => { + if (run.flushTimer) { + clearTimeout(run.flushTimer); + run.flushTimer = null; + } + if (!run.events.length) return; + const events = run.events.splice(0, run.events.length); + post("log", run.pieceId, { data: { events } }); + }; + current = run; + post("start", pieceId, { + meta: { + slug, + params, + colon, + host, + user, + bootId: typeof self !== "undefined" ? self.acBOOT_ID || null : null, + startedAt: (/* @__PURE__ */ new Date()).toISOString(), + userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null + } + }); + run.events.push({ + level: "info", + at: Date.now(), + elapsed: 0, + message: `\u25B6 piece-run started: ${slug}` + }); + run.flushTimer = setTimeout(run.flush, 2e3); + if (prev) setTimeout(() => complete(prev), 0); + return pieceId; + }, + error(err) { + if (!current) return; + post("error", current.pieceId, { + data: { message: err?.message || String(err), stack: err?.stack || null } + }); + }, + flush() { + current?.flush?.(); + }, + get pieceId() { + return current?.pieceId || null; + } + }; +})(); +var alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +var nanoid2 = customAlphabet(alphabet, 4); +var defaults = { + boot: ({ cursor: cursor2, screen: { width: width2, height: height2 }, resolution, api }) => { + if (location.host.indexOf("botce") > -1) resolution(width2, height2, 0); + if (AestheticExtension) resolution(width2, height2, 0); + cursor2("native"); + }, + // aka Setup + sim: () => false, + // A framerate independent of rendering. + paint: ({ noise16Aesthetic: noise16Aesthetic2, noise16Sotce: noise16Sotce2, slug, wipe, ink: ink3, write, screen: screen2, net }) => { + if (typeof window !== "undefined" && window.acPACK_MODE) { + wipe("black"); + return; + } + if (!projectionMode) { + if (slug?.indexOf("wipppps") > -1) { + wipe("black"); + } else if (slug?.indexOf("botce") > -1) { + noise16Sotce2(); + } else { + noise16Aesthetic2(); + if (net.motd) { + ink3(255, 255, 255, 200).write( + net.motd, + { center: "x", y: Math.floor(screen2.height / 2) }, + void 0, + screen2.width - 18 + ); + } + if (net.loadFailureText) { + ink3("white").write( + net.loadFailureText, + { x: 6, y: 6 }, + [64, 64], + screen2.width - 6 + ); + } + } + } + }, + beat: () => false, + // Runs every bpm. + act: () => false, + // All user interaction. + leave: () => false, + // Before unload. + receive: () => false, + // Handle messages from BIOS (file drops, etc.) + preview: ({ wipe, slug }) => { + wipe(64).ink(255).write(slug, { center: "xy", size: 1 }); + }, + icon: ({ glaze, wipe, screen: screen2 }) => { + glaze({ on: false }); + wipe(70, 50, 100).ink(200, 30, 100).box(screen2.width / 2, screen2.height / 2, 48, 72, "*center"); + } +}; +var loadAfterPreamble = null; +var hotSwap = null; +var nopaint = { + leave: function leave($) { + const { store: store2, system: system2, page, screen: screen2, flatten } = $; + if (NPnoOnLeave === false) { + if (system2.nopaint.bakeOnLeave) { + page(system2.painting); + bake2($); + flatten(); + } + addUndoPainting(system2.painting, $.slug); + store2["painting"] = { + width: system2.painting.width, + height: system2.painting.height, + pixels: system2.painting.pixels + }; + store2.persist("painting", "local:db"); + system2.nopaint.syncWip($).catch((error) => console.warn("Painting WIP:", error.message)); + $commonApi.broadcastPaintingUpdate("updated", { + source: "leave", + slug: $.slug + }); + store2["painting:transform"] = { + translation: system2.nopaint.translation, + zoom: system2.nopaint.zoomLevel + }; + store2.persist("painting:transform", "local:db"); + } else { + const paintings2 = system2.nopaint.undo.paintings; + page(system2.painting).paste(paintings2[paintings2.length - 1]).page(screen2); + } + NPnoOnLeave = false; + }, + // 🥞 Bake (to the painting) + bake: function bake({ paste: paste3, system: system2 }) { + paste3(system2.nopaint.buffer); + } +}; +var undoPaintings = []; +var undoPosition = 0; +function addUndoPainting(painting2, step = "unspecified") { + if (!painting2) return; + const op = painting2.pixels; + const pixels2 = new Uint8ClampedArray(op.length); + pixels2.set(op); + if (undoPaintings.length > undoPosition + 1) { + undoPaintings.length = undoPosition + 1; + } + if (undoPaintings.length > 0) { + const lastPainting = undoPaintings[undoPaintings.length - 1]; + const eq = painting2.width === lastPainting.width && painting2.height === lastPainting.height && pixels2.every((value, index) => value === lastPainting.pixels[index]); + if (eq) { + return; + } + } + undoPaintings.push({ + pixels: pixels2, + width: painting2.width, + height: painting2.height + }); + if ($commonApi.system.nopaint.recording) { + $commonApi.system.nopaint.addToRecord({ + label: step, + painting: { + pixels: pixels2, + width: painting2.width, + height: painting2.height + } + }); + } + undoPosition = undoPaintings.length - 1; + if ($commonApi.system.nopaint.wipSync) { + clearTimeout($commonApi.system.nopaint.wipTimer); + const sync = $commonApi.system.nopaint.wipSync; + $commonApi.system.nopaint.wipTimer = setTimeout(() => { + if ($commonApi.system.nopaint.wipSync === sync) { + $commonApi.system.nopaint.syncWip().catch((error) => console.warn("Painting WIP:", error.message)); + } + }, 120); + } + const maxUndoSteps = 32; + if (undoPaintings.length > maxUndoSteps) undoPaintings.shift(); + if (debug3 && logs.painting) + console.log("\u{1F4A9} Added undo painting...", undoPaintings.length); +} +var system = null; +var pieceFPS = null; +var lastPaintTime = 0; +var lastPaintOut = void 0; +var shouldSkipPaint = false; +var pieceFrameCount = 0; +var TRANSITION_TYPE = "none"; +var pieceTransition = { + active: false, + phase: "loading", + // "loading" (slow, reach ~50%) or "revealing" (fast, finish) + overlayPixels: null, + // The captured frame that overlays on top (from outgoing piece) + generation: 0, + maxGenerations: 60, + // Total frames for full transition + width: 0, + height: 0, + // Drip-specific state (per-column y-offset) - BLINDS STYLE + dripOffsets: null, + // Int16Array of column y-offsets (positive = distance moved) + dripSpeeds: null, + // Float32Array of per-column speeds + dripDirections: null, + // Int8Array: 1 = down, -1 = up (alternating blinds) + targetOffset: 0, + // Target offset for loading phase (~50% of height) + // Bubble-wrap-specific state + blockData: null, + // Per-block data for bubble-wrap + blockSize: 8, + blocksX: 0, + blocksY: 0 +}; +var golTransition = pieceTransition; +function scaleOverlayPixels(srcPixels, srcWidth, srcHeight, dstWidth, dstHeight) { + const dstPixels = new Uint8ClampedArray(dstWidth * dstHeight * 4); + const xRatio = srcWidth / dstWidth; + const yRatio = srcHeight / dstHeight; + for (let y = 0; y < dstHeight; y++) { + for (let x = 0; x < dstWidth; x++) { + const srcX = Math.floor(x * xRatio); + const srcY = Math.floor(y * yRatio); + const srcIdx = (srcY * srcWidth + srcX) * 4; + const dstIdx = (y * dstWidth + x) * 4; + dstPixels[dstIdx] = srcPixels[srcIdx]; + dstPixels[dstIdx + 1] = srcPixels[srcIdx + 1]; + dstPixels[dstIdx + 2] = srcPixels[srcIdx + 2]; + dstPixels[dstIdx + 3] = srcPixels[srcIdx + 3]; + } + } + return dstPixels; +} +function initDripTransition(width2, height2) { + pieceTransition.dripOffsets = new Int16Array(width2); + pieceTransition.dripSpeeds = new Float32Array(width2); + pieceTransition.dripDirections = new Int8Array(width2); + pieceTransition.maxGenerations = 30; + pieceTransition.phase = "loading"; + pieceTransition.targetOffset = Math.floor(height2 * 0.35); + for (let x = 0; x < width2; x++) { + pieceTransition.dripOffsets[x] = 0; + pieceTransition.dripDirections[x] = x % 2 === 0 ? 1 : -1; + pieceTransition.dripSpeeds[x] = 4 + Math.random() * 3; + } +} +function transitionPieceLoaded() { + if (pieceTransition.active && pieceTransition.phase === "loading" && pieceTransition.dripSpeeds) { + pieceTransition.phase = "revealing"; + for (let x = 0; x < pieceTransition.dripSpeeds.length; x++) { + pieceTransition.dripSpeeds[x] = 10 + Math.random() * 8; + } + console.log("\u{1F3AC} Transition: Piece loaded! Switching to reveal phase"); + } +} +function dripStep() { + pieceTransition.generation++; + const { dripOffsets, dripSpeeds, dripDirections, height: height2, phase, targetOffset } = pieceTransition; + let allDone = true; + for (let x = 0; x < dripOffsets.length; x++) { + const currentOffset = dripOffsets[x]; + if (phase === "loading") { + if (currentOffset < targetOffset) { + dripOffsets[x] += dripSpeeds[x]; + if (dripOffsets[x] > targetOffset) { + dripOffsets[x] = targetOffset; + } + allDone = false; + } + } else { + if (currentOffset < height2) { + dripOffsets[x] += dripSpeeds[x]; + dripSpeeds[x] += 0.3; + allDone = false; + } + } + } + if (phase === "loading") { + return true; + } + return !allDone && pieceTransition.generation < pieceTransition.maxGenerations; +} +function applyDripOverlay(screenPixels) { + const { overlayPixels, dripOffsets, dripDirections, width: width2, height: height2 } = pieceTransition; + if (!overlayPixels || !screenPixels || !dripOffsets || !dripDirections) return; + for (let x = 0; x < width2; x++) { + const offset = Math.max(0, dripOffsets[x] | 0); + const direction = dripDirections[x]; + if (offset >= height2) continue; + const visibleRows = height2 - offset; + if (direction > 0) { + for (let y = 0; y < visibleRows; y++) { + const srcY = y; + const dstY = y + offset; + const srcIdx = (srcY * width2 + x) * 4; + const dstIdx = (dstY * width2 + x) * 4; + screenPixels[dstIdx] = overlayPixels[srcIdx]; + screenPixels[dstIdx + 1] = overlayPixels[srcIdx + 1]; + screenPixels[dstIdx + 2] = overlayPixels[srcIdx + 2]; + screenPixels[dstIdx + 3] = overlayPixels[srcIdx + 3]; + } + } else { + for (let y = 0; y < visibleRows; y++) { + const srcY = offset + y; + const dstY = y; + const srcIdx = (srcY * width2 + x) * 4; + const dstIdx = (dstY * width2 + x) * 4; + screenPixels[dstIdx] = overlayPixels[srcIdx]; + screenPixels[dstIdx + 1] = overlayPixels[srcIdx + 1]; + screenPixels[dstIdx + 2] = overlayPixels[srcIdx + 2]; + screenPixels[dstIdx + 3] = overlayPixels[srcIdx + 3]; + } + } + } +} +function initBubbleWrapCells(width2, height2) { + pieceTransition.blockSize = 1; + pieceTransition.blocksX = width2; + pieceTransition.blocksY = height2; + pieceTransition.blockData = null; + pieceTransition.maxGenerations = 30; +} +function bubbleWrapStep(screenPixels) { + pieceTransition.generation++; + return pieceTransition.generation < pieceTransition.maxGenerations; +} +function applyBubbleWrapOverlay(screenPixels) { + const { overlayPixels, width: width2, height: height2, generation, maxGenerations } = pieceTransition; + if (!overlayPixels || !screenPixels) return; + const progress = generation / maxGenerations; + const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2; + const t2 = generation * 0.1; + const dispPeak = Math.sin(progress * Math.PI); + const maxDisp = dispPeak * 16; + const bs = 4; + for (let y = 0; y < height2; y++) { + const rowOff = y * width2; + const qy = (y / bs | 0) * bs; + for (let x = 0; x < width2; x++) { + const i2 = (rowOff + x) * 4; + const qx = (x / bs | 0) * bs; + const n1 = Math.sin(qx * 0.05 + t2) * Math.cos(qy * 0.04 + t2 * 0.7); + const n2 = Math.sin(qx * 0.09 + qy * 0.07 - t2 * 0.6) * 0.5; + const n3 = Math.cos(qx * 0.02 - qy * 0.03 + t2 * 0.4) * 0.4; + const noise = (n1 + n2 + n3) * 0.5 + 0.5; + const threshold = eased * 1.5 - 0.25; + const edge = 0.3; + let blend3 = (threshold - noise * 0.85) / edge; + blend3 = blend3 < 0 ? 0 : blend3 > 1 ? 1 : blend3; + const swirl = Math.sin(qx * 0.06 + qy * 0.04 + t2 * 1.5) * maxDisp; + const churn = Math.cos(qx * 0.05 - qy * 0.07 + t2 * 1.2) * maxDisp; + let ox = x + swirl | 0; + let oy = y + churn | 0; + ox = ox < 0 ? 0 : ox >= width2 ? width2 - 1 : ox; + oy = oy < 0 ? 0 : oy >= height2 ? height2 - 1 : oy; + const oi = (oy * width2 + ox) * 4; + let nx = x - churn * 0.7 | 0; + let ny = y + swirl * 0.6 | 0; + nx = nx < 0 ? 0 : nx >= width2 ? width2 - 1 : nx; + ny = ny < 0 ? 0 : ny >= height2 ? height2 - 1 : ny; + const ni = (ny * width2 + nx) * 4; + const oldR = overlayPixels[oi]; + const oldG = overlayPixels[oi + 1]; + const oldB = overlayPixels[oi + 2]; + const newR = screenPixels[ni]; + const newG = screenPixels[ni + 1]; + const newB = screenPixels[ni + 2]; + const inv = 1 - blend3; + let r2 = oldR * inv + newR * blend3; + let g = oldG * inv + newG * blend3; + let b2 = oldB * inv + newB * blend3; + if (dispPeak > 0.5 && (qx + qy + generation & 7) === 0) { + const tmp = r2; + r2 = g; + g = b2; + b2 = tmp; + } + screenPixels[i2] = r2; + screenPixels[i2 + 1] = g; + screenPixels[i2 + 2] = b2; + } + } +} +function initGOLCells(width2, height2) { + if (TRANSITION_TYPE === "none") return; + if (TRANSITION_TYPE === "drip") { + initDripTransition(width2, height2); + } else { + initBubbleWrapCells(width2, height2); + } +} +function golStep(screenPixels) { + if (TRANSITION_TYPE === "none") return false; + if (TRANSITION_TYPE === "drip") { + return dripStep(); + } else { + return bubbleWrapStep(screenPixels); + } +} +function applyGOLOverlay(screenPixels) { + if (TRANSITION_TYPE === "drip") { + applyDripOverlay(screenPixels); + } else { + applyBubbleWrapOverlay(screenPixels); + } +} +var boot = defaults.boot; +var sim = defaults.sim; +var paint = defaults.paint; +var beat = defaults.beat; +var brush; +var lift; +var filter; +var act = defaults.act; +var leave2 = defaults.leave; +var receive = defaults.receive; +var preview = defaults.preview; +var icon = defaults.icon; +var bake2; +var leaving = false; +var leaveLoad; +var previewMode = false; +var firstPreviewOrIcon = true; +var iconMode = false; +var previewOrIconMode; +var hideLabel = false; +var hideLabelForFrame = false; +function toggleHUDVisibility(isDoubleTap = false, skipSound = false) { + const currentTime = performance.now(); + if (hudAnimationState.animating) { + const elapsed = currentTime - hudAnimationState.startTime; + const progress = Math.min(elapsed / hudAnimationState.duration, 1); + hudAnimationState.visible = !hudAnimationState.visible; + if (hudAnimationState.qrFullscreen) { + hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible; + } + const remainingProgress = 1 - progress; + hudAnimationState.startTime = currentTime - remainingProgress * hudAnimationState.duration; + if (isDoubleTap && !hudAnimationState.visible) { + hudAnimationState.animating = false; + hudAnimationState.visible = true; + hudAnimationState.opacity = 1; + hudAnimationState.slideOffset = { x: 0, y: 0 }; + hudAnimationState.qrSlideOffset = { x: 0, y: 0 }; + if (hudAnimationState.qrFullscreen) { + hudAnimationState.cornersVisibleBeforeFullscreen = true; + } + } + if (!skipSound) { + $commonApi.sound.synth({ + type: "sine", + tone: hudAnimationState.visible ? 600 : 400, + duration: 0.15, + attack: 0.1, + decay: 0.8, + volume: 0.2 + }); + } + } else { + hudAnimationState.animating = true; + hudAnimationState.startTime = currentTime; + hudAnimationState.visible = !hudAnimationState.visible; + if (hudAnimationState.qrFullscreen) { + hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible; + } + if (!skipSound) { + $commonApi.sound.synth({ + type: "sine", + tone: hudAnimationState.visible ? 600 : 400, + duration: 0.15, + attack: 0.1, + decay: 0.8, + volume: 0.2 + }); + } + } +} +function toggleQRFullscreen() { + try { + const sourceCode = currentText || currentHUDTxt; + console.log("\u{1F50D} [QR Toggle] sourceCode:", sourceCode, "currentPath:", currentPath); + const isInlineKidlispPiece = currentPath && isKidlispSource(currentPath) && !currentPath.endsWith(".lisp") || currentPath === "(...)" || sourceCode && sourceCode.startsWith("$") || currentPath && currentPath.includes("/disks/$") || sourceCode && isKidlispSource(sourceCode); + console.log("\u{1F50D} [QR Toggle] isInlineKidlispPiece:", isInlineKidlispPiece); + if (!isInlineKidlispPiece) { + console.log("\u26A0\uFE0F [QR Toggle] Not a KidLisp piece, skipping QR fullscreen toggle"); + return; + } + if (!hudAnimationState.qrFullscreen) { + console.log("\u2705 [QR Toggle] Enabling fullscreen mode"); + hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible; + if (hudAnimationState.visible) { + if (hudAnimationState.animating) { + const currentTime = performance.now(); + const elapsed = currentTime - hudAnimationState.startTime; + const remaining = Math.max(0, hudAnimationState.duration - elapsed); + hudAnimationState.startTime = currentTime - remaining; + } else { + hudAnimationState.animating = true; + hudAnimationState.startTime = performance.now(); + } + hudAnimationState.visible = false; + } + hudAnimationState.qrFullscreen = true; + send({ type: "button:hitbox:remove", content: "qr-corner" }); + } else { + console.log("\u2705 [QR Toggle] Disabling fullscreen mode"); + hudAnimationState.qrFullscreen = false; + send({ type: "button:hitbox:remove", content: "qr-fullscreen" }); + if (hudAnimationState.cornersVisibleBeforeFullscreen && !hudAnimationState.visible) { + if (hudAnimationState.animating) { + const currentTime = performance.now(); + const elapsed = currentTime - hudAnimationState.startTime; + const remaining = Math.max(0, hudAnimationState.duration - elapsed); + hudAnimationState.startTime = currentTime - remaining; + } else { + hudAnimationState.animating = true; + hudAnimationState.startTime = performance.now(); + } + hudAnimationState.visible = true; + } + } + if ($commonApi?.sound?.synth) { + $commonApi.sound.synth({ + type: "sine", + tone: hudAnimationState.qrFullscreen ? 500 : 250, + duration: 0.12, + attack: 0.1, + decay: 0.9, + volume: 0.25 + }); + } + } catch (err) { + console.error("\u274C [QR Toggle] Error toggling QR fullscreen:", err); + } +} +var _packModeHudVisible = getPackMode() ? typeof window !== "undefined" && window.acPACK_COLOPHON?.piece?.isKidLisp === false : true; +var hudAnimationState = { + visible: _packModeHudVisible, + animating: false, + startTime: 0, + duration: 500, + // 500ms animation + opacity: 1, + slideOffset: { x: 0, y: 0 }, + // HUD label offset (slides to top-left) + qrSlideOffset: { x: 0, y: 0 }, + // QR overlay offset (slides to bottom-right) + labelWidth: 120, + // HUD label width - updated when HUD is drawn + labelHeight: 40, + // HUD label height - updated when HUD is drawn + qrSize: 80, + // QR overlay size for bounding box animations + lastTabTime: 0, + // Track last tab press for double-tap detection + qrFullscreen: false, + // Track if QR code is in fullscreen mode + cornersVisibleBeforeFullscreen: true + // Remember corner state before fullscreen QR +}; +var cachedClockCode = null; +var module3; +var loadedModule; +var lastPaintingHash = null; +var paintingChangeCheckInterval = null; +var frameBasedMonitoring = false; +function enableFrameBasedMonitoring() { + if (frameBasedMonitoring) return; + frameBasedMonitoring = true; + let lastDimensions = null; + function checkPaintingChanges() { + if ($commonApi.system?.painting && !$commonApi._processingBroadcast) { + const currentHash2 = generatePaintingHash($commonApi.system.painting); + const currentDimensions = { + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height + }; + const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); + const nopaintBuffer = $commonApi.system?.nopaint?.buffer; + if (lastPaintingHash !== null && currentHash2 !== lastPaintingHash) { + const isDimensionChange = lastDimensions && (lastDimensions.width !== currentDimensions.width || lastDimensions.height !== currentDimensions.height); + if (isNopaintActive) { + console.log(`\u{1F6AB} SKIPPING broadcast during nopaint operation to prevent live preview interference`); + } else { + $commonApi.broadcastPaintingUpdateImmediate("frame_update", { + source: isDimensionChange ? "resize" : "frame_monitor", + hash: currentHash2.substr(0, 8) + }); + } + } + lastPaintingHash = currentHash2; + lastDimensions = currentDimensions; + } + if (frameBasedMonitoring) { + requestAnimationFrame(checkPaintingChanges); + } + } + requestAnimationFrame(checkPaintingChanges); +} +var lastBroadcastTime = 0; +var broadcastThrottleDelay = 100; +var pieceMetadata = null; +var currentPath; +var currentHost; +var currentSearch; +var currentColon; +var currentParams; +var currentHash; +var currentText; +var currentCode; +var currentHUDTxt; +var currentHUDPlainTxt; +var currentOriginalCodeId; +var currentHUDTextColor; +var currentHUDStatusColor = "red"; +var currentHUDButton; +var currentHUDButtonActive = false; +var currentHUDButtonDirectTouch = false; +var currentHUDHovered = false; +var currentHUDHoverAmount = 0; +var currentHUDHoverClock = 0; +var currentHUDScrub = 0; +var currentHUDScrubReadyState = null; +var currentHUDLabelFontName; +var currentHUDLabelBlockWidth = tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH; +var currentHUDLabelBlockHeight = tf?.blockHeight ?? DEFAULT_TYPEFACE_BLOCK_HEIGHT; +var currentHUDShareWidth = (tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH) * "share ".length; +var currentHUDEditWidth = tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH; +var currentHUDLabelMeasuredWidth = 0; +var currentHUDTextHeight = 0; +var currentHUDTextBoxWidth = 0; +var currentHUDTextBoxHeight = 0; +var currentHUDLeftPad = 0; +var currentHUDOffset; +var currentHUDQR = null; +var currentHUDQRCells = null; +var currentHUDAuthor = null; +var currentHUDHits = null; +var currentHUDSuperscript = null; +var forceTinyHudLabel = false; +var qrOverlayCache = /* @__PURE__ */ new Map(); +var globalUpdateReady = false; +var globalVersionInfo = null; +var globalRecentCommits = []; +var updatePollStarted = false; +var updatePollController = null; +var updateBadgeBoxScreen = null; +var updateBadgeHitboxRegistered = false; +var globalAutoReload = false; +var autoUpdateForFrame = false; +var devIdentity = null; +var remoteLogQueue = []; +var remoteLogSocket = null; +var MAX_REMOTE_LOG_QUEUE = 50; +var diagnostics = new Diagnostics({ + send: (type, content) => socket?.send(type, content) +}); +diagnostics.watch(globalThis); +function setupRemoteLogging() { + const originalConsole = { + log: console.log.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console) + }; + function sendRemoteLog(level, args) { + diagnostics.note(level, serializeArgs(args)); + if (!devIdentity || !remoteLogSocket?.connected) { + if (remoteLogQueue.length < MAX_REMOTE_LOG_QUEUE) { + remoteLogQueue.push({ level, args: serializeArgs(args), time: Date.now() }); + } + return; + } + try { + const logData = { + level, + args: serializeArgs(args), + deviceName: devIdentity.name, + connectionId: devIdentity.connectionId, + time: Date.now() + }; + remoteLogSocket.send("dev:log", logData); + } catch (e2) { + } + } + function serializeArgs(args) { + return args.map((arg) => { + if (arg === null) return "null"; + if (arg === void 0) return "undefined"; + if (typeof arg === "string") return arg; + if (typeof arg === "number" || typeof arg === "boolean") return String(arg); + if (arg instanceof Error) return `${arg.name}: ${arg.message} +${arg.stack}`; + try { + return JSON.stringify(arg, null, 2); + } catch (e2) { + return String(arg); + } + }); + } + console.log = (...args) => { + originalConsole.log(...args); + sendRemoteLog("log", args); + }; + console.warn = (...args) => { + originalConsole.warn(...args); + sendRemoteLog("warn", args); + }; + console.error = (...args) => { + originalConsole.error(...args); + sendRemoteLog("error", args); + }; + if (typeof window !== "undefined") { + window.addEventListener("error", (event) => { + sendRemoteLog("error", [`Uncaught: ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]); + }); + window.addEventListener("unhandledrejection", (event) => { + sendRemoteLog("error", [`Unhandled Promise: ${event.reason}`]); + }); + } +} +function flushRemoteLogQueue() { + if (!devIdentity || !remoteLogSocket?.connected) return; + while (remoteLogQueue.length > 0) { + const log3 = remoteLogQueue.shift(); + try { + remoteLogSocket.send("dev:log", { + level: log3.level, + args: log3.args, + deviceName: devIdentity.name, + connectionId: devIdentity.connectionId, + time: log3.time, + queued: true + }); + } catch (e2) { + break; + } + } +} +var lastMatrixChunkyWriteDiagnosticLog = null; +function isQROverlayCacheDisabled() { + try { + return typeof window !== "undefined" && window.acDISABLE_QR_OVERLAY_CACHE; + } catch (e2) { + return false; + } +} +if (typeof window !== "undefined") { + window.qrOverlayCache = qrOverlayCache; + if (isQROverlayCacheDisabled()) { + qrOverlayCache.clear(); + } +} +function stripColorCodes4(str7) { + return stripColorCodes(str7); +} +function textContainsColorCodes(str7) { + return hasColorCodes(str7); +} +function hasKidLispMarkers(text) { + if (!text) return false; + return text.includes("ink ") || text.includes("line ") || text.includes("box ") || text.includes("circle ") || text.includes("spin ") || text.includes("(") || textContainsColorCodes(text) || /\d+s\.\.\./.test(text) || /\?\s/.test(text); +} +function detectKidLispPiece({ currentPath: currentPath2, currentHUDTxt: currentHUDTxt2, currentText: currentText2, cleanText }) { + const sourceCode = currentText2 || currentHUDTxt2; + return currentPath2 && isKidlispSource(currentPath2) && !currentPath2.endsWith(".lisp") || currentPath2 === "(...)" || sourceCode && sourceCode.startsWith("$") || currentPath2 && (currentPath2.includes("/disks/$") || currentPath2.includes("$")) || sourceCode && isKidlispSource && isKidlispSource(sourceCode) || hasKidLispMarkers(currentHUDTxt2) || textContainsColorCodes(cleanText) || hasKidLispMarkers(sourceCode); +} +function detectEmbeddedKidLisp({ currentPath: currentPath2, currentHUDTxt: currentHUDTxt2, currentText: currentText2 }) { + const sourceCode = currentText2 || currentHUDTxt2; + return currentPath2 === "(...)" || sourceCode && sourceCode.startsWith("$") || currentPath2 && currentPath2.includes("/disks/$") || currentPath2 && currentPath2.includes("$") && !currentPath2.endsWith(".lisp"); +} +function updateHUDStatus() { + if (udp.connected && socket?.connected) { + currentHUDStatusColor = "lime"; + } else if (udp.connected || socket?.connected) { + currentHUDStatusColor = "orange"; + } else { + currentHUDStatusColor = "red"; + } +} +var loading = false; +var loadingStartTime = null; +var LOADING_TIMEOUT_MS = 1e4; +function checkLoadingTimeout() { + if (loading && loadingStartTime && Date.now() - loadingStartTime > LOADING_TIMEOUT_MS) { + console.warn("\u{1F534} Loading timeout exceeded - forcing loading reset"); + loading = false; + loadingStartTime = null; + return true; + } + return false; +} +var reframe; +var pendingPieceReload = null; +var sfxProgressReceivers = {}; +var sfxSampleReceivers = {}; +var sfxKillReceivers = {}; +var sfxDurationReceivers = {}; +var sfxDurations = {}; +var $sampleCount = 0n; +var signals = []; +var actAlerts = []; +var reframed = false; +var formReframing = false; +var paintings = {}; +if (typeof window !== "undefined") { + window.acPendingImages = () => Object.values(paintings).filter((v2) => v2 === "fetching").length; +} +var imageCache = { + // In-memory cache for fast access (RAM) + memory: /* @__PURE__ */ new Map(), + // url -> ImageBitmap + // IndexedDB persistence reference (set during boot) + store: null, + // Initialize with store reference for persistent caching + init(storeRef) { + this.store = storeRef; + }, + // Get from memory cache + get(url) { + return this.memory.get(url); + }, + // Check if image is cached in memory + has(url) { + return this.memory.has(url); + }, + // Save to both memory and persistent storage + async set(url, imageBitmap) { + this.memory.set(url, imageBitmap); + if (this.store && imageBitmap) { + try { + let pixelData, imgWidth, imgHeight; + if (imageBitmap.pixels) { + imgWidth = imageBitmap.width; + imgHeight = imageBitmap.height; + pixelData = imageBitmap.pixels; + } else { + const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(imageBitmap.width, imageBitmap.height) : document.createElement("canvas"); + canvas.width = imageBitmap.width; + canvas.height = imageBitmap.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(imageBitmap, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + imgWidth = imageData.width; + imgHeight = imageData.height; + pixelData = imageData.data; + } + this.store[`image-cache:${url}`] = { + width: imgWidth, + height: imgHeight, + data: Array.from(pixelData), + cached: Date.now(), + version: 1 + }; + this.store.persist(`image-cache:${url}`, "local:db"); + } catch (error) { + console.warn(`Failed to save image to persistent cache: ${url}`, error); + } + } + }, + // Load from persistent storage (IndexedDB) + async loadFromPersistent(url) { + if (!this.store) return null; + try { + const data = await this.store.retrieve(`image-cache:${url}`, "local:db"); + if (data && data.data && data.width && data.height) { + const imageData = new ImageData( + new Uint8ClampedArray(data.data), + data.width, + data.height + ); + const imageBitmap = await createImageBitmap(imageData); + this.memory.set(url, imageBitmap); + return imageBitmap; + } + } catch (error) { + console.warn(`Failed to load image from persistent cache: ${url}`, error); + } + return null; + }, + // Clear all caches + clear() { + this.memory.clear(); + }, + // Check if URL is from aesthetic.computer media backend (safe to cache) + isCacheable(url) { + if (!url) return false; + const urlStr = url.toString().toLowerCase(); + return urlStr.includes("aesthetic.computer") || urlStr.includes("art.aesthetic.computer") || urlStr.includes("/media/@") || // User paintings + urlStr.match(/\/@[\w-]+\/\d+/); + } +}; +var screen; +var currentDisplay; +var hdCanvas = null; +var hdContext = null; +var hdPainted = false; +var hdVisible = false; +var hdPixelRatio = 1; +var HD_MAX_PIXELS = 4096 * 2304; +function hd() { + if (!screen || typeof OffscreenCanvas === "undefined") return null; + if (PREVIEW_OR_ICON) return null; + const sub6 = currentDisplay?.subdivisions || 2; + const dpr = currentDisplay?.pixelRatio || hdPixelRatio; + const maxScale = Math.sqrt(HD_MAX_PIXELS / (screen.width * screen.height)); + const scale7 = Math.max(1, Math.min(sub6 * dpr, maxScale)); + const w = Math.round(screen.width * scale7); + const h = Math.round(screen.height * scale7); + if (!hdCanvas || hdCanvas.width !== w || hdCanvas.height !== h) { + hdCanvas = new OffscreenCanvas(w, h); + hdContext = hdCanvas.getContext("2d"); + } + hdContext.setTransform(scale7, 0, 0, scale7, 0, 0); + hdPainted = true; + return { ctx: hdContext, width: screen.width, height: screen.height, scale: scale7 }; +} +function hdUnload() { + if (!hdCanvas) return; + hdCanvas = hdContext = null; + hdPainted = false; + hdVisible = false; + send({ type: "hd:clear" }); +} +var cursorCode; +var pieceHistoryIndex = -1; +var paintCount = 0n; +var simCount = 0n; +var booted = false; +var noPaint = false; +var labelBack = false; +var hiccupTimeout; +function redirectIfBrandedDomain() { + const { hostname } = getSafeUrlParts(); + if (!hostname) return false; + if (hostname === "notepat.com" || hostname === "www.notepat.com") { + send({ type: "web", content: { url: "https://aesthetic.computer", blank: false } }); + return true; + } + if (hostname === "laklok.com" || hostname === "www.laklok.com") { + send({ type: "web", content: { url: "https://aesthetic.computer", blank: false } }); + return true; + } + return false; +} +var storeRetrievalResolutions = {}; +var storeDeletionResolutions = {}; +var usbDeviceListResolution = null; +var usbFlashResolution = null; +var socket; +var socketStartDelay; +var chatDebug = location.host === "local.aesthetic.computer" || location.host === "localhost:8888" || location.host === "aesthetic.local:8888"; +var chatClient = new Chat(chatDebug, send); +var udp = { + send: (type, content) => { + send({ type: "udp:send", content: { type, content } }); + }, + receive: ({ type, content }) => { + if (type === "fairy:point" && visible) { + const isKidlisp = detectKidLispPiece({ currentPath, currentHUDTxt, currentText }) || currentPath && currentPath.endsWith(".lisp"); + if (!isKidlisp) { + fairies.push({ x: content.x, y: content.y }); + } + return; + } + udpReceive?.(type, content); + }, + kill: (outageSeconds) => { + udp.connected = false; + send({ type: "udp:disconnect", content: { outageSeconds } }); + }, + connected: false +}; +var udpReceive = void 0; +var scream = null; +var screaming = false; +var screamingTimer; +var fairies = []; +var glazeEnabled = false; +function darkMode(enabled) { + if (enabled === "default") { + darkMode($commonApi.dark || false); + store.delete("dark-mode"); + actAlerts.push($commonApi.dark ? "dark-mode" : "light-mode"); + return $commonApi.dark; + } else { + store["dark-mode"] = enabled; + store.persist("dark-mode"); + $commonApi.dark = enabled; + actAlerts.push($commonApi.dark ? "dark-mode" : "light-mode"); + return enabled; + } +} +var store = { + persist: function(key, method = "local") { + send({ + type: "store:persist", + content: { + key, + data: this[key], + method + } + }); + }, + retrieve: function(key, method = "local") { + const promise = new Promise((resolve) => { + storeRetrievalResolutions[key] = resolve; + }); + send({ type: "store:retrieve", content: { key, method } }); + return promise; + }, + delete: function(key, method = "local") { + delete store[key]; + const promise = new Promise((resolve) => { + storeDeletionResolutions[key] = resolve; + }); + send({ + type: "store:delete", + content: { + key, + method + } + }); + return promise; + } +}; +var fileImport; +var serverUpload; +var serverUploadProgressReporter; +var zipCreation; +var authorizationRequests = []; +var fileOpenRequest; +var fileEncodeRequest; +var gpuResponse; +var web3Response; +var tezosConnectResponse; +var tezosDisconnectResponse; +var tezosAddressResponse; +var tezosSignResponse; +var tezosCallResponse; +var activeVideo; +var videoDeviceCount = 0; +var lastActiveVideo; +var videoSwitching = false; +var preloadPromises = {}; +var inFocus; +var loadFailure; +var pieceCodeCache = /* @__PURE__ */ new Map(); +var durationStartTime = null; +var durationTotal = null; +var durationProgress = 0; +var durationCompleted = false; +var durationBlinkState = false; +var pageLoadTime = performance.now(); +var wiggleAngle = 0; +var NPnoOnLeave = false; +var Recorder = class { + printProgress = 0; + presentProgress = 0; + printing = false; + // Set by a callback from `bios`. + printed = false; + // " + recording = false; + // " + rollingCallback; + recorded = false; + // " + presenting = false; + // " + playing = false; + // " + cutCallback; + printCallback; + framesCallback; + loadCallback; + tapeTimerStart; + tapeProgress = 0; + tapeTimerDuration; + tapeFrameMode = false; + // Whether we're recording based on frames instead of time + tapeFrameStart = 0; + // Starting frame count + tapeFrameTarget = 0; + // Target number of frames to record + videoOnLeave = false; + constructor() { + } + tapeTimerSet(durationOrFrames, time, isFrameMode = false) { + if (isFrameMode) { + this.tapeFrameMode = true; + this.tapeFrameStart = Number($commonApi.paintCount || 0n); + this.tapeFrameTarget = durationOrFrames; + this.tapeTimerStart = null; + this.tapeTimerDuration = null; + log.tape.log(`Starting frame-based recording: ${durationOrFrames} frames`); + if (this.failsafeTimeout) { + clearTimeout(this.failsafeTimeout); + } + this.failsafeTimeout = setTimeout(() => { + log.tape.warn("Frame-based failsafe triggered! Recording may have stalled."); + this.tapeProgress = 0; + this.tapeFrameMode = false; + this.tapeFrameStart = 0; + this.tapeFrameTarget = 0; + if (typeof this.cut === "function") { + this.cut(() => { + $commonApi.jump("video"); + }); + } else { + log.tape.warn("Cut function not available in failsafe"); + $commonApi.jump("video"); + } + this.failsafeTimeout = null; + }, Math.max(3e4, durationOrFrames * 1e3)); + } else { + this.tapeFrameMode = false; + this.tapeTimerStart = time; + this.tapeTimerDuration = durationOrFrames; + if (this.failsafeTimeout) { + clearTimeout(this.failsafeTimeout); + } + this.failsafeTimeout = setTimeout(() => { + log.tape.warn("Failsafe timer triggered! Normal timer may have failed."); + if (this.tapeTimerDuration && this.tapeTimerStart) { + this.tapeProgress = 0; + this.tapeTimerStart = null; + this.tapeTimerDuration = null; + if (typeof this.cut === "function") { + this.cut(() => { + $commonApi.jump("video"); + }); + } else { + console.warn(`\u{1F3AC} \u26A0\uFE0F Cut function not available in failsafe, manual jump to video`); + $commonApi.jump("video"); + } + } + this.failsafeTimeout = null; + }, (durationOrFrames + 1) * 1e3); + } + } + tapeTimerStep({ needsPaint, sound: { time } }) { + if (this.tapeFrameMode) { + if (!this.tapeFrameTarget) return; + const currentFrame = Number($commonApi.paintCount || 0n); + const framesPassed = currentFrame - this.tapeFrameStart; + this.tapeProgress = framesPassed / this.tapeFrameTarget; + needsPaint(); + if (framesPassed >= this.tapeFrameTarget) { + if (this.failsafeTimeout) { + clearTimeout(this.failsafeTimeout); + this.failsafeTimeout = null; + } + this.tapeProgress = 0; + this.tapeFrameMode = false; + this.tapeFrameStart = 0; + this.tapeFrameTarget = 0; + log.tape.success(`Frame-based recording complete: ${framesPassed} frames`); + if (typeof this.cut === "function") { + this.cut(() => { + $commonApi.jump("video"); + }); + } else { + log.tape.warn("Cut function not available"); + $commonApi.jump("video"); + } + } + } else { + if (!this.tapeTimerDuration) return; + this.tapeProgress = (time - this.tapeTimerStart) / this.tapeTimerDuration; + needsPaint(); + const secondsOver = this.tapeProgress * this.tapeTimerDuration - this.tapeTimerDuration; + if (this.tapeProgress >= 1 && secondsOver > 0.15) { + if (this.failsafeTimeout) { + clearTimeout(this.failsafeTimeout); + this.failsafeTimeout = null; + } + this.tapeProgress = 0; + this.tapeTimerStart = null; + this.tapeTimerDuration = null; + if (typeof this.cut === "function") { + this.cut(() => { + $commonApi.jump("video"); + }); + } else { + log.tape.warn("Cut function not available"); + $commonApi.jump("video"); + } + } + } + } + slate() { + send({ type: "recorder:slate" }); + $commonApi.rec.recording = false; + $commonApi.rec.recorded = false; + $commonApi.rec.printed = false; + $commonApi.rec.printProgress = 0; + $commonApi.rec.cleanMode = false; + this.tapeFrameMode = false; + this.tapeFrameStart = 0; + this.tapeFrameTarget = 0; + } + rolling(opts, cb) { + send({ type: "recorder:rolling", content: opts }); + this.rollingCallback = cb; + } + cut(cb) { + $commonApi.rec.cutCallback = cb; + this.tapeProgress = 0; + this.tapeTimerStart = null; + this.tapeTimerDuration = null; + this.tapeFrameMode = false; + this.tapeFrameStart = 0; + this.tapeFrameTarget = 0; + send({ type: "signal", content: "recorder:cut" }); + } + print(cb) { + $commonApi.rec.printCallback = cb; + send({ type: "recorder:print" }); + } + present(noplay = false) { + this.presentProgress = 0; + send({ type: "recorder:present", content: { noplay } }); + } + unpresent() { + send({ type: "recorder:unpresent" }); + } + play() { + send({ type: "recorder:present:play" }); + } + pause() { + send({ type: "recorder:present:pause" }); + } + requestFrames(cb) { + $commonApi.rec.framesCallback = cb; + send({ type: "recorder:request-frames" }); + } +}; +var cachedAPI; +var hourGlasses = []; +async function uploadPainting(picture, progress, handle2, filename) { + if (typeof picture === "string") { + return { url: picture }; + } else { + filename ||= `painting-${timestamp()}.png`; + try { + const data = await $commonApi.upload( + filename, + { + pixels: picture.pixels, + width: picture.width, + height: picture.height + }, + (p) => { + console.log("Painting upload progress:", p); + progress?.(p); + }, + !handle2 ? "art" : void 0 + // Store in temporary if no HANDLE. + ); + console.log("\u{1FA84} Painting uploaded:", data.slug, data.ext, data.url); + return data; + } catch (err) { + console.error("\u{1FA84} Painting upload failed:", err); + } + } +} +function isLeaving(set7) { + if (set7 === true || set7 === false) leaving = set7; + return leaving; +} +var docs; +var baseTime = Date.now(); +var baseReal = Date.now(); +var clockFetching = false; +var lastServerTime = void 0; +var clockOffset = 0; +var Robo = class { + constructor() { + this.currentAPI = null; + this.pen = { x: 0, y: 0, px: 0, py: 0, pressure: 0.5, delta: { x: 0, y: 0 }, device: "robot" }; + } + setAPI(api) { + this.currentAPI = api; + } + // Send a synthetic event. The event payload carries its own coordinates — + // downstream handlers (nopaint updateBrush etc.) should prefer these over + // $api.pen for events with device === "robot". + sendEvent(eventType, coordinates = {}) { + if (!this.currentAPI) { + console.warn("\u{1F916} Robo: No API context available"); + return; + } + const x = coordinates.x ?? 0; + const y = coordinates.y ?? 0; + const px = coordinates.px ?? this.pen.x ?? 0; + const py = coordinates.py ?? this.pen.y ?? 0; + const pressure = coordinates.pressure ?? 0.5; + this.pen.px = this.pen.x; + this.pen.py = this.pen.y; + this.pen.x = x; + this.pen.y = y; + this.pen.pressure = pressure; + this.pen.delta.x = x - px; + this.pen.delta.y = y - py; + this.pen.device = "robot"; + const eventPayload = { + device: "robot", + type: eventType, + x, + y, + px, + py, + pressure, + delta: { x: x - px, y: y - py } + }; + try { + this.currentAPI.act(eventType, eventPayload); + } catch (error) { + console.error("\u{1F916} Robo: Error dispatching event:", error); + } + } + touch(x, y) { + this.sendEvent("touch:1", { x, y, pressure: 0.5 }); + } + draw(x, y, px, py) { + this.sendEvent("draw:1", { x, y, px, py, pressure: 0.5 }); + } + lift(x, y) { + this.sendEvent("lift:1", { x, y, pressure: 0.5 }); + } + act(eventType, coordinates) { + this.sendEvent(eventType, coordinates); + } +}; +var persistentDawState = { + bpm: null, + playing: null, + time: null, + sampleRate: null +}; +var persistentSpreadnobState = { + note: null, + target: null, + value: null, + active: null, + min: null, + max: null +}; +var $commonApi = { + lisp: kidlisp_exports, + // A global reference to the `kidlisp` evalurator. + undef: void 0, + // A global api shorthand for undefined. + hd, + // 🖼️ Native-resolution Canvas2D layer (see the hd() definition above). + clock: { + offset: function() { + if (clockFetching) return; + if (getPackMode() || typeof window !== "undefined" && window.acSPIDER) { + clockFetching = false; + return; + } + clockFetching = true; + const t0 = Date.now(); + fetch("/api/clock").then((response) => { + if (!response.ok) { + return response.text().then((err) => { + clockFetching = false; + throw new Error( + `Failed to fetch offset: ${response.status} ${err}` + ); + }); + } + return response.text().then((serverTimeISO) => { + const t1 = Date.now(); + const serverTime = new Date(serverTimeISO).getTime(); + const rtt = t1 - t0; + const approxClientMidpoint = t0 + rtt / 2; + const targetOffset = serverTime - approxClientMidpoint; + const blendFactor = 0.25; + clockOffset += (targetOffset - clockOffset) * blendFactor; + baseTime = Date.now() + clockOffset; + baseReal = Date.now(); + lastServerTime = serverTime; + clockFetching = false; + }); + }).catch((err) => { + console.error("Clock:", err); + clockFetching = false; + }); + }, + resync: function() { + $commonApi.clock.offset(); + }, + time: function() { + return new Date(baseTime + (Date.now() - baseReal)); + } + }, + // Enable Pointer Lock + penLock: () => { + send({ type: "pen:lock" }); + }, + // Send a message to BIOS (for effect control, etc.) + send: (msg) => { + send(msg); + }, + chat: chatClient.system, + dark: void 0, + // If we are in dark mode. + theme: { + light: { + wipeBG: 150, + wipeNum: 200 + }, + dark: { + wipeBG: 32, + wipeNum: 64 + } + }, + glaze: function(content) { + if (glazeEnabled === content.on) return; + glazeEnabled = content.on; + if (content.on) { + send({ type: "glaze", content }); + } else { + glazeAfterReframe = { type: "glaze", content }; + } + }, + // Toggle HUD visibility (same as Tab key functionality) + // skipSound: true for KidLisp pieces to avoid beep/bop sounds on tap + toggleHUD: function(isDoubleTap = false, skipSound = false) { + toggleHUDVisibility(isDoubleTap, skipSound); + }, + jump: function jump(to, ahistorical = false, alias = false) { + console.log("\u{1F9ED} jump() called:", to, "SOLO_MODE:", SOLO_MODE, "leaving:", leaving, "loading:", loading); + if (SOLO_MODE) { + console.log("\u{1F512} Jump blocked: solo mode active"); + return; + } + if (leaving) { + console.log("\u{1F6AA}\u{1F434} Jump cancelled, already leaving..."); + return; + } + const jumpOut = to.startsWith("out:") || to.startsWith("http") && Aesthetic; + if (signed.indexOf(to) > -1) to = "/" + to; + if ((to.startsWith("http") || to.startsWith("/")) && !to.endsWith(".mjs") || jumpOut) { + to = to.replace("out:", ""); + try { + console.log("\u{1F40E} Jumping to web URL:", to); + $commonApi.net.web(to, jumpOut); + return; + } catch (e2) { + console.log("\u{1F40E} URL construction failed, treating as local piece:", e2.message); + return; + } + } else { + leaving = true; + unmask(); + } + { + const params = []; + if (DEVICE_MODE) params.push("device=true"); + if (TV_MODE) params.push("tv=true"); + if (SOLO_MODE) params.push("solo=true"); + if (HIGHLIGHT_MODE) { + if (HIGHLIGHT_COLOR && HIGHLIGHT_COLOR !== "64,64,64") { + params.push(`highlight=${encodeURIComponent(HIGHLIGHT_COLOR)}`); + } else { + params.push("highlight=true"); + } + } + if (PERF_MODE) params.push("perf=true"); + if (AUTO_SCALE_MODE) params.push("autoScale=true"); + if (hideLabel) params.push("nolabel=true"); + if (NOGAP_MODE) params.push("nogap=true"); + if (SPOOF_AUDIO_MODE) params.push("spoofaudio=true"); + if (params.length > 0) { + const hashIdx = to.indexOf("#"); + const base = hashIdx >= 0 ? to.slice(0, hashIdx) : to; + const frag = hashIdx >= 0 ? to.slice(hashIdx) : ""; + const separator = base.includes("?") ? "&" : "?"; + to = base + separator + params.join("&") + frag; + console.log("\u{1F9ED} Preserving resolution params:", to); + } + } + function loadLine() { + load(parse2(to), ahistorical, alias, false, callback); + } + let callback; + leaveLoad = () => { + if ($commonApi.rec.videoOnLeave && to.split("~")[0] === "prompt") { + to = "video"; + $commonApi.rec.videoOnLeave = false; + $commonApi.rec.cut(loadLine); + } else { + if (to.split("~")[0] === "prompt" && globalKidLispInstance2?.clearBakedLayers) { + globalKidLispInstance2.clearBakedLayers(); + unmask(); + } + loadLine(); + } + }; + return (cb) => callback = cb; + }, + // 🎄 Preload piece modules for merry pipelines (prevents network latency during fast cycling) + preloadPieces: async function preloadPieces(pieceNames, onProgress) { + if (!pieceNames || pieceNames.length === 0) return; + const baseUrl = getBuiltInPieceBaseUrl(); + let done = 0; + const total = pieceNames.length; + const bump = () => { + done += 1; + try { + onProgress?.(done / total); + } catch (e2) { + } + }; + const fetchPromises = pieceNames.map(async (piece) => { + if (pieceCodeCache.has(piece)) { + console.log(`\u{1F384} Piece already cached: ${piece}`); + bump(); + return; + } + try { + const mjsUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.mjs?v=${Date.now()}`; + let response = await fetch(mjsUrl, { cache: "no-store" }); + if (response.ok) { + const code2 = await response.text(); + pieceCodeCache.set(piece, { code: code2, type: "mjs" }); + console.log(`\u{1F384} Preloaded mjs: ${piece}`); + return; + } + const luaUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.lua?v=${Date.now()}`; + response = await fetch(luaUrl, { cache: "no-store" }); + if (response.ok) { + const code2 = await response.text(); + pieceCodeCache.set(piece, { code: code2, type: "lua" }); + console.log(`\u{1F384} Preloaded lua: ${piece}`); + return; + } + const lispUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.lisp?v=${Date.now()}`; + response = await fetch(lispUrl, { cache: "no-store" }); + if (response.ok) { + const code2 = await response.text(); + pieceCodeCache.set(piece, { code: code2, type: "lisp" }); + console.log(`\u{1F384} Preloaded lisp: ${piece}`); + return; + } + console.warn(`\u{1F384} Could not preload piece: ${piece}`); + } catch (err) { + console.warn(`\u{1F384} Error preloading ${piece}:`, err); + } finally { + bump(); + } + }); + await Promise.all(fetchPromises); + console.log(`\u{1F384} Preloaded ${pieceCodeCache.size} pieces`); + }, + // Get cached piece code (returns undefined if not cached) + getCachedPieceCode: function(pieceName) { + return pieceCodeCache.get(pieceName); + }, + canShare: false, + // Whether navigator.share is enabled for mobile devices. + leaving: isLeaving, + handle: () => { + return HANDLE; + }, + notice: (msg, color3 = ["white", "green"], opts) => { + notice = msg; + noticeColor = color3; + noticeOpts = opts; + const sound2 = {}; + if (color3[0] === "yellow" && color3[1] === "red") sound2.tone = 300; + noticeBell(cachedAPI, sound2); + if (shellHTMLMode) { + send({ type: "notice:shell", content: { text: msg, color: color3 } }); + } + }, + // 🪟 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 Hourglass(time, { completed: () => fun() })); + }, + // Different syntax than `delay` but the same with looped behavior. + blink: (time, fun) => { + hourGlasses.push( + new Hourglass(time, { completed: () => fun(), autoFlip: true }) + ); + }, + // 🎟️ Open a ticketed paywall on the page. + ticket: (name) => { + send({ type: "ticket-wall", content: name }); + }, + // 🪙 Mint a url or the `pixels` that get passed into the argument to a + // network of choice. + mint: async (picture, progress, params) => { + console.log("\u{1FA99} Minting...", picture); + let filename; + let zipped; + if (picture.record && HANDLE) { + const record = picture.record; + filename = `painting-${record[record.length - 1].timestamp}.png`; + zipped = await $commonApi.zip( + { destination: "upload", painting: { record } }, + (p) => { + console.log("\u{1F910} Zip progress:", p); + progress?.(p); + } + ); + console.log("\u{1F910} Zipped:", zipped); + } + const data = await uploadPainting(picture, progress, HANDLE, filename); + let description; + if (picture.width && picture.height) { + description = `A ${picture.width}x${picture.height} pixel painting made on [aesthetic computer](https://aesthetic.computer).`; + } else { + description = `A painting made on [aesthetic computer](https://aesthetic.computer).`; + } + if (data) { + if (HANDLE && zipped) { + description = `[\` ${HANDLE}/${data.slug}\`](https://aesthetic.computer/painting~${HANDLE}/${data.slug}) + +${description}`; + } + if (HANDLE) data.slug = `${HANDLE}/painting/${data.slug}`; + const pixels2 = `https://aesthetic.computer/api/pixel/2048:contain/${data.slug}.${data.ext}`; + $commonApi.jump( + encodeURI( + `https://zora.co/create/single-edition?image=${pixels2}&name=${params[0] || "Untitled Painting"}&symbol=$${data.slug}&description=${description}` + ) + ); + } + }, + // 🖨️ Print either a url or the `pixels` that get passed into + // the argument, with N quantity. + print: async (picture, quantity = 1, progress) => { + console.log("\u{1F5A8}\uFE0F Printing:", picture, "Quantity:", quantity); + const data = await uploadPainting(picture, progress); + let pixels2; + if (data && data.code) { + pixels2 = `${data.code}.${data.ext}`; + } else if (data && data.slug) { + pixels2 = `${data.slug}.${data.ext}`; + } else if (data) { + pixels2 = data.url; + } else { + $commonApi.notice("UPLOAD ERROR", ["red", "yellow"]); + return; + } + try { + const headers2 = { "Content-Type": "application/json" }; + try { + const token = await $commonApi.authorize(); + if (token) headers2.Authorization = `Bearer ${token}`; + } catch (err) { + } + const res = await fetch(`/api/print?new=true&pixels=${pixels2}`, { + method: "POST", + headers: headers2, + body: JSON.stringify({ quantity, slug: $commonApi.slug }) + // TODO: Add order info here. ^ + }); + const data2 = await res.json(); + if (!res.ok) + throw new Error( + `\u{1F5A8}\uFE0F Print: HTTP error! Status: ${JSON.stringify(data2)}` + ); + console.log("\u{1F5A8}\uFE0F Print order:", data2); + $commonApi.jump(data2.location); + } catch (error) { + console.error("\u{1F5A8}\uFE0F Print order error:", error); + } + }, + // ☕ Mug - Print a painting on a ceramic mug with optional color. + // `picture` can be a painting object OR a code string like "abc" or "#abc" + mug: async (picture, color3 = "white", quantity = 1, progress) => { + console.log("\u2615 Mug:", picture, "Color:", color3, "Quantity:", quantity); + let pixels2; + if (typeof picture === "string") { + const code2 = picture.startsWith("#") ? picture.slice(1) : picture; + pixels2 = `${code2}.png`; + console.log("\u2615 Using existing painting code:", code2); + } else { + const data = await uploadPainting(picture, progress); + if (data && data.code) { + pixels2 = `${data.code}.${data.ext}`; + } else if (data && data.slug) { + pixels2 = `${data.slug}.${data.ext}`; + } else if (data) { + pixels2 = data.url; + } else { + $commonApi.notice("UPLOAD ERROR", ["red", "yellow"]); + return; + } + } + try { + const headers2 = { "Content-Type": "application/json" }; + try { + const token = await $commonApi.authorize(); + if (token) headers2.Authorization = `Bearer ${token}`; + } catch (err) { + } + const res = await fetch(`/api/mug?new=true&pixels=${pixels2}&color=${color3}`, { + method: "POST", + headers: headers2, + body: JSON.stringify({ quantity, slug: $commonApi.slug }) + }); + const data = await res.json(); + if (!res.ok) + throw new Error( + `\u2615 Mug: HTTP error! Status: ${JSON.stringify(data)}` + ); + console.log("\u2615 Mug order:", data); + $commonApi.jump(data.location); + } catch (error) { + console.error("\u2615 Mug order error:", error); + } + }, + // Create a zip file of specified content. (Used for storing painting data.) + zip: (content, progress) => { + const prom = new Promise((resolve, reject) => { + zipCreation = { resolve, reject }; + }); + if (content.destination === "upload") { + serverUploadProgressReporter = progress; + serverUploadProgressReporter?.(0); + } + send({ type: "zip", content }); + return prom; + }, + // Track device motion. + motion: { + on: false, + start: () => { + send({ type: "motion:start" }); + }, + stop: () => { + send({ type: "motion:stop" }); + }, + current: {} + // Will get replaced by an update event. + }, + // 🔷 Tezos wallet management (via bios.mjs) + wallet: { + _state: { + connected: false, + address: null, + balance: null, + network: "ghostnet", + domain: null + // .tez domain if resolved + }, + _stateRequested: false, + // Track if we've requested state from bios + // Get current wallet state (requests from bios if not yet synced) + get: function() { + if (!$commonApi.wallet._stateRequested) { + $commonApi.wallet._stateRequested = true; + send({ type: "wallet:get-state" }); + } + return { ...$commonApi.wallet._state }; + }, + // Connect wallet (with optional address for manual entry) + connect: function(options = {}) { + if (typeof options === "string") { + send({ type: "wallet:connect", content: { network: options } }); + } else { + send({ type: "wallet:connect", content: options }); + } + }, + // Disconnect wallet + disconnect: function() { + send({ type: "wallet:disconnect" }); + }, + // Refresh balance from RPC + refreshBalance: function() { + send({ type: "wallet:refresh-balance" }); + }, + // Sign a message with the connected wallet + sign: function(message, callback) { + if (callback) $commonApi.wallet._signCallback = callback; + send({ type: "wallet:sign", content: { message } }); + }, + // Get pairing URI for mobile wallet QR code + getPairingUri: function(network = "mainnet", callback) { + if (callback) $commonApi.wallet._pairingCallback = callback; + send({ type: "wallet:get-pairing-uri", content: { network } }); + }, + // Internal callbacks + _pairingCallback: null, + _signCallback: null, + // Check if connected + isConnected: function() { + return $commonApi.wallet._state.connected; + }, + // Request state sync from bios + sync: function() { + send({ type: "wallet:get-state" }); + } + }, + // Speak an `utterance` aloud. + speak: function speak(utterance, voice = "female:18", mode = "cloud", opts) { + return send({ type: "speak", content: { utterance, voice, mode, opts } }); + }, + // Broadcast an event through the entire act system. + act: (event, data = {}) => { + data.is = (e2) => e2 === event; + cachedAPI.event = data; + try { + act(cachedAPI); + } catch (e2) { + console.warn("\uFE0F \u2712 Act failure...", e2); + } + }, + // 🚥 `Get` api + // Retrieve media assets from a user account. + get: { + picture: (url) => { + return $commonApi.net.preload( + encodeURI(url), + true, + void 0 + // byOpts, + ); + }, + painting: (code2, opts) => { + return { + by: async function(handle2 = "anon", byOpts) { + const extension = opts?.record ? "zip" : "png"; + const { protocol, hostname } = getSafeUrlParts(); + let baseUrl; + const isDevelopment = hostname === "localhost" && typeof location !== "undefined" && location.port; + if (isDevelopment) { + baseUrl = `${protocol}//${hostname}:${location.port}`; + } else { + baseUrl = `https://aesthetic.computer`; + } + const isTimestamp = code2.match(/^\d{4}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,3}$/); + let mediaUrl; + if (isTimestamp && handle2 && handle2 !== "anon") { + const handleWithAt = handle2.startsWith("@") ? handle2 : `@${handle2}`; + mediaUrl = `${baseUrl}/media/${handleWithAt}/painting/${code2}.${extension}`; + } else { + mediaUrl = `${baseUrl}/media/paintings/${code2}.${extension}`; + } + return $commonApi.net.preload( + mediaUrl, + true, + void 0, + byOpts + ); + } + }; + } + }, + // ***Actually*** upload a file to the server. + // 📓 The file name can have `media-` which will sort it on the server into + // a directory via `presigned-url.js`. + upload: async (filename, data, progress, bucket, recordingSlug, metadata2) => { + const prom = new Promise((resolve, reject) => { + serverUpload = { resolve, reject }; + }); + serverUploadProgressReporter = progress; + serverUploadProgressReporter?.(0); + console.log("Uploading:", filename, { width: data?.width, height: data?.height }); + send({ type: "upload", content: { filename, data, bucket, recordingSlug, metadata: metadata2 } }); + return prom; + }, + code: { + channel: (chan) => { + codeChannel = chan; + store["code-channel"] = codeChannel; + store.persist("code-channel"); + if (!codeChannel || codeChannel?.length === 0) { + console.log("\u{1F4ED} Code channel cleared!"); + } else { + console.log("\u{1F4EC} Code channel set to:", codeChannel); + } + socket.send("code-channel:sub", codeChannel); + send({ + type: "post-to-parent", + content: { type: "setCode", value: codeChannel } + }); + }, + // Store a painting code mapping (timestamp/slug -> 3-letter code) + store: (slug, handle2, code2) => { + codeCache.storeCode(slug, handle2, code2); + }, + // Get a painting code from timestamp/slug + get: (slug, handle2) => { + return codeCache.getCode(slug, handle2); + } + }, + encode: async (file) => { + const prom = new Promise((resolve, reject) => { + fileEncodeRequest = { resolve, reject }; + }); + send({ type: "file-encode:request", content: file }); + return prom; + }, + // Open a local file picker. Pass { mode: "video" } (and optionally + // { accept }) to pick a clip and resolve with + // { kind:"video", data:ArrayBuffer, mime, name, duration }; the default + // resolves with a bitmap (image picker). + file: async (opts = {}) => { + const prom = new Promise((resolve, reject) => { + fileOpenRequest = { resolve, reject }; + }); + send({ type: "file-open:request", content: opts }); + return prom; + }, + // Authorize a user. + authorize: async () => { + const prom = new Promise((resolve, reject) => { + authorizationRequests.push({ resolve, reject }); + }); + send({ type: "authorization:request" }); + return prom; + }, + // Get a token for a logged in user. + // Hand-tracking. 23.04.27.10.19 TODO: Move eventually. + hand: { mediapipe: { screen: [], world: [], hand: "None" } }, + hud: { + label: (text, color3, offset, plainTextOverride) => { + currentHUDTxt = text; + currentHUDPlainTxt = plainTextOverride || stripColorCodes4(text); + if (!color3) { + currentHUDTextColor = currentHUDTextColor || findColor2(color3); + } else { + currentHUDTextColor = findColor2(color3); + } + currentHUDOffset = offset; + if (currentHUDTxt && currentHUDTxt.length > 0) { + const textForMeasurement = currentHUDPlainTxt || currentHUDTxt; + const sourceCode = currentText || currentHUDTxt; + const isKidlispPiece = currentPath && kidlisp_exports?.isKidlispSource && isKidlispSource(currentPath) && !currentPath.endsWith(".lisp") || currentPath === "(...)" || sourceCode && sourceCode.startsWith("$") || currentPath && currentPath.includes("/disks/$") || sourceCode && kidlisp_exports?.isKidlispSource && isKidlispSource(sourceCode); + const maxWidth = cachedAPI.screen.width; + const labelBounds = cachedAPI.text.box( + textForMeasurement, + void 0, + maxWidth, + 1, + // scale + true, + // wordWrap + // TODO: This should check useTinyHudLabel like the main corner label, but that's not accessible here + // For now, use default font to maintain consistency with current behavior + void 0 + // Use default font - will need to be updated when useTinyHudLabel is globally accessible + ); + const measuredWidth = labelBounds.box.width; + const fallbackShareWidth = (currentHUDLabelBlockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH) * "share ".length; + const shareWidth = Math.max(currentHUDShareWidth || 0, fallbackShareWidth); + currentHUDLeftPad = shareWidth; + const baseLabelWidth = measuredWidth + shareWidth; + const scrubExtension = Math.max(0, currentHUDScrub); + const h = labelBounds.box.height + cachedAPI.typeface.blockHeight; + currentHUDLabelMeasuredWidth = baseLabelWidth; + hudAnimationState.labelWidth = baseLabelWidth + scrubExtension; + hudAnimationState.labelHeight = h; + } + }, + // 📱 Set a QR code to display to the LEFT of the HUD label + qr: (url) => { + console.log("\u{1F4F1} HUD QR called with:", JSON.stringify(url), "current:", JSON.stringify(currentHUDQR)); + if (url !== currentHUDQR) { + currentHUDQR = url; + currentHUDQRCells = null; + console.log("\u{1F4F1} HUD QR set to:", url); + } + }, + // 🔤 Force MatrixChunky8 (tiny) font for HUD label + tinyLabel: (enabled = true) => { + forceTinyHudLabel = enabled; + }, + // 🌐 Set a compact suffix (e.g. ".com") after the HUD label. + suffix: (text) => { + currentHUDSuperscript = text || null; + }, + // Backward-compatible name for older pieces. + superscript: (text) => { + currentHUDSuperscript = text || null; + }, + currentStatusColor: () => currentHUDStatusColor, + currentLabel: () => ({ + text: currentHUDTxt, + plainText: currentHUDPlainTxt, + // Include plain text version + btn: currentHUDButton, + qrSize: currentHUDQRCells?.length || hudAnimationState?.qrSize || 0, + leftPad: currentHUDLeftPad || 0 + }), + labelBack: () => { + labelBack = true; + send({ type: "labelBack:source", content: { sourcePiece: $commonApi.piece } }); + } + }, + // 📱 LAN Dev mode identity (from session server) + get devIdentity() { + return devIdentity; + }, + send, + platform: platform_exports, + // USB flash support (Electron only) + usb: { + listDevices: () => new Promise((resolve) => { + usbDeviceListResolution = resolve; + send({ type: "usb:list-devices" }); + }), + flashImage: (opts) => new Promise((resolve) => { + usbFlashResolution = resolve; + send({ type: "usb:flash-image", content: opts }); + }) + }, + history: [], + // Populated when a disk loads and sets the former piece. + // Trigger background music. + // Eventually add an "@" style parameter similar to what a stamp system would have. + bgm: { + set: function(trackNumber, volume) { + send({ type: "bgm-change", content: { trackNumber, volume } }); + }, + stop: () => send({ type: "bgm-stop" }), + data: {} + }, + system: { + // 🐚 True when an HTML shell (prompt.ac) hosts the runtime and owns the + // prompt / corner chrome in DOM — pieces skip their own low-res chrome. + shellhtml: shellHTMLMode, + // prompt: { input: undefined }, Gets set in `prompt_boot`. + // 🎵 ac-electron drag-drop: set when a file is dropped onto the app + // (Dock icon or running window). Pieces (notably `play`) read this on + // boot; a `dropped:file` act event also fires for already-running + // pieces. Cleared by the consumer. + droppedFile: null, + // 📦 Global update poll state — `ready` flips true when /api/version + // long-poll detects a new deployment. Pieces can read this to render + // their own update UI; the corner ↑ badge in disk.mjs is automatic. + update: { + get ready() { + return globalUpdateReady; + }, + get versionInfo() { + return globalVersionInfo; + }, + get recentCommits() { + return globalRecentCommits; + }, + reload: () => send({ type: "window:reload" }) + }, + world: { + // Populated in `world_boot` of `world.mjs`. + teleported: false, + telepos: void 0, + teleport: (to, telepos) => { + $commonApi.system.world.teleported = true; + $commonApi.system.world.telepos = telepos; + $commonApi.jump(to); + } + }, + nopaint: { + //boot: nopaint_boot, // TODO: Why are these in the commonApi? 23.02.12.14.26 + // act: nopaint_act, + buffer: null, + // An overlapping brush buffer that gets drawn on top of the + // painting. + piece: null, + // Canonical code + pixel layer stack for No Paint paintings. + syncWip: async (api = cachedAPI) => { + if (!api) return null; + const { syncACPaintingWip } = await import("./painting-wip.mjs"); + return syncACPaintingWip(api); + }, + recording: false, + record: [], + // Store a recording here. + gestureRecord: [], + // Store the active gesture. + startRecord: function(fullText) { + const sys = $commonApi.system; + sys.nopaint.record = []; + sys.nopaint.recording = true; + sys.nopaint.addToRecord({ + label: fullText || "start", + painting: { + pixels: new Uint8ClampedArray(sys.painting.pixels), + width: sys.painting.width, + height: sys.painting.height + } + }); + }, + addToRecord: function(record) { + record.timestamp = timestamp(); + record.gesture = $commonApi.system.nopaint.gestureRecord.slice(); + if (record.gesture.length === 0) delete record.gesture; + $commonApi.system.nopaint.gestureRecord = []; + $commonApi.system.nopaint.record.push(record); + store["painting:record"] = $commonApi.system.nopaint.record; + store.persist("painting:record", "local:db"); + }, + is: nopaint_is, + cancelStroke: nopaint_cancelStroke, + undo: { paintings: undoPaintings }, + needsBake: false, + needsPresent: false, + bakeOnLeave: false, + addUndoPainting, + // Regresses the system painting to a previous state. + // Or the reverse... ("yes") + no: ({ system: system2, store: store2, needsPaint }, yes = false) => { + const paintings2 = system2.nopaint.undo.paintings; + let dontRecord = false; + if (yes) { + undoPosition += 1; + if (undoPosition > paintings2.length - 1) { + undoPosition = paintings2.length - 1; + dontRecord = true; + } + } else { + undoPosition -= 1; + if (undoPosition < 0) { + undoPosition = 0; + dontRecord = true; + } + } + if (paintings2.length > 1) { + const p = paintings2[undoPosition]; + const op = p.pixels; + const pixels2 = new Uint8ClampedArray(op.length); + pixels2.set(op); + store2["painting"] = { + width: p.width, + height: p.height, + pixels: pixels2 + }; + const resolutionChange = paintings2[0].width !== paintings2[1].width || paintings2[0].height !== paintings2[1].height; + store2.persist("painting", "local:db"); + $commonApi.broadcastPaintingUpdate("updated", { + source: "yes-no-decision" + }); + system2.painting = store2["painting"]; + if (system2.nopaint.recording && dontRecord === false) { + const label = yes ? "yes" : "no"; + system2.nopaint.addToRecord({ + label + //, + // painting: { + // width: system.painting.width, + // height: system.painting.height, + // pixels: new Uint8Array(system.painting.pixels), + // }, + }); + } + if (resolutionChange) { + system2.nopaint.resetTransform({ system: system2 }); + system2.nopaint.storeTransform(store2, system2); + } + needsPaint(); + } + }, + // Center the picture within the screen / default translation. + resetTransform: ({ system: sys }) => { + sys.nopaint.zoomLevel = 1; + if (!sys.painting) { + sys.nopaint.translation = { x: 0, y: 0 }; + return; + } + sys.nopaint.translation.x = floor11( + screen.width / 2 - sys.painting.width / 2 + ); + sys.nopaint.translation.y = floor11( + screen.height / 2 - sys.painting.height / 2 + ); + }, + storeTransform: (store2, sys) => { + store2["painting:transform"] = { + translation: sys.nopaint.translation, + zoom: sys.nopaint.zoomLevel + }; + store2.persist("painting:transform", "local:db"); + }, + translation: { x: 0, y: 0 }, + zoomLevel: 1, + translate: ({ system: system2 }, x, y) => { + system2.nopaint.translation.x += x; + system2.nopaint.translation.y += y; + }, + // zoom: ({ system }, dir) => { + // system.nopaint.zoomLevel += dir === "in" ? 1 : -1; + // console.log("🔭 Zoom level:", system.nopaint.zoomLevel); + // if (system.nopaint.zoomLevel <= 0) system.nopaint.zoomLevel = 1; + // // TODO: Adjust the translation based on system.nopaint.brush.x and y + // // Which would serve as the zoom origin point. + // }, + zoom: ({ system: system2 }, dir, cursor2) => { + const oldZoomLevel = system2.nopaint.zoomLevel; + system2.nopaint.zoomLevel += dir === "in" ? 1 : -1; + if (system2.nopaint.zoomLevel <= 0) system2.nopaint.zoomLevel = 1; + const scale7 = system2.nopaint.zoomLevel / oldZoomLevel; + system2.nopaint.translation.x = floor11( + cursor2.x + (system2.nopaint.translation.x - cursor2.x) * scale7 + ); + system2.nopaint.translation.y = floor11( + cursor2.y + (system2.nopaint.translation.y - cursor2.y) * scale7 + ); + }, + brush: { x: 0, y: 0, dragBox: void 0 }, + transform: (p) => { + return { + x: (p.x - nopaintAPI.translation.x) / nopaintAPI.zoomLevel, + y: (p.y - nopaintAPI.translation.y) / nopaintAPI.zoomLevel + }; + }, + // Similar to `updateBrush` but for arbitrary points, + // with no change to `system.nopaint.brush`. + pointToPainting: ({ system: system2, pen }) => { + const zoom2 = system2.nopaint.zoomLevel; + const x = floor11(((pen?.x || 0) - system2.nopaint.translation.x) / zoom2); + const y = floor11(((pen?.y || 0) - system2.nopaint.translation.y) / zoom2); + return { x, y }; + }, + updateBrush: ({ pen, event, system: system2 }, act2) => { + const source = event && event.device === "robot" ? event : pen; + const zoom2 = system2.nopaint.zoomLevel; + const x = floor11(((source?.x || 0) - system2.nopaint.translation.x) / zoom2); + const y = floor11(((source?.y || 0) - system2.nopaint.translation.y) / zoom2); + if (act2 === "touch") { + system2.nopaint.startDrag = { x, y }; + } + if (!system2.nopaint.startDrag) { + system2.nopaint.startDrag = { x, y }; + } + const dragBox = new Box( + system2.nopaint.startDrag.x, + system2.nopaint.startDrag.y, + x - system2.nopaint.startDrag.x, + y - system2.nopaint.startDrag.y + ); + system2.nopaint.brush = { x, y, dragBox, pressure: source?.pressure || 0.5 }; + $commonApi.needsPaint(); + }, + // Helper to display the existing painting on the screen, with an + // optional pan amount, that returns an adjusted pen pointer as `brush`. + // TODO: - [] Add Zoom + // - [] And Rotation! + present: ({ system: system2, screen: screen2, wipe, paste: paste3, ink: ink3, slug, dark, theme, blend: blend3 }, tx, ty) => { + system2.nopaint.needsPresent = false; + if (!system2.painting) { + console.warn("\u{1F5BC}\uFE0F present: system.painting is undefined, skipping"); + return; + } + const x = tx || system2.nopaint.translation.x; + const y = ty || system2.nopaint.translation.y; + system2.nopaint.translation = { x, y }; + const fullbleed = x === 0 && y === 0 && screen2.width <= system2.painting.width && screen2.height <= system2.painting.height; + if (fullbleed) { + paste3(system2.painting); + paste3(system2.nopaint.buffer); + } else { + wipe(theme[dark ? "dark" : "light"].wipeBG).paste(system2.painting, x, y, system2.nopaint.zoomLevel); + paste3(system2.nopaint.buffer, x, y, system2.nopaint.zoomLevel); + ink3(128).box( + x, + y, + system2.painting.width * system2.nopaint.zoomLevel, + system2.painting.height * system2.nopaint.zoomLevel, + "outline" + ); + } + if (system2.nopaint.zoomLevel !== 1 && slug !== "prompt") + ink3(255, 127).write(`${system2.nopaint.zoomLevel}x`, { x: 6, y: 18 }); + return { + x, + y + //, + //brush: { x: (pen?.x || 0) - x, y: (pen?.y || 0) - y }, + }; + }, + // Kill an existing painting. + noBang: async ({ system: system2, store: store2, needsPaint, painting: painting2, theme, dark }, res = { w: screen.width, h: screen.height }) => { + const deleted = await store2.delete("painting", "local:db"); + await store2.delete("painting:piece", "local:db"); + await store2.delete("painting:resolution-lock", "local:db"); + await store2.delete("painting:transform", "local:db"); + system2.nopaint.undo.paintings.length = 0; + system2.painting = null; + system2.nopaint.piece = null; + system2.nopaint.resetTransform({ system: system2, screen }); + needsPaint(); + system2.painting = painting2(res.w, res.h, ($) => { + $.wipe(theme[dark ? "dark" : "light"].wipeNum); + }); + store2["painting"] = { + width: system2.painting.width, + height: system2.painting.height, + pixels: system2.painting.pixels + }; + store2.persist("painting", "local:db"); + $commonApi.broadcastPaintingUpdate("cleared", { + source: "clear", + width: res.w, + height: res.h, + resetTransform: true + // Flag that receiving tabs should reset their transform + }); + await store2.delete("painting:record", "local:db"); + if (system2.nopaint.recording) { + system2.nopaint.recording = false; + system2.nopaint.record.length = 0; + } + return deleted; + }, + // Replace a painting entirely, remembering the last one. + // (This will always enable fixed resolution mode.) + replace: ({ system: system2, screen: screen2, store: store2, needsPaint }, painting2, message = "(replace)") => { + store2.delete("painting:piece", "local:db"); + system2.nopaint.piece = null; + system2.painting = painting2; + store2["painting"] = { + width: system2.painting.width, + height: system2.painting.height, + pixels: system2.painting.pixels + }; + store2.persist("painting", "local:db"); + $commonApi.broadcastPaintingUpdate("replaced", { + source: "replace", + message + }); + store2["painting:resolution-lock"] = true; + store2.persist("painting:resolution-lock", "local:db"); + system2.nopaint.resetTransform({ system: system2, screen: screen2 }); + system2.nopaint.storeTransform(store2, system2); + system2.nopaint.addUndoPainting(system2.painting, message); + system2.nopaint.needsPresent = true; + needsPaint(); + }, + abort: () => NPnoOnLeave = true + } + }, + // Paint all queued rendering commands immediately. + flatten: () => { + return painting.paint(true); + }, + connect: () => { + const p = new Promise((resolve, reject) => { + web3Response = { resolve, reject }; + }); + send({ type: "web3-connect" }); + return p; + }, + // Tezos Wallet API + tezos: { + // Connect to a Tezos wallet (opens Beacon popup) + connect: (network = "mainnet") => { + const p = new Promise((resolve, reject) => { + tezosConnectResponse = { resolve, reject }; + }); + send({ type: "tezos-connect", content: { network } }); + return p; + }, + // Disconnect from Tezos wallet + disconnect: () => { + const p = new Promise((resolve, reject) => { + tezosDisconnectResponse = { resolve, reject }; + }); + send({ type: "tezos-disconnect" }); + return p; + }, + // Get current wallet address (null if not connected) + address: () => { + const p = new Promise((resolve, reject) => { + tezosAddressResponse = { resolve, reject }; + }); + send({ type: "tezos-address" }); + return p; + }, + // Sign a message with the connected wallet + sign: (message) => { + const p = new Promise((resolve, reject) => { + tezosSignResponse = { resolve, reject }; + }); + send({ type: "tezos-sign", content: { message } }); + return p; + }, + // Call a contract method + call: (contractAddress, method, args, amount = 0) => { + const p = new Promise((resolve, reject) => { + tezosCallResponse = { resolve, reject }; + }); + send({ type: "tezos-call", content: { contractAddress, method, args, amount } }); + return p; + } + }, + wiggle: function(n2, level = 0.2, speed = 6) { + wiggleAngle = (wiggleAngle + 1 * speed) % 360; + const osc = sin5(radians(wiggleAngle)); + return n2 + n2 * level * osc; + }, + dark: true, + // Dark mode. (Gets set on startup and on any change.) + darkMode, + // Toggle dark mode or set to `true` or `false`. + // content: added programmatically: see Content class + gpuReady: false, + gpu: { + message: (content) => { + const p = new Promise((resolve, reject) => { + gpuResponse = { resolve, reject }; + }); + send({ type: "gpu-event", content }); + return p; + } + }, + // WebGPU 2D Renderer API + webgpu: { + enabled: false, + // Flag to disable CPU renderer when true + backend: null, + // Preferred backend: "webgpu", "webgl2", "canvas2d", "vello", etc. + // 🛑 Disable WebGPU and restore normal CPU rendering + disable: () => { + $commonApi.webgpu.enabled = false; + $commonApi.webgpu.backend = null; + send({ type: "webgpu-command", content: { type: "disable" } }); + }, + clear: (r2 = 0, g = 0, b2 = 0, a2 = 255) => { + send({ + type: "webgpu-command", + content: { + type: "clear", + color: [r2, g, b2, a2] + } + }); + }, + line: (x1, y1, x2, y2, r2 = 255, g = 255, b2 = 255, a2 = 255) => { + send({ + type: "webgpu-command", + content: { + type: "line", + x1, + y1, + x2, + y2, + color: [r2, g, b2, a2] + } + }); + }, + render: () => { + send({ + type: "webgpu-command", + content: { type: "render" } + }); + }, + // 📊 Toggle performance overlay + perf: (enabled) => { + send({ + type: "webgpu-command", + content: { type: "perf-overlay", enabled } + }); + } + }, + // 📊 DOM-based stats overlay (always on top of everything, even GPU canvases) + stats: { + show: (data = {}) => { + send({ type: "stats-overlay", content: { enabled: true, data } }); + }, + hide: () => { + send({ type: "stats-overlay", content: { enabled: false } }); + }, + update: (data) => { + send({ type: "stats-overlay", content: { data } }); + } + }, + // Deprecated in favor of `bios` -> `hitboxes`. (To support iOS) + // clipboard: { + // copy: (text) => { + // send({ type: "copy", content: text }); + // }, + // }, + text: { + capitalize, + reverse, + // Get the pixel width of a string of characters. + width: (text, fontName) => { + if (Array.isArray(text)) text = text.join(" "); + const useTypeface = getTypefaceForMeasurement(fontName) || tf; + if (!useTypeface) { + return text.length * DEFAULT_TYPEFACE_BLOCK_WIDTH; + } + const isProportional = useTypeface?.data?.proportional === true || !!useTypeface?.data?.advances || !!useTypeface?.data?.bdfFont; + if (isProportional && typeof useTypeface.getAdvance === "function") { + let totalWidth = 0; + for (const char of text) { + if (char === "\n" || char === "\r") continue; + const advance = useTypeface.getAdvance(char); + totalWidth += typeof advance === "number" ? advance : useTypeface.blockWidth; + } + return totalWidth; + } else { + const blockWidth = useTypeface.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; + return text.length * blockWidth; + } + }, + height: (text) => { + return 10; + }, + // Return a text's bounding box. + box: (text, pos = { x: 0, y: 0 }, bounds, scale7 = 1, wordWrap = true, fontName) => { + if (!text) { + console.warn("\u26A0\uFE0F No text for `box`."); + return; + } + pos = { ...pos }; + const absScale = abs5(scale7 ?? 1); + let useTypeface = getTypefaceForMeasurement(fontName) || tf; + if (!useTypeface) { + useTypeface = tf; + } + const baseBlockWidth = useTypeface?.blockWidth ?? tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH; + const baseBlockHeight = useTypeface?.blockHeight ?? tf?.blockHeight ?? DEFAULT_TYPEFACE_BLOCK_HEIGHT; + const blockHeight = baseBlockHeight * absScale; + const isProportional = useTypeface?.data?.proportional === true || !!useTypeface?.data?.advances || !!useTypeface?.data?.bdfFont; + const getAdvanceWidth = (char) => { + if (!char) return baseBlockWidth * absScale; + if (isProportional && typeof useTypeface?.getAdvance === "function") { + const raw = useTypeface.getAdvance(char); + if (typeof raw === "number") { + return raw * absScale; + } + } + return baseBlockWidth * absScale; + }; + const getCharWidth = (char) => { + if (!char || char === "\n" || char === "\r") return 0; + if (char === " ") { + return getAdvanceWidth(" ") * 4; + } + return getAdvanceWidth(char); + }; + if (bounds === void 0) { + const sampleWidth = getAdvanceWidth("0"); + bounds = (text.length + 2) * sampleWidth; + } + if (!(bounds > 0)) { + bounds = Number.POSITIVE_INFINITY; + } + const lines = [""]; + const charMap = [[]]; + let line2 = 0; + let run = 0; + let maxWidth = 0; + const commitLineWidth = () => { + if (run > maxWidth) { + maxWidth = run; + } + }; + const newLine = () => { + commitLineWidth(); + line2 += 1; + lines[line2] = ""; + charMap[line2] = []; + run = 0; + }; + const appendChar = (char, sourceIndex) => { + const width2 = getCharWidth(char); + const renderedChar = char === " " ? " " : char; + lines[line2] += renderedChar; + charMap[line2].push(sourceIndex); + run += width2; + if (run > maxWidth) { + maxWidth = run; + } + }; + if (!wordWrap) { + for (let idx = 0; idx < text.length; idx += 1) { + const char = text[idx]; + if (char === "\r") continue; + if (char === "\n") { + newLine(); + continue; + } + appendChar(char, idx); + } + } else { + const tokens = []; + let idx = 0; + while (idx < text.length) { + const char = text[idx]; + if (char === "\r") { + idx += 1; + continue; + } + if (char === "\n") { + tokens.push({ type: "newline", indices: [idx] }); + idx += 1; + continue; + } + if (char === " " || char === " ") { + let textBuffer2 = ""; + const indices2 = []; + while (idx < text.length) { + const c4 = text[idx]; + if (c4 !== " " && c4 !== " ") break; + textBuffer2 += c4; + indices2.push(idx); + idx += 1; + } + tokens.push({ type: "whitespace", text: textBuffer2, indices: indices2 }); + continue; + } + let textBuffer = ""; + const indices = []; + while (idx < text.length) { + const c4 = text[idx]; + if (c4 === "\n" || c4 === " " || c4 === " " || c4 === "\r") break; + textBuffer += c4; + indices.push(idx); + idx += 1; + } + if (textBuffer.length > 0) { + tokens.push({ type: "word", text: textBuffer, indices }); + } + } + tokens.forEach((token) => { + if (token.type === "newline") { + newLine(); + return; + } + if (token.type === "whitespace") { + for (let i2 = 0; i2 < token.text.length; i2 += 1) { + const char = token.text[i2]; + const sourceIndex = token.indices[i2]; + const width2 = getCharWidth(char); + if (run > 0 && run + width2 > bounds) { + newLine(); + } + appendChar(char, sourceIndex); + } + return; + } + if (token.type === "word") { + const chars = token.text.split(""); + const widths = chars.map((char) => getCharWidth(char)); + const wordWidth = widths.reduce((acc, value) => acc + value, 0); + if (bounds > 0 && wordWidth > bounds) { + chars.forEach((char, charIdx) => { + const width2 = widths[charIdx]; + if (run > 0 && run + width2 > bounds) { + newLine(); + } + appendChar(char, token.indices[charIdx]); + }); + return; + } + if (run > 0 && run + wordWidth > bounds) { + newLine(); + } + chars.forEach((char, charIdx) => { + appendChar(char, token.indices[charIdx]); + }); + } + }); + } + commitLineWidth(); + const lineHeightGap = 1 * absScale; + const finalBlockHeight = blockHeight + lineHeightGap; + if (lines.length >= 1 && pos.center && pos.center.indexOf("y") !== -1) { + pos.y = $activePaintApi.screen.height / 2 - lines.length * finalBlockHeight / 2 + finalBlockHeight / 2 + (pos.y || 0); + } + const height2 = lines.length * finalBlockHeight; + const lineWidths = lines.map((lineText, index) => { + if (!lineText) return 0; + const indices = charMap[index] || []; + let width2 = 0; + for (let i2 = 0; i2 < lineText.length; i2 += 1) { + const sourceIndex = indices[i2]; + if (typeof sourceIndex === "number" && sourceIndex >= 0 && sourceIndex < text.length) { + width2 += getCharWidth(text[sourceIndex]); + } else { + width2 += getCharWidth(lineText[i2]); + } + } + return width2; + }); + const maxLineWidth = lineWidths.reduce((acc, value) => Math.max(acc, value), 0); + const box2 = { x: pos.x, y: pos.y, width: maxLineWidth, height: height2 }; + return { pos, box: box2, lines, lineWidths, lineHeight: finalBlockHeight, charMap }; + } + }, + num: { + add: add7, + wrap, + even, + odd, + clamp, + wave, + rand, + randInt, + randInd, + randIntArr, + randIntRange, + rangedInts, + multiply: multiply7, + perlin, + dist: dist4, + dist3d, + radians, + degrees, + lerp: lerp5, + map, + arrMax, + arrCompress, + Track, + timestamp, + p2, + midp, + number, + intersects, + signedCeil, + signedFloor, + vec2: vec2_exports, + vec3: vec3_exports, + vec4: vec4_exports, + mat3: mat3_exports, + mat4: mat4_exports, + quat: quat_exports, + parseColor, + findColor, + saturate, + desaturate, + shiftRGB, + rgbToHexStr, + hexToRgb, + blend, + rgbToHsl, + hslToRgb, + rainbow, + zebra, + resetZebraCache + }, + geo: { + Box, + DirtyBox, + Grid, + Circle, + linePointsFromAngle, + pointFrom, + Race, + Quantizer + }, + ui: { + Button, + Slider, + TextButton, + TextButtonSmall, + TextInput, + TextFields + }, + help: { + choose, + flip, + repeat, + every, + any, + anyIndex, + anyKey, + resampleArray, + each, + shuffleInPlace, + serializePainting: (painting2) => { + if (!painting2) return; + const pixels2 = uint8ArrayToBase64(painting2.pixels); + return { width: painting2.width, height: painting2.height, pixels: pixels2 }; + }, + deserializePainting: (painting2) => { + if (!painting2) return; + const pixels2 = base64ToUint8Array(painting2.pixels); + return { width: painting2.width, height: painting2.height, pixels: pixels2 }; + } + }, + gizmo: { Hourglass, EllipsisTicker, Ticker }, + rec: new Recorder(), + robo: new Robo(), + net: { + signup: () => { + send({ type: "signup" }); + }, + login: () => { + store.delete("handle"); + send({ type: "login" }); + }, + // { email } + logout: () => { + store.delete("handle"); + send({ type: "logout" }); + $commonApi.broadcast("logout:success"); + chatClient.system?.server?.send("logout"); + }, + pieces: `${(() => { + if (typeof window !== "undefined" && window.acSPIDER) { + return "https://aesthetic.computer"; + } + const { protocol, hostname } = getSafeUrlParts(); + return `${protocol}//${hostname}`; + })()}/aesthetic.computer/disks`, + parse: parse2, + // Parse a piece slug. + // lan: // Set dynamically. + // host: // Set dynamically. + // loadFailureText: // Set dynamically. + // Make a user authorized / signed request to the api. + // Used both in `motd` and `handle`. + requestDocs: async () => { + if (typeof docs === "object") return Promise.resolve(docs); + return fetch("/docs.json").then((response) => { + if (response.status !== 200) { + throw new Error("Network failure: " + response.status); + } + return response.json(); + }).then((d2) => { + docs = d2; + return docs; + }).catch((err) => console.error("\u{1F534} \u{1F4DA} Couldn't get docs:", err)); + }, + // Get the authorization token for the current user (for streaming/custom requests) + getToken: async () => { + try { + return await $commonApi.authorize(); + } catch (error) { + if (error) console.warn("\u{1F511} getToken:", error); + return null; + } + }, + userRequest: async (method, endpoint, body) => { + try { + const token = await $commonApi.authorize(); + if (!token) throw new Error("\u{1F9D6} Not logged in."); + const headers2 = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json" + }; + const options = { method, headers: headers2 }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(endpoint, options); + if (response.status === 500) { + try { + const json = await response.json(); + return { status: response.status, ...json }; + } catch (e2) { + return { status: response.status, message: response.statusText }; + } + } else { + const clonedResponse = response.clone(); + try { + return { + ...await clonedResponse.json(), + status: response.status + }; + } catch { + return { status: response.status, body: await response.text() }; + } + } + } catch (error) { + if (/^\/api\/(?:mail|mail-status|tell)(?:[/?#]|$)/.test(endpoint)) { + console.error("mail.request.failed"); + } else { + console.error("\u{1F6AB} Error:", error); + } + return { message: "unauthorized" }; + } + }, + // Loosely connect the UDP receiver. + udp: (receive2) => { + udpReceive = receive2; + return udp; + }, + hiccup: (hiccupIn = 5, outageSeconds = 5) => { + console.log("\u{1F635} Hiccuping in:", hiccupIn, "seconds."); + clearTimeout(hiccupTimeout); + hiccupTimeout = setTimeout(() => { + console.log("\u{1F636}\u200D\u{1F32B}\uFE0F Hiccup!"); + chatClient.system?.server.kill(outageSeconds); + socket?.kill(outageSeconds); + udp?.kill(outageSeconds); + }, hiccupIn * 1e3); + }, + // Remote debugging: Send log messages to session server for debugging on any device + log: function(levelOrFilename, ...args) { + if (levelOrFilename.startsWith("/tmp/") || levelOrFilename.includes(".log")) { + const filename = levelOrFilename; + const content = args[0]; + if (socket && socket.send) { + socket.send("dev-log", { + level: "INFO", + message: `${filename}: ${content}`, + device: navigator.userAgent || "unknown", + timestamp: Date.now() + }); + } + } else { + const level = levelOrFilename; + const serializedArgs = args.map((arg) => { + if (typeof arg === "object" && arg !== null) { + try { + return JSON.stringify(arg); + } catch (e2) { + return String(arg); + } + } + return String(arg); + }); + const message = serializedArgs.join(" "); + if (level === "warn") { + console.warn(...args); + } else if (level === "error") { + console.error(...args); + } else { + console.log(...args); + } + if (socket && socket.send) { + socket.send("dev-log", { + level: level.toUpperCase(), + message, + device: navigator.userAgent || "unknown", + timestamp: Date.now() + }); + } + } + } + }, + needsPaint: () => { + noPaint = false; + if (system === "nopaint") { + $commonApi.system.nopaint.needsPresent = true; + } + }, + // TODO: Does "paint" needs this? + store, + pieceCount: -1, + // Incs to 0 when the first piece (usually the prompt) loads. + // Increments by 1 each time a new piece loads. + debug: debug3, + nopaintPerf, + // 🎯 Cursor control API - pieces can request custom cursors + cursor: (cursorStyle) => { + send({ type: "cursor:set", content: { style: cursorStyle } }); + } +}; +$commonApi.net.log.info = (...args) => $commonApi.net.log("info", ...args); +$commonApi.net.log.warn = (...args) => $commonApi.net.log("warn", ...args); +$commonApi.net.log.error = (...args) => $commonApi.net.log("error", ...args); +var pendingRecordingUIOverlays = {}; +$commonApi.recordingUI = { + // Add an overlay that will be visible on screen but not recorded in tapes + // name: unique identifier for the overlay + // painting: a painting buffer created with $api.painting() + // x, y: position to draw the overlay + // opacity: optional opacity (0-1) + add: function(name, painting2, x, y, opacity = 1) { + if (!painting2 || !painting2.pixels) return; + pendingRecordingUIOverlays[name] = { + x, + y, + opacity, + img: { + width: painting2.width, + height: painting2.height, + pixels: painting2.pixels + } + }; + }, + // Clear all recording UI overlays + clear: function() { + pendingRecordingUIOverlays = {}; + }, + // Get pending overlays (used internally when sending frame data) + _getPending: function() { + return pendingRecordingUIOverlays; + }, + // Clear pending overlays (used internally after sending) + _clearPending: function() { + pendingRecordingUIOverlays = {}; + } +}; +chatClient.$commonApi = $commonApi; +var nopaintAPI = $commonApi.system.nopaint; +startPaintingChangeMonitoring(); +var channel = new BroadcastChannel("aesthetic.computer"); +channel.onmessage = (event) => { + processMessage(event.data); +}; +async function processMessage(msg) { + if (logs.messaging) console.log(`\u{1F5FC} Processing broadcast: ${msg}`); + if (msg.startsWith("painting:") || msg.startsWith("{") && msg.includes('"type":"painting:updated"')) { + const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); + if (isNopaintActive) { + console.log(`\u{1F6AB} CROSS-TAB INTERFERENCE: Receiving painting update during nopaint operation - this may disrupt live preview!`, { + msgPreview: msg.substring(0, 100) + "...", + nopaintState: "painting" + }); + } + await handlePaintingUpdate(msg); + return; + } + if (msg.startsWith("handle:updated")) { + const newHandle = msg.split(":").pop(); + HANDLE = "@" + newHandle; + if (typeof window !== "undefined") window.acHANDLE = HANDLE; + if (typeof window !== "undefined" && window.acBootCanvas?.setHandle) window.acBootCanvas.setHandle(HANDLE); + send({ type: "handle", content: HANDLE }); + store["handle:received"] = true; + store["handle"] = newHandle; + return; + } + if (msg === "login:success" && !USER) { + $commonApi.net.refresh(); + return; + } + if (msg === "logout:success" && USER) { + $commonApi.net.refresh(); + return; + } +} +async function handlePaintingUpdate(msg) { + try { + const data = JSON.parse(msg); + if (data.type !== "painting:updated") return; + if (data.tabId === $commonApi._tabId) { + console.log(`\u{1F3A8} SKIPPED: Own message (${data.action})`); + return; + } + if (!$commonApi.system) { + console.log(`\u{1F3A8} SKIPPED: No system available`); + return; + } + if (data.action === "resized" && data.metadata?.source === "screen_resize" || data.action === "updated" && (data.source === "resize" || data.source === "crop")) { + const width2 = data.width || data.metadata?.width; + const height2 = data.height || data.metadata?.height; + console.log(`\u{1F4D0} DIMENSION CHANGE EVENT: ${width2}x${height2} (source: ${data.source || data.metadata?.source})`); + $commonApi._processingResize = true; + $commonApi._awaitingResizeStorage = { width: width2, height: height2, timestamp: data.timestamp }; + console.log(`\u{1F4D0} Waiting for storage completion for ${data.source} ${width2}x${height2}...`); + return; + } + if (data.action === "storage_complete" && (data.source === "resize" || data.source === "crop")) { + const awaitingResize = $commonApi._awaitingResizeStorage; + console.log(`\u{1FA9D} RECEIVED storage_complete:`, { + source: data.source, + timestamp: data.timestamp, + awaitingResize, + timestampMatch: awaitingResize && data.timestamp === awaitingResize.timestamp + }); + if (awaitingResize && data.timestamp === awaitingResize.timestamp) { + console.log(`\u{1F4D0} Storage completed for ${data.source} ${awaitingResize.width}x${awaitingResize.height}, applying...`); + try { + const storedPainting = await store.retrieve("painting", "local:db"); + if (storedPainting) { + $commonApi.system.painting = { + width: storedPainting.width, + height: storedPainting.height, + pixels: new Uint8ClampedArray(storedPainting.pixels) + }; + store["painting"] = { + width: storedPainting.width, + height: storedPainting.height, + pixels: storedPainting.pixels + }; + lastPaintingHash = generatePaintingHash($commonApi.system.painting); + const kidlispInstance = getGlobalKidLisp(); + if (kidlispInstance) { + kidlispInstance.setAPI($commonApi); + console.log(`\u{1F3AF} KidLisp API refreshed with ${data.source} painting: ${storedPainting.width}x${storedPainting.height}`); + } + if ($commonApi.system.nopaint) { + $commonApi.system.nopaint.needsPresent = true; + } + $commonApi.needsPaint(); + console.log(`\u{1F4D0} Screen updated after ${data.source}: ${storedPainting.width}x${storedPainting.height}`); + } + } catch (error) { + console.error(`\u{1F4D0} ERROR applying ${data.source} after storage completion:`, error); + } + delete $commonApi._awaitingResizeStorage; + $commonApi._processingResize = false; + return; + } + } + $commonApi._processingBroadcast = true; + try { + const storedPainting = await store.retrieve("painting", "local:db"); + if (storedPainting) { + $commonApi.system.painting = { + width: storedPainting.width, + height: storedPainting.height, + pixels: new Uint8ClampedArray(storedPainting.pixels) + }; + store["painting"] = { + width: storedPainting.width, + height: storedPainting.height, + pixels: storedPainting.pixels + }; + lastPaintingHash = generatePaintingHash($commonApi.system.painting); + const kidlispInstance = getGlobalKidLisp(); + if (kidlispInstance) { + kidlispInstance.setAPI($commonApi); + console.log(`\u{1F3AF} KidLisp API refreshed with updated painting`); + } + if ($commonApi.system.nopaint) { + $commonApi.system.nopaint.needsPresent = true; + } + $commonApi.needsPaint(); + if ((data.action === "cleared" || data.action === "new" || data.resetTransform) && $commonApi.system.nopaint) { + console.log(`\u{1F3AF} ${data.action?.toUpperCase() || "TRANSFORM_RESET"}: Forcing nopan (reset transform) on receiver`); + $commonApi.system.nopaint.resetTransform({ + system: $commonApi.system, + screen: $commonApi.screen + }); + } + console.log(`\u{1F3A8} PAINTING SYNCED: ${storedPainting.width}x${storedPainting.height}`, { + hash: generatePaintingHash($commonApi.system.painting)?.substr(0, 8), + fromHash: data.hash || "unknown", + firstPixels: Array.from($commonApi.system.painting.pixels.slice(0, 12)) + // First 3 pixels (RGBA) + }); + } else { + } + } catch (storageError) { + console.error(`\u{1F3A8} ERROR loading from storage:`, storageError); + } + } catch (error) { + console.error(`\u{1F3A8} ERROR handling painting update:`, error); + if (typeof msg === "string" && msg.startsWith("painting:")) { + const action = msg.split(":")[1]; + console.log(`\u{1F3A8} FALLBACK: Processing legacy format ${action}`); + try { + const storedPainting = await store.retrieve("painting", "local:db"); + if (storedPainting) { + $commonApi.system.painting = { + width: storedPainting.width, + height: storedPainting.height, + pixels: new Uint8ClampedArray(storedPainting.pixels) + }; + store["painting"] = { + width: storedPainting.width, + height: storedPainting.height, + pixels: storedPainting.pixels + }; + const kidlispInstance = getGlobalKidLisp(); + if (kidlispInstance) { + kidlispInstance.setAPI($commonApi); + } + if ($commonApi.system.nopaint) { + $commonApi.system.nopaint.needsPresent = true; + } + $commonApi.needsPaint(); + } + } catch (storageError) { + console.error(`\u{1F3A8} ERROR in fallback storage load:`, storageError); + } + } + } finally { + setTimeout(() => { + $commonApi._processingBroadcast = false; + }, 50); + } +} +$commonApi.broadcast = (msg) => { + processMessage(msg); + channel.postMessage(msg); +}; +$commonApi.broadcastPaintingUpdate = (action, data = {}) => { + if (!$commonApi.system?.painting) return; + const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); + if (isNopaintActive) { + console.log(`\u{1F6AB} NOPAINT INTERFERENCE: Attempting to broadcast "${action}" during nopaint operation - this may disrupt live preview!`, { + action, + source: data.source, + nopaintState: "painting" + }); + } + const now = Date.now(); + if (now - lastBroadcastTime < broadcastThrottleDelay) { + return; + } + lastBroadcastTime = now; + if (!$commonApi._tabId) { + $commonApi._tabId = Math.random().toString(36).substr(2, 9); + } + const message = { + type: "painting:updated", + action, + tabId: $commonApi._tabId, + timestamp: now, + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height, + ...data + }; + channel.postMessage(JSON.stringify(message)); +}; +$commonApi.broadcastPaintingUpdateImmediate = (action, data = {}) => { + if (!$commonApi.system?.painting || $commonApi._processingBroadcast) return; + const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); + if (isNopaintActive) { + console.log(`\u{1F6AB} IMMEDIATE NOPAINT INTERFERENCE: Attempting immediate broadcast "${action}" during nopaint operation!`, { + action, + source: data.source, + nopaintState: "painting" + }); + } + const now = Date.now(); + if (now - (lastBroadcastTime || 0) < 16) { + return; + } + if (!$commonApi._tabId) { + $commonApi._tabId = Math.random().toString(36).substr(2, 9); + } + const paintingHash = generatePaintingHash($commonApi.system.painting); + const message = { + type: "painting:updated", + action, + tabId: $commonApi._tabId, + timestamp: now, + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height, + hash: paintingHash?.substr(0, 8), + immediate: true, + ...data + }; + setTimeout(() => { + store["painting"] = { + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height, + pixels: $commonApi.system.painting.pixels + }; + store.persist("painting", "local:db"); + if (data.source === "resize" || data.source === "crop") { + const storageCompleteMessage = { + type: "painting:updated", + action: "storage_complete", + source: data.source, + tabId: message.tabId, + timestamp: message.timestamp, + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height + }; + channel.postMessage(JSON.stringify(storageCompleteMessage)); + console.log(`\u{1FA9D} STORAGE COMPLETE hook fired for ${data.source} ${$commonApi.system.painting.width}x${$commonApi.system.painting.height}`, { + timestamp: message.timestamp, + tabId: message.tabId.substr(0, 4) + "..." + }); + } + }, 0); + lastPaintingHash = paintingHash; + lastBroadcastTime = now; + channel.postMessage(JSON.stringify(message)); +}; +function generatePaintingHash(painting2) { + if (!painting2?.pixels) return null; + let hash = painting2.width * 31 + painting2.height * 37; + for (let i2 = 0; i2 < painting2.pixels.length; i2 += 100) { + hash = (hash << 5) - hash + painting2.pixels[i2] & 4294967295; + } + return hash.toString(36); +} +function startPaintingChangeMonitoring() { + if (paintingChangeCheckInterval) return; + if ($commonApi.system?.painting) { + lastPaintingHash = generatePaintingHash($commonApi.system.painting); + } + enableFrameBasedMonitoring(); + paintingChangeCheckInterval = setInterval(() => { + if (!$commonApi.system?.painting || frameBasedMonitoring) return; + const currentHash2 = generatePaintingHash($commonApi.system.painting); + if (lastPaintingHash !== null && currentHash2 !== lastPaintingHash && !$commonApi._processingBroadcast) { + $commonApi._processingBroadcast = true; + store["painting"] = { + width: $commonApi.system.painting.width, + height: $commonApi.system.painting.height, + pixels: $commonApi.system.painting.pixels + }; + store.persist("painting", "local:db"); + $commonApi.broadcastPaintingUpdate("updated", { + source: "interval_monitor", + hash: currentHash2.substr(0, 8) + }); + setTimeout(() => { + $commonApi._processingBroadcast = false; + }, 100); + } + lastPaintingHash = currentHash2; + }, 1e3); +} +async function session(slug, forceProduction = false, service) { + let endPoint = "/session/" + slug; + const params = { service }; + if (forceProduction) params.forceProduction = 1; + endPoint += "?" + new URLSearchParams(params); + const req = await fetch(endPoint); + let session2; + if (req.status === 200 || req.status === 304) { + session2 = await req.text().then((text) => { + try { + return JSON.parse(text); + } catch (e2) { + return text; + } + }); + } else { + session2 = await req.text(); + } + if (typeof session2 === "string") return session2; + return session2; +} +var $updateApi = {}; +var QUAD = { + type: "quad", + positions: [ + // Triangle 1 (Left Side) + [-1, -1, 0, 1], + // Bottom Left + [-1, 1, 0, 1], + // Top Left + [1, 1, 0, 1], + // Top Right + // Triangle 2 (Right Side) + [-1, -1, 0, 1], + // Bottom Left + [1, -1, 0, 1], + // Bottom Right + [1, 1, 0, 1] + // Top Right + ], + indices: [ + // These are not re-used for now. + // One + 0, + 1, + 2, + //Two + 3, + 4, + 5 + ] +}; +var CUBEL = { + type: "line", + positions: [ + // Back + [-0.5, -0.5, 0.5, 1], + // Down + [-0.5, 0.5, 0.5, 1], + [-0.5, 0.5, 0.5, 1], + // Across + [0.5, 0.5, 0.5, 1], + [0.5, 0.5, 0.5, 1], + // Up + [0.5, -0.5, 0.5, 1], + [0.5, -0.5, 0.5, 1], + // Back + [-0.5, -0.5, 0.5, 1], + // Front + [-0.5, -0.5, -0.5, 1], + // Down + [-0.5, 0.5, -0.5, 1], + [-0.5, 0.5, -0.5, 1], + // Across + [0.5, 0.5, -0.5, 1], + [0.5, 0.5, -0.5, 1], + // Up + [0.5, -0.5, -0.5, 1], + [0.5, -0.5, -0.5, 1], + // Back + [-0.5, -0.5, -0.5, 1], + // Bars (back to front) + [-0.5, -0.5, 0.5, 1], + // Top Left + [-0.5, -0.5, -0.5, 1], + [-0.5, 0.5, 0.5, 1], + // Bottom Left + [-0.5, 0.5, -0.5, 1], + [0.5, 0.5, 0.5, 1], + // Up + [0.5, 0.5, -0.5, 1], + [0.5, -0.5, 0.5, 1], + // Back + [0.5, -0.5, -0.5, 1] + ] +}; +var ORIGIN = { + type: "line", + positions: [ + [-0.5, 0, 0, 1], + // Horizontal X + [0.5, 0, 0, 1], + [0, 0, -0.5, 1], + // Horizontal Z + [0, 0, 2, 1], + [0, -0.5, 0, 1], + // Vertical + [0, 0.5, 0, 1] + ], + colors: [ + [255, 0, 0, 255], + [255, 0, 0, 255], + [0, 255, 0, 255], + [0, 255, 0, 255], + [0, 0, 255, 255], + [0, 0, 255, 255] + ] +}; +var TRI = { + type: "triangle", + positions: [ + [-1, -1, 0, 1], + // Bottom Left + [0, 1, 0, 1], + // Top Left + [1, -1, 0, 1] + // Top Right + // Triangle 2 (Right Side) + ], + indices: [0, 1, 2] +}; +var LINE = { + type: "line", + positions: [ + [0, 0, 0, 1], + // Bottom + [0, 1, 0, 1] + // Top + ], + indices: [0, 1] +}; +function ink() { + const foundColor = findColor2(...arguments); + if (inkFloodLoggingEnabled2()) { + console.log( + `${inkFloodLogPrefix2()}\u{1F58D}\uFE0F INK DEBUG`, + { + args: cloneArgsForLog(arguments), + resolved: cloneColorForLog2(foundColor) + } + ); + } + const result = color(...foundColor); + if (inkFloodLoggingEnabled2()) { + console.log( + `${inkFloodLogPrefix2()}\u{1F58D}\uFE0F INK APPLIED`, + { + color: cloneColorForLog2(result) + } + ); + } + return result; +} +function ink2() { + if (arguments[0] === null) { + return color2(null); + } else { + return color2(...findColor2(...arguments)); + } +} +function initializeGlobalKidLisp(api) { + if (!globalKidLispInstance2) { + if (typeof ensureGlobalInstance === "function") { + globalKidLispInstance2 = ensureGlobalInstance(); + } else if (typeof getGlobalInstance === "function") { + globalKidLispInstance2 = getGlobalInstance() || new KidLisp(); + } else { + globalKidLispInstance2 = new KidLisp(); + } + globalKidLispInstance2.setAPI(api); + if (typeof window !== "undefined") { + window.__acGlobalKidLispInstance = globalKidLispInstance2; + } + if (PERF_MODE) { + globalKidLispInstance2.startPerformanceMonitoring(); + } + if (AUTO_SCALE_MODE) { + globalKidLispInstance2.enableAutoDensity(); + } + } + return globalKidLispInstance2; +} +function getGlobalKidLisp() { + return globalKidLispInstance2; +} +function updateKidLispAudio2(audioData) { + const payload = audioData && typeof audioData === "object" ? { ...audioData, __source: audioData.__source || "embedded:paintApi" } : audioData; + if (globalKidLispInstance2 && globalKidLispInstance2.updateAudioGlobals) { + globalKidLispInstance2.updateAudioGlobals(payload); + } else if (payload?.amp > 0) { + console.warn("\u{1F50A} updateKidLispAudio: no instance!", !!globalKidLispInstance2, !!globalKidLispInstance2?.updateAudioGlobals); + } +} +var $paintApi = { + // 1. Composite functions (that use $activePaintApi) + // (Must be unwrapped) + // Prints a line of text using the default / current global font. + // Argument options: + // text, pos: {x, y, center}, bg (optional) + // Parameters: + // text, x, y, options, wordWrap, customTypeface + // text, pos, bg, bounds, wordWrap = true, customTypeface + write: function() { + let text = arguments[0], pos, bg, bounds, wordWrap = true, customTypeface = null, rotation = 0; + if (text === void 0 || text === null || text === "" || !tf) + return $activePaintApi; + text = typeof text === "object" && text !== null ? JSON.stringify(text) : text.toString(); + if (typeof arguments[1] === "number") { + pos = { x: arguments[1], y: arguments[2] }; + const options = arguments[3]; + bg = options?.bg; + bounds = options?.bounds; + wordWrap = options?.wordWrap === void 0 ? wordWrap : options.wordWrap; + customTypeface = options?.typeface; + rotation = options?.rotation ?? options?.angle ?? 0; + } else { + pos = arguments[1]; + bg = arguments[2]; + bounds = arguments[3]; + wordWrap = arguments[4] === void 0 ? wordWrap : arguments[4]; + customTypeface = arguments[5]; + rotation = pos?.rotation ?? pos?.angle ?? 0; + } + if (customTypeface) { + const resolvedTypeface = resolveTypefaceInstance(customTypeface); + if (resolvedTypeface) { + ensureTypefaceLoaded(resolvedTypeface); + customTypeface = resolvedTypeface; + } + } + const hasColorCodes2 = textContainsColorCodes(text); + if (hasColorCodes2) { + const originalColor = $activePaintApi.inkrn(); + let cleanText = ""; + let charColors = []; + let currentColor = null; + const segments = splitColorCodes(text); + for (let i2 = 0; i2 < segments.length; i2++) { + if (i2 % 2 === 0) { + const segment = segments[i2]; + for (let j = 0; j < segment.length; j++) { + cleanText += segment[j]; + charColors.push(currentColor); + } + } else { + const colorStr = segments[i2]; + if (!colorStr) { + continue; + } + const normalized = colorStr.trim(); + const lower = normalized.toLowerCase(); + if (!normalized) { + continue; + } + if (lower === "reset" || lower === "default" || lower === "base") { + currentColor = null; + continue; + } + if (normalized.includes(",")) { + const parts = normalized.split(",").map((n2) => { + const parsed = parseInt(n2.trim(), 10); + return Number.isFinite(parsed) ? parsed : 0; + }); + while (parts.length < 3) parts.push(0); + if (parts.length === 3) parts.push(255); + currentColor = parts.slice(0, 4); + } else if (lower === "transparent" || lower === "clear") { + currentColor = [0, 0, 0, 0]; + } else { + const resolved = findColor2(normalized); + if (Array.isArray(resolved)) { + currentColor = resolved.slice(); + } else if (resolved && typeof resolved === "object") { + currentColor = { ...resolved }; + } else if (resolved !== void 0) { + currentColor = resolved; + } else { + currentColor = null; + } + } + } + } + if (cleanText.trim().length === 0) { + return $activePaintApi; + } + const scale8 = pos?.size || 1; + if (bounds) { + const tb = $commonApi.text.box(cleanText, pos, bounds, scale8, wordWrap, customTypeface); + if (!tb || !tb.lines) { + return $activePaintApi; + } + const charMap = tb.charMap || []; + tb.lines.forEach((lineText, index) => { + const renderedLine = typeof lineText === "string" ? lineText : lineText?.join?.(" ") || ""; + const sourceIndices = charMap[index] || []; + const lineColors = []; + for (let i2 = 0; i2 < renderedLine.length; i2++) { + const sourceIndex = sourceIndices[i2]; + if (typeof sourceIndex === "number" && sourceIndex >= 0 && sourceIndex < charColors.length) { + lineColors.push(charColors[sourceIndex]); + } else { + lineColors.push(null); + } + } + const posWithRotation = { ...tb.pos, rotation }; + (customTypeface || tf)?.print( + $activePaintApi, + posWithRotation, + index, + renderedLine, + bg, + lineColors + ); + }); + } else { + if (cleanText.indexOf("\n") !== -1) { + const lines = cleanText.split("\n"); + const lineHeightGap = 2; + let charIndex = 0; + lines.forEach((line2, index) => { + const lineColors = charColors?.slice( + charIndex, + charIndex + line2.length + ); + (customTypeface || tf)?.print( + $activePaintApi, + { + x: pos?.x, + y: pos ? pos.y + index * (customTypeface || tf).blockHeight + lineHeightGap : void 0, + rotation + }, + 0, + line2, + bg, + lineColors + ); + charIndex += line2.length + 1; + }); + } else { + const posWithRotation = { ...pos, rotation }; + (customTypeface || tf)?.print($activePaintApi, posWithRotation, 0, cleanText, bg, charColors); + } + } + const typefaceName = typeof customTypeface === "string" ? customTypeface : customTypeface?.name; + if (typefaceName === "MatrixChunky8" || typefaceName === "unifont") { + const hasColorAssignments = charColors?.some((color3) => { + if (!color3) return false; + if (Array.isArray(color3)) { + return color3.some((component) => component !== null && component !== void 0); + } + if (typeof color3 === "object") { + return Object.keys(color3).length > 0; + } + return typeof color3 === "string"; + }); + if (!hasColorAssignments) { + const snippet = cleanText.length > 120 ? `${cleanText.slice(0, 117)}\u2026` : cleanText; + if (lastMatrixChunkyWriteDiagnosticLog !== snippet) { + lastMatrixChunkyWriteDiagnosticLog = snippet; + console.warn("\u{1F3A8} MatrixChunky8 HUD charColors missing", { + snippet, + charColorsLength: charColors?.length, + cleanTextLength: cleanText.length + }); + } + } + } + $activePaintApi.ink(...originalColor); + return $activePaintApi; + } + const scale7 = pos?.size || 1; + if (bounds) { + const tb = $commonApi.text.box(text, pos, bounds, scale7, wordWrap, customTypeface); + tb.lines.forEach((lineText, index) => { + const renderedLine = typeof lineText === "string" ? lineText : lineText?.join?.(" ") || ""; + const posWithRotation = { ...tb.pos, rotation }; + (customTypeface || tf)?.print($activePaintApi, posWithRotation, index, renderedLine, bg); + }); + } else { + if (text.indexOf("\n") !== -1) { + const lines = text.split("\n"); + const lineHeightGap = 2; + lines.forEach((line2, index) => { + (customTypeface || tf)?.print( + $activePaintApi, + { + x: pos?.x, + y: pos ? pos.y + index * (customTypeface || tf).blockHeight + lineHeightGap : void 0, + rotation + }, + 0, + line2, + bg + ); + }); + } else { + const actualFont = customTypeface || tf; + const posWithRotation = { ...pos, rotation }; + actualFont?.print($activePaintApi, posWithRotation, 0, text, bg); + } + } + return $activePaintApi; + }, + // 2. Image Utilities + clonePixels: cloneBuffer, + colorsMatch, + color: findColor2, + resize, + // 3. 3D Classes & Objects + Camera, + Form, + Dolly, + sign, + cacheSignGlyph, + // Helper to get glyphs from a typeface for use with sign() + glyphs: (fontName = "MatrixChunky8") => { + const typeface = typefaceCache.get(fontName); + return typeface?.glyphs || {}; + }, + TRI, + QUAD, + LINE, + CUBEL, + ORIGIN, + // Project text glyphs onto a 3D plane, drawing each character's line/point + // commands through a projection function. + // Usage: ink(r,g,b,a).write3D("text", { origin, right, down, project, typeface }, scale) + // origin: [x,y,z] — top-left of the first character in world space + // right: [x,y,z] — unit-ish direction for text flow (gets scaled by glyph size) + // down: [x,y,z] — unit-ish direction for descending lines + // project: ([x,y,z]) => [screenX, screenY] — world-to-screen projection + // typeface: optional typeface name (e.g. "MatrixChunky8") — defaults to active tf + // scale: multiplier for glyph coordinate spacing (default 1) + write3D: function(text, plane, scale7 = 1) { + if (!text || !plane) return $activePaintApi; + const { origin, right, down, project } = plane; + if (!origin || !right || !down || !project) return $activePaintApi; + let face = tf; + if (plane.typeface && typefaceCache.has(plane.typeface)) { + face = typefaceCache.get(plane.typeface); + } + if (!face) return $activePaintApi; + const blockW = face.blockWidth || face.data?.glyphWidth || 6; + const fontData = face.data || {}; + const advances = fontData.advances || {}; + let cursorX = 0; + for (let ci = 0; ci < text.length; ci++) { + const char = text[ci]; + const glyph = face.getGlyph?.(char) || face.glyphs?.[char]; + const advance = advances[char] || face.getAdvance?.(char) || blockW; + if (glyph) { + let offX = 0, offY = 0; + if (glyph.baselineOffset) { + offX = glyph.baselineOffset[0] * scale7; + offY = glyph.baselineOffset[1] * scale7; + } else if (glyph.offset) { + offX = glyph.offset[0] * scale7; + offY = glyph.offset[1] * scale7; + } + const projectPt = (gx, gy) => { + const px = cursorX + (gx * scale7 + offX); + const py = gy * scale7 + offY; + const wx = origin[0] + px * right[0] + py * down[0]; + const wy = origin[1] + px * right[1] + py * down[1]; + const wz = origin[2] + px * right[2] + py * down[2]; + return project([wx, wy, wz]); + }; + const [refX, refY] = projectPt(0, 0); + const [refRX, refRY] = projectPt(1, 0); + const [refDX, refDY] = projectPt(0, 1); + const pxW = Math.sqrt((refRX - refX) ** 2 + (refRY - refY) ** 2) || 1; + const pxH = Math.sqrt((refDX - refX) ** 2 + (refDY - refY) ** 2) || 1; + const pxSize = Math.max(1, Math.round((pxW + pxH) / 2)); + const pixelCb = plane.pixelCallback; + const renderPixel = (gx, gy) => { + const [sx, sy] = projectPt(gx + 0.5, gy + 0.5); + const bx = Math.floor(sx - pxSize / 2); + const by = Math.floor(sy - pxSize / 2); + if (pixelCb) pixelCb(bx, by, gx, gy, ci); + $activePaintApi.box(bx, by, pxSize, pxSize); + }; + if (glyph.pixels) { + for (let row = 0; row < glyph.pixels.length; row++) { + const pixelRow = glyph.pixels[row]; + if (!Array.isArray(pixelRow)) continue; + for (let col = 0; col < pixelRow.length; col++) { + if (pixelRow[col] === 1) renderPixel(col, row); + } + } + } + if (glyph.commands) { + for (const { name, args } of glyph.commands) { + if (name === "point") { + renderPixel(args[0], args[1]); + } else if (name === "line") { + renderPixel(args[0], args[1]); + renderPixel(args[2], args[3]); + const [s1x, s1y] = projectPt(args[0], args[1]); + const [s2x, s2y] = projectPt(args[2], args[3]); + $activePaintApi.line( + Math.floor(s1x), + Math.floor(s1y), + Math.floor(s2x), + Math.floor(s2y) + ); + } + } + } + } + cursorX += advance * scale7; + } + return $activePaintApi; + } +}; +function normalizeAngle(angle3) { + return (angle3 % 360 + 360) % 360; +} +var turtleAngle = 270; +var turtleDown = false; +var turtlePosition = { x: 0, y: 0 }; +var formsToClear = []; +var backgroundColor3D = [0, 0, 0, 255]; +var formsSent = {}; +function form(forms, cam2 = $commonApi.system.fps.doll.cam, { cpu, background } = { + cpu: true, + keep: true, + background: backgroundColor3D +}) { + if (forms == null || forms?.length === 0) return; + if (cpu === true) { + if (formReframing) { + cam2.resize(); + formReframing = false; + } + if (Array.isArray(forms)) + forms.filter(Boolean).forEach((form2) => form2.graph(cam2)); + else forms.graph(cam2); + } else { + if (!Array.isArray(forms)) forms = [forms]; + formsToClear.forEach((id) => delete formsSent[id]); + formsToClear.length = 0; + const formsToSend = []; + forms.filter(Boolean).forEach((form2) => { + if (formsSent[form2.uid] === void 0 && form2.vertices.length > 0) { + formsToSend.push(form2); + formsSent[form2.uid] = form2; + form2.gpuVerticesSent = form2.vertices.length; + form2.gpuReset = false; + } else { + let msgCount = 0; + if (form2.gpuRecolored === true) { + formsToSend.push({ + update: "form:color", + uid: form2.uid, + color: form2.color + }); + msgCount += 1; + form2.gpuRecolored = false; + } + if (form2.gpuTransformed === true) { + formsToSend.push({ + update: "form:transform", + uid: form2.uid, + rotation: form2.rotation, + position: form2.position, + scale: form2.scale + }); + form2.gpuTransformed = false; + msgCount += 1; + } + if (form2.vertices.length > form2.gpuVerticesSent || form2.gpuReset) { + formsToSend.push({ + update: "form:buffered:add-vertices", + uid: form2.uid, + reset: form2.gpuReset, + vertices: form2.vertices.slice(form2.gpuVerticesSent), + length: form2.vertices.length, + // TODO: These aren't being used anymore / they are generated from the GPU. + pastLength: form2.gpuVerticesSent + }); + form2.gpuReset = false; + form2.gpuVerticesSent = form2.vertices.length; + msgCount += 1; + } + if (msgCount === 0) { + formsToSend.push({ + update: "form:touch", + uid: form2.uid + }); + } + } + }); + if (formsToSend.length === 0) return; + if (background !== backgroundColor3D) { + send({ + type: "gpu-event", + content: { + type: "background-change", + content: background + } + }); + backgroundColor3D = background; + } + send({ + type: "forms", + content: { + forms: formsToSend, + cam: { + position: cam2.position, + rotation: cam2.rotation, + scale: cam2.scale, + fov: cam2.fov, + near: cam2.near, + far: cam2.far + }, + color: color() + } + }); + } +} +var codeCache = { + memory: /* @__PURE__ */ new Map(), + // code -> @handle/slug resolution cache + reverseMemory: /* @__PURE__ */ new Map(), + // @handle/slug -> code reverse lookup cache + async resolve(code2) { + if (this.memory.has(code2)) { + return this.memory.get(code2); + } + const response = await fetch(`/api/painting-code?code=${code2}`); + if (!response.ok) { + throw new Error(`Code #${code2} not found`); + } + const data = await response.json(); + let path; + if (data.handle && data.handle !== "anon") { + path = `@${data.handle}/${data.slug}`; + } else { + path = data.slug; + } + this.memory.set(code2, path); + this.reverseMemory.set(path, code2); + return path; + }, + // Store a mapping from timestamp to code (called by profile.mjs and other pieces) + storeCode(slug, handle2, code2) { + if (!slug || !code2) return; + const normalizedHandle = handle2?.replace(/^@+/, ""); + const key = normalizedHandle ? `@${normalizedHandle}/${slug}` : slug; + this.reverseMemory.set(key, code2); + this.memory.set(code2, key); + }, + // Get code from timestamp+handle + getCode(slug, handle2) { + const normalizedHandle = handle2?.replace(/^@+/, ""); + const key = normalizedHandle ? `@${normalizedHandle}/${slug}` : slug; + return this.reverseMemory.get(key); + } +}; +if (typeof window !== "undefined") { + window.acCodeCacheStats = () => { + console.log("\u{1F511} Code resolution cache stats:", { + cacheSize: codeCache.memory.size, + codes: Array.from(codeCache.memory.entries()).map(([code2, path]) => ({ + code: `#${code2}`, + resolvedTo: path + })) + }); + }; +} +async function prefetchPicture(code2) { + const originalCode = code2; + let actualCode = code2; + if (code2.startsWith("#")) { + actualCode = code2.slice(1); + } + if (paintings[code2] === "fetching") return; + if (paintings[code2] && paintings[code2] !== "fetching") { + return; + } + if (imageCache.has(code2)) { + paintings[code2] = imageCache.get(code2); + return; + } + if (imageCache.isCacheable(code2)) { + const cachedImage = await imageCache.loadFromPersistent(code2); + if (cachedImage) { + paintings[code2] = cachedImage; + return; + } + } + const globalScope = (function() { + if (typeof globalThis !== "undefined") return globalThis; + if (typeof self !== "undefined") return self; + if (typeof window !== "undefined") return window; + return {}; + })(); + if (globalScope.acEMBEDDED_PAINTING_BITMAPS) { + const embeddedBitmap = globalScope.acEMBEDDED_PAINTING_BITMAPS[code2] || globalScope.acEMBEDDED_PAINTING_BITMAPS["#" + actualCode] || globalScope.acEMBEDDED_PAINTING_BITMAPS[actualCode]; + if (embeddedBitmap) { + console.log("\u{1F5BC}\uFE0F Loaded from embedded bundle:", code2); + paintings[code2] = embeddedBitmap; + return; + } + } + paintings[code2] = "fetching"; + const cacheAndStore = async (img) => { + paintings[code2] = img; + if (imageCache.isCacheable(code2)) { + await imageCache.set(code2, img); + } + }; + if (actualCode.startsWith("http")) { + $commonApi.get.picture(actualCode).then(({ img }) => cacheAndStore(img)).catch(() => { + delete paintings[code2]; + }); + } else if (actualCode.includes("/")) { + const [author, paintingSlug] = actualCode.split("/"); + $commonApi.get.painting(paintingSlug).by(author).then(({ img }) => cacheAndStore(img)).catch(() => { + delete paintings[code2]; + }); + } else if (actualCode.match(/^\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3}$/)) { + console.warn(`\u26A0\uFE0F Painting timestamp without handle: ${actualCode}`); + delete paintings[code2]; + } else if (actualCode.length === 3 && actualCode.match(/^[a-zA-Z0-9]{3}$/)) { + $commonApi.get.painting(actualCode).by().then(({ img }) => cacheAndStore(img)).catch(() => { + delete paintings[code2]; + }); + } else { + $commonApi.get.painting(actualCode).by().then(({ img }) => cacheAndStore(img)).catch(() => { + delete paintings[code2]; + }); + } +} +var $paintApiUnwrapped = { + // Turtle graphics: 🐢 crawl, left, right, up, down, goto, face + // Move the turtle forward based on angle, wrapping at screen edges. + crawl: (steps = 1) => { + const w = screen.width, h = screen.height; + const x2 = turtlePosition.x + steps * cos4(radians(turtleAngle)); + const y2 = turtlePosition.y + steps * sin5(radians(turtleAngle)); + const wx2 = (x2 % w + w) % w; + const wy2 = (y2 % h + h) % h; + const wrapped = Math.abs(wx2 - x2) > 0.5 || Math.abs(wy2 - y2) > 0.5; + if (turtleDown && !wrapped) { + line(turtlePosition.x, turtlePosition.y, wx2, wy2); + } + turtlePosition.x = wx2; + turtlePosition.y = wy2; + return { x: turtlePosition.x, y: turtlePosition.y }; + }, + // Turn turtle left n degrees. + left: (d2 = 1) => { + turtleAngle = normalizeAngle(turtleAngle - d2); + return turtleAngle; + }, + // Turn turtle right n degrees. + right: (d2 = 1) => { + turtleAngle = normalizeAngle(turtleAngle + d2); + return turtleAngle; + }, + // Turtle pen up. + up: () => { + turtleDown = false; + }, + // Turtle pen down. + down: () => { + turtleDown = true; + }, + // Teleport the turtle position (wraps to screen bounds). + goto: (x = screen.width / 2, y = screen.height / 2) => { + const w = screen.width, h = screen.height; + const wx = (x % w + w) % w; + const wy = (y % h + h) % h; + if (turtleDown) { + line(turtlePosition.x, turtlePosition.y, wx, wy); + } + turtlePosition.x = wx; + turtlePosition.y = wy; + return { x: turtlePosition.x, y: turtlePosition.y }; + }, + face: (angle3 = 0) => { + turtleAngle = normalizeAngle(angle3); + return turtleAngle; + }, + // Shortcuts + // l: graph.line, + // i: ink, + // Defaults + blend: blendMode, + setEraseTarget, + page: function() { + if (arguments[0]?.api) { + } + const buf = arguments[0]; + if (buf === _mainScreenObject && _trueScreenPixels) { + buf.pixels = _trueScreenPixels; + buf.width = _trueScreenWidth; + buf.height = _trueScreenHeight; + } else { + $activePaintApi.screen.width = buf.width; + $activePaintApi.screen.height = buf.height; + $activePaintApi.screen.pixels = buf.pixels; + } + setBuffer(buf); + }, + edit: changePixels, + // Edit pixels by pasing a callback. + // Color + ink: function() { + const out = ink(...arguments); + twoDCommands2.push(["ink", ...out]); + }, + ink2: function() { + const out = ink2(...arguments); + twoDCommands2.push(["ink2", ...out || []]); + }, + // inkrn: () => graph.c.slice(), // Get current inkColor. + // 2D + wipe: function() { + const cc2 = c.slice(0); + const preserveFadeAlpha2 = getPreserveFadeAlpha?.() || false; + if (!preserveFadeAlpha2 && typeof setPreserveFadeAlpha === "function") { + setPreserveFadeAlpha(true); + } + if (arguments.length === 0) { + ink(255, 255, 255); + } else { + ink(...arguments); + } + if ($commonApi.webgpu.enabled) { + send({ + type: "webgpu-command", + content: { + type: "clear", + color: c.slice(0) + } + }); + ink(...cc2); + if (!preserveFadeAlpha2 && typeof setPreserveFadeAlpha === "function") { + setPreserveFadeAlpha(false); + } + return; + } + clear(); + twoDCommands2.push(["wipe", ...c]); + ink(...cc2); + if (!preserveFadeAlpha2 && typeof setPreserveFadeAlpha === "function") { + setPreserveFadeAlpha(false); + } + if (this.kidlispInstance?.layer0) { + this.kidlispInstance.layer0.pixels.fill(0); + } + }, + // Set background fill color for reframe operations (especially for KidLisp pieces) + backgroundFill: function(color3) { + const cc2 = c.slice(0); + if (arguments.length === 0) { + ink(255, 255, 255); + } else { + ink(...arguments); + } + clear(); + twoDCommands2.push(["backgroundFill", ...c]); + ink(...cc2); + }, + // Erase the screen. + clear: function() { + const cc2 = c.slice(0); + ink(0, 0); + clear(); + ink(...cc2); + }, + copy: copy7, + paste: function paste2() { + if (typeof arguments[0] === "string") { + const code2 = arguments[0]; + if (paintings[code2] && paintings[code2] !== "fetching") { + paste(paintings[code2], ...[...arguments].slice(1)); + } else if (paintings[code2] !== "fetching") { + prefetchPicture(code2); + } + } else { + paste(...arguments); + } + }, + // Similar to paste, but always draws from the center of x, y. + // Has partial support for {center, bottom}. 24.02.15.12.19 + stamp: function stamp2() { + let params; + function makeLayout() { + if (typeof params[0] === "object") { + const layout = params[0]; + if (layout.center === "x") { + params[0] = $activePaintApi.screen.width / 2; + } else { + params[0] = 0; + } + if (layout.bottom !== void 0) { + params[1] = $activePaintApi.screen.height - layout.bottom - paintings[code].height / 2; + } else { + params[1] = 0; + } + } + } + if (typeof arguments[0] === "string") { + const code2 = arguments[0]; + params = [...arguments].slice(1); + if (paintings[code2] && paintings[code2] !== "fetching") { + makeLayout(); + stamp(paintings[code2], ...params); + } else if (paintings[code2] !== "fetching") { + prefetchPicture(code2); + } + } else { + params = [...arguments].slice(1); + if (params.length === 0) params = [0, 0]; + makeLayout(); + stamp(arguments[0], ...params); + } + }, + pixel, + plot: function() { + if (arguments.length === 1) { + plot(arguments[0].x, arguments[0].y); + } else { + plot(...arguments); + } + }, + // TODO: Should this be renamed to set? + flood, + compositeLayers, + // GPU-accelerated multi-layer compositing + batchedEffects, + // GPU-accelerated batched effects (zoom+scroll+contrast+brightness in one pass) + point: function() { + const out = point(...arguments); + twoDCommands2.push(["point", ...out]); + }, + line: function() { + if ($commonApi.webgpu.enabled && arguments.length >= 4) { + const [x1, y1, x2, y2] = arguments; + const color3 = c.slice(0); + send({ + type: "webgpu-command", + content: { + type: "line", + x1, + y1, + x2, + y2, + color: color3 + } + }); + return; + } + return line(...arguments); + }, + lineAngle, + pline, + // Set the alpha of every non-transparent pixel in a buffer to a uniform value. + // Useful for per-stroke alpha — must be called after drawing on the buffer. + setBufferAlpha: function(buffer, alpha) { + if (!buffer || !buffer.pixels) return; + const target = Math.round(alpha * 255); + const px = buffer.pixels; + for (let i2 = 3; i2 < px.length; i2 += 4) { + if (px[i2] > 0) px[i2] = target; + } + }, + pppline: pixelPerfectPolyline, + oval, + circle, + pie, + tri, + poly, + box, + shape, + grid, + draw, + setShowClippedWireframes, + clearWireframeBuffer, + drawBufferedWireframes, + getRenderStats, + printLine, + // TODO: This is kind of ugly and I need a state machine for type. + form, + pan, + unpan, + savepan, + loadpan, + mask, + unmask, + steal, + putback, + skip, + scroll, + flip: flip2, + spin, + sort, + zoom, + suck, + blur: function(radius = 1) { + return blur(radius); + }, + sharpen: function(strength = 1) { + return sharpen(strength); + }, + invert: function() { + return invert4(); + }, + contrast, + shear, + resetScrollState: function() { + return resetScrollState(); + }, + noise16, + noise16DIGITPAIN, + noise16Aesthetic, + noise16Sotce, + noiseTinted, + // 🎨 Alpha-blended paste for crossfade compositing + pasteWithAlpha: function pasteWithAlpha(source, x, y, alpha) { + if (!source || !source.pixels || alpha <= 0) return; + const dst = $activePaintApi.screen; + if (!dst || !dst.pixels) return; + const srcPixels = source.pixels; + const dstPixels = dst.pixels; + const sw = source.width, sh = source.height; + const dw = dst.width, dh = dst.height; + if (alpha >= 255) { + $activePaintApi.paste(source, x, y); + return; + } + const alphaFactor = alpha / 255; + for (let sy = 0; sy < sh; sy++) { + const dy = sy + y; + if (dy < 0 || dy >= dh) continue; + for (let sx = 0; sx < sw; sx++) { + const dx = sx + x; + if (dx < 0 || dx >= dw) continue; + const si = (sy * sw + sx) * 4; + const di = (dy * dw + dx) * 4; + let srcA = srcPixels[si + 3]; + if (srcA === 0 && (srcPixels[si] || srcPixels[si + 1] || srcPixels[si + 2])) { + srcA = 255; + } + if (srcA === 0) continue; + const ea = srcA * alphaFactor + 1 | 0; + const inv = 256 - ea; + dstPixels[di] = ea * srcPixels[si] + inv * dstPixels[di] >> 8; + dstPixels[di + 1] = ea * srcPixels[si + 1] + inv * dstPixels[di + 1] >> 8; + dstPixels[di + 2] = ea * srcPixels[si + 2] + inv * dstPixels[di + 2] >> 8; + dstPixels[di + 3] = Math.min(255, dstPixels[di + 3] + ea); + } + } + }, + // 🎯 Simplified KidLisp integration using global singleton instance + kidlisp: function kidlisp(x = 0, y = 0, width2, height2, source, options = {}) { + if (!globalKidLispInstance2) { + initializeGlobalKidLisp($activePaintApi); + } + if (!width2) width2 = $activePaintApi.screen.width; + if (!height2) height2 = $activePaintApi.screen.height; + const { noCache = false, accumulate: forceAccumulate = false, noPaste = false } = options; + try { + if (!globalKidLispInstance2.persistentPaintings) { + globalKidLispInstance2.persistentPaintings = /* @__PURE__ */ new Map(); + } + let resolvedSource = source; + if (source && source.startsWith && source.startsWith("$") && source.length > 1) { + const cacheId = source.slice(1); + if (!globalKidLispInstance2.singletonDollarCodeCache) { + globalKidLispInstance2.singletonDollarCodeCache = /* @__PURE__ */ new Map(); + } + if (!globalKidLispInstance2.loadingDollarCodes) { + globalKidLispInstance2.loadingDollarCodes = /* @__PURE__ */ new Set(); + } + if (globalKidLispInstance2.singletonDollarCodeCache.has(cacheId)) { + resolvedSource = globalKidLispInstance2.singletonDollarCodeCache.get(cacheId); + } else if (globalKidLispInstance2.loadingDollarCodes.has(cacheId)) { + return null; + } else { + console.log(`\u{1F3AF} Loading ${source} for first time...`); + globalKidLispInstance2.loadingDollarCodes.add(cacheId); + getCachedCodeMultiLevel(cacheId).then((loadedSource) => { + if (loadedSource) { + console.log(`\u{1F3AF} \u2705 Loaded source for ${cacheId}: ${loadedSource.length} chars`); + globalKidLispInstance2.singletonDollarCodeCache.set(cacheId, loadedSource); + } else { + console.warn(`\u274C Could not load source for ${cacheId}`); + } + }).catch((error) => { + console.error(`\u274C Error loading ${cacheId}:`, error); + }).finally(() => { + console.log(`\u{1F3AF} Finished loading attempt for ${cacheId}`); + globalKidLispInstance2.loadingDollarCodes.delete(cacheId); + }); + return null; + } + } + if (!resolvedSource || typeof resolvedSource !== "string") { + return null; + } + const colorWords = ["red", "blue", "green", "yellow", "purple", "orange", "cyan", "magenta", "black", "white", "gray", "grey", "brown", "pink"]; + const firstWord = resolvedSource.trim().split(/\s+/)[0]?.toLowerCase(); + const startsWithColor = colorWords.includes(firstWord); + const accumulate = forceAccumulate || startsWithColor; + const accumulateKey = accumulate ? `auto_${x}_${y}_${width2}_${height2}_${firstWord}` : null; + let regionKey; + let contentKey; + if (accumulate && accumulateKey) { + regionKey = `${x},${y},${width2},${height2}:ACCUMULATE:${accumulateKey}`; + contentKey = `ACCUMULATE:auto_${x}_${y}_${firstWord}`; + } else { + regionKey = `${x},${y},${width2},${height2}:${resolvedSource}`; + contentKey = resolvedSource; + } + let resizedPreviousPainting = null; + let resizedPreviousKey = null; + if (!globalKidLispInstance2.persistentPaintings.has(regionKey)) { + for (const [key, val] of globalKidLispInstance2.persistentPaintings) { + if (key !== regionKey && key.endsWith(`:${contentKey}`)) { + resizedPreviousPainting = val; + resizedPreviousKey = key; + break; + } + } + } + const hasFrameDependentCommands = /\(\s*ink\s*\)|\(\s*color\s*\)|\(\s*rand\s*\)/.test(resolvedSource); + const shouldReset = resolvedSource.includes("wipe") && !accumulate; + const animationCommands = ["rainbow", "zebra", "time", "random", "noise", "clock", "scroll", "zoom", "flip", "contrast", "fade"]; + const hasTimingCommands = /\d+\.?\d*s\b/.test(resolvedSource); + const hasAnimationCommands = animationCommands.some((cmd) => resolvedSource.includes(cmd)); + const isDollarCode = source && source.startsWith && source.startsWith("$"); + const needsFreshExecution = !accumulate && (noCache || isDollarCode && !hasTimingCommands || hasAnimationCommands && !hasTimingCommands); + let painting2; + if (shouldReset || needsFreshExecution || !globalKidLispInstance2.persistentPaintings.has(regionKey)) { + const reason = shouldReset ? "wipe command" : needsFreshExecution ? "animation content" : "first time"; + const previousPainting = !shouldReset && globalKidLispInstance2.persistentPaintings.has(regionKey) ? globalKidLispInstance2.persistentPaintings.get(regionKey) : !shouldReset ? resizedPreviousPainting : null; + if (resizedPreviousKey && resizedPreviousPainting) { + globalKidLispInstance2.persistentPaintings.delete(resizedPreviousKey); + } + painting2 = $activePaintApi.painting(width2, height2, (api) => { + if (previousPainting && needsFreshExecution && !shouldReset) { + api.paste(previousPainting); + } + globalKidLispInstance2.setAPI(api); + if (!api.clock) { + api.clock = { time: () => /* @__PURE__ */ new Date() }; + } + const originalZoom = api.zoom; + const originalScroll = api.scroll || $activePaintApi.scroll; + api.zoom = (...args) => { + if (originalZoom && typeof originalZoom === "function") { + return originalZoom(...args); + } + }; + api.scroll = (dx, dy) => { + if (originalScroll && typeof originalScroll === "function") { + return originalScroll(dx, dy); + } else if ($activePaintApi.scroll && typeof $activePaintApi.scroll === "function") { + return $activePaintApi.scroll(dx, dy); + } + }; + if (!api.num) { + api.num = $activePaintApi.num || { + random: () => Math.random(), + randInt: (min10, max9) => Math.floor(Math.random() * (max9 - min10 + 1)) + min10, + rainbow, + // Use the actual rainbow function from num.mjs + zebra + // Use the actual zebra function from num.mjs + }; + } + if (!api.color) { + api.color = $activePaintApi.color || { + random: () => [ + Math.floor(Math.random() * 256), + Math.floor(Math.random() * 256), + Math.floor(Math.random() * 256) + ] + }; + } + const cssColors3 = cssColors2; + if (cssColors3) { + Object.keys(cssColors3).forEach((colorName) => { + if (!api[colorName]) { + api[colorName] = () => cssColors3[colorName]; + } + }); + } + if (!api.rainbow) { + api.rainbow = () => rainbow(); + } + if (!api.zebra) { + api.zebra = () => zebra(); + } + globalKidLispInstance2.currentInk = null; + const originalInEmbedPhase = globalKidLispInstance2.inEmbedPhase; + const originalIsNestedInstance = globalKidLispInstance2.isNestedInstance; + const originalEmbeddedLayers = globalKidLispInstance2.embeddedLayers; + const originalIsEmbeddedContext = globalKidLispInstance2.isEmbeddedContext; + globalKidLispInstance2.isEmbeddedContext = true; + const hasTimingExpressions = /\d+\.?\d*s(\.\.\.|!)?/.test(resolvedSource); + const hasScrollZoom = /\(\s*(scroll|zoom|flip)\s/.test(resolvedSource); + if (hasTimingExpressions) { + globalKidLispInstance2.embeddedLayers = null; + } else if (hasScrollZoom) { + globalKidLispInstance2.inEmbedPhase = false; + globalKidLispInstance2.isNestedInstance = false; + globalKidLispInstance2.embeddedLayers = null; + } else { + globalKidLispInstance2.inEmbedPhase = true; + globalKidLispInstance2.isNestedInstance = true; + globalKidLispInstance2.embeddedLayers = null; + } + executeLispCode(resolvedSource, api, false); + globalKidLispInstance2.inEmbedPhase = originalInEmbedPhase; + globalKidLispInstance2.isNestedInstance = originalIsNestedInstance; + globalKidLispInstance2.embeddedLayers = originalEmbeddedLayers; + globalKidLispInstance2.isEmbeddedContext = originalIsEmbeddedContext; + globalKidLispInstance2.setAPI($activePaintApi); + }); + globalKidLispInstance2.persistentPaintings.set(regionKey, painting2); + } else { + const existingPainting = globalKidLispInstance2.persistentPaintings.get(regionKey); + painting2 = $activePaintApi.painting(width2, height2, (api) => { + api.paste(existingPainting); + globalKidLispInstance2.setAPI(api); + if (!api.clock) { + api.clock = { time: () => /* @__PURE__ */ new Date() }; + } + const originalZoom = api.zoom; + const originalScroll = api.scroll || $activePaintApi.scroll; + api.zoom = (...args) => { + if (originalZoom && typeof originalZoom === "function") { + return originalZoom(...args); + } + }; + api.scroll = (dx, dy) => { + if (originalScroll && typeof originalScroll === "function") { + return originalScroll(dx, dy); + } else if ($activePaintApi.scroll && typeof $activePaintApi.scroll === "function") { + return $activePaintApi.scroll(dx, dy); + } + }; + if (!api.num) { + api.num = $activePaintApi.num || { + random: () => Math.random(), + randInt: (min10, max9) => Math.floor(Math.random() * (max9 - min10 + 1)) + min10, + rainbow, + // Use the actual rainbow function from num.mjs + zebra + // Use the actual zebra function from num.mjs + }; + } + if (!api.color) { + api.color = $activePaintApi.color || { + random: () => [ + Math.floor(Math.random() * 256), + Math.floor(Math.random() * 256), + Math.floor(Math.random() * 256) + ] + }; + } + const cssColors3 = cssColors2; + if (cssColors3) { + Object.keys(cssColors3).forEach((colorName) => { + if (!api[colorName]) { + api[colorName] = () => cssColors3[colorName]; + } + }); + } + if (!api.rainbow) { + api.rainbow = () => rainbow(); + } + if (!api.zebra) { + api.zebra = () => zebra(); + } + globalKidLispInstance2.currentInk = null; + const originalInEmbedPhase = globalKidLispInstance2.inEmbedPhase; + const originalIsNestedInstance = globalKidLispInstance2.isNestedInstance; + const originalEmbeddedLayers = globalKidLispInstance2.embeddedLayers; + const originalIsEmbeddedContext = globalKidLispInstance2.isEmbeddedContext; + globalKidLispInstance2.isEmbeddedContext = true; + const hasTimingExpressions = /\d+\.?\d*s(\.\.\.|!)?/.test(resolvedSource); + const hasScrollZoom = /\(\s*(scroll|zoom|flip)\s/.test(resolvedSource); + if (hasTimingExpressions) { + globalKidLispInstance2.embeddedLayers = null; + } else if (hasScrollZoom) { + globalKidLispInstance2.inEmbedPhase = false; + globalKidLispInstance2.isNestedInstance = false; + globalKidLispInstance2.embeddedLayers = null; + } else { + globalKidLispInstance2.inEmbedPhase = true; + globalKidLispInstance2.isNestedInstance = true; + globalKidLispInstance2.embeddedLayers = null; + } + executeLispCode(resolvedSource, api, true); + globalKidLispInstance2.inEmbedPhase = originalInEmbedPhase; + globalKidLispInstance2.isNestedInstance = originalIsNestedInstance; + globalKidLispInstance2.embeddedLayers = originalEmbeddedLayers; + globalKidLispInstance2.isEmbeddedContext = originalIsEmbeddedContext; + globalKidLispInstance2.setAPI($activePaintApi); + }); + if (!noCache || !needsFreshExecution) { + globalKidLispInstance2.persistentPaintings.set(regionKey, painting2); + } + } + if (!noPaste && $activePaintApi.paste && painting2) { + $activePaintApi.paste(painting2, x, y); + } + return painting2; + } catch (error) { + console.error("\u{1F6AB} Simple KidLisp error:", error); + if (!projectionMode) { + const originalInk = $activePaintApi.ink(); + $activePaintApi.ink(255, 0, 0); + if ($activePaintApi.write) { + $activePaintApi.write("KidLisp Error", x, y); + } + $activePaintApi.ink(originalInk); + } + return null; + } + }, + // 🎵 Update KidLisp audio globals (accessible to all pieces) + updateKidLispAudio: updateKidLispAudio2 + // glaze: ... +}; +if (typeof globalThis !== "undefined") { + globalThis.$paintApiUnwrapped = $paintApiUnwrapped; + globalThis.wipe = $paintApiUnwrapped.wipe; +} +function executeLispCode(source, api, isAccumulating = false) { + try { + const shouldTrace = $commonApi?.kidlispEnableTrace; + if (shouldTrace) { + enableKidlispTrace(); + clearExecutionTrace(); + } else { + disableKidlispTrace(); + } + globalKidLispInstance2.firstLineColor = null; + globalKidLispInstance2.currentSource = source; + globalKidLispInstance2.parse(source); + if (globalKidLispInstance2.ast) { + globalKidLispInstance2.detectFirstLineColor(); + if (globalKidLispInstance2.firstLineColor && !isAccumulating) { + api.wipe(globalKidLispInstance2.firstLineColor); + } + if (!api.clock) { + api.clock = { time: () => /* @__PURE__ */ new Date() }; + } + if (typeof globalKidLispInstance2.frameCount !== "number") { + globalKidLispInstance2.frameCount = 0; + } + globalKidLispInstance2.frameCount++; + const result = globalKidLispInstance2.evaluate(globalKidLispInstance2.ast, api, globalKidLispInstance2.localEnv); + if (shouldTrace && globalKidLispInstance2.frameCount === 1) { + postExecutionTrace(); + } + } else { + } + } catch (evalError) { + console.error("\u{1F6AB} KidLisp evaluation error:", evalError); + if (!projectionMode) { + api.wipe(60, 0, 0); + api.ink(255, 255, 255); + if (api.write) { + api.write("KidLisp Eval Error", 5, 15); + } + } + } +} +var $activePaintApi; +var paintingAPIid = 0n; +var twoDCommands2 = []; +twoD(twoDCommands2); +var Painting = class _Painting { + #layers = []; + #layer = 0; + api = {}; + inkrn; + // panrn; // In order for this feature to work, translation needs to be stored outside of graph / captured differently? + constructor() { + Object.assign(this.api, $paintApi); + const p = this; + p.api.index = paintingAPIid; + paintingAPIid += 1n; + p.inkrn = c.slice(); + p.pagern = getBuffer(); + function globals(k, args) { + if (k === "page") p.pagern = args[0]; + } + for (const k in $paintApiUnwrapped) { + if (typeof $paintApiUnwrapped[k] === "function") { + p.api[k] = function() { + if (k === "ink") { + $paintApiUnwrapped[k](...arguments); + p.inkrn = c.slice(); + } else { + globals(k, arguments); + } + if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; + const callArgs = arguments; + p.#layers[p.#layer].push([ + k, + () => { + if (k === "ink") { + $paintApiUnwrapped[k](...callArgs); + p.inkrn = c.slice(); + } else { + globals(k, callArgs); + $paintApiUnwrapped[k](...callArgs); + } + } + ]); + return p.api; + }; + } + } + if (!p.api.box && typeof box === "function") { + p.api.box = function() { + const callArgs = arguments; + if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; + p.#layers[p.#layer].push([ + "box", + () => box(...callArgs) + ]); + return p.api; + }; + } + if (!p.api.line && typeof line === "function") { + p.api.line = function() { + const callArgs = arguments; + if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; + p.#layers[p.#layer].push([ + "line", + () => line(...callArgs) + ]); + return p.api; + }; + } + if (!p.api.wipe && typeof $paintApiUnwrapped.wipe === "function") { + p.api.wipe = function() { + const callArgs = arguments; + if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; + p.#layers[p.#layer].push([ + "wipe", + () => $paintApiUnwrapped.wipe(...callArgs) + ]); + return p.api; + }; + } + if (!p.api.ink && typeof $paintApiUnwrapped.ink === "function") { + p.api.ink = function() { + $paintApiUnwrapped.ink(...arguments); + p.inkrn = c.slice(); + return p.api; + }; + } + this.api.layer = function(n2) { + p.#layer = n2; + return p.api; + }; + this.api.painting = function painting2() { + const oldActivePaintApi = $activePaintApi; + const painting3 = new _Painting(); + $activePaintApi = painting3.api; + $activePaintApi.screen = { + width: arguments[0], + height: arguments[1] + // pix gets added in the makeBuffer... + }; + const pix = makeBuffer(...arguments, painting3, $activePaintApi); + $activePaintApi = oldActivePaintApi; + return pix; + }; + this.api.pixel = function() { + return pixel(...arguments); + }; + this.api.inkrn = () => this.inkrn; + this.api.pagern = () => this.pagern; + this.api.abstract = { bresenham: void 0 }; + } + // Paints every layer. + //async paint(immediate = false) { + paint(immediate = false) { + for (let layer of this.#layers) { + layer ||= []; + for (const paint2 of layer) { + paint2[1](); + } + } + this.#layers.length = 0; + this.#layer = 0; + } +}; +var painting = new Painting(); +var glazeAfterReframe; +var lastGap = 8; +$commonApi.resolution = function(width2, height2 = width2, gap = 8) { + if (typeof width2 === "object") { + const props = width2; + height2 = props.height; + width2 = props.width || props.height; + gap = props.gap === 0 ? 0 : props.gap || 8; + } + if (typeof width2 === "number" && typeof height2 === "number") { + width2 = round7(width2); + height2 = round7(height2); + } + if (screen.width === width2 && screen.height === height2 && gap === lastGap) + return; + lastGap = gap; + painting.paint(); + if (width2 === void 0 && height2 === void 0) { + width2 = round7(currentDisplay.width / currentDisplay.subdivisions); + height2 = round7(currentDisplay.height / currentDisplay.subdivisions); + reframe = { + width: void 0, + height: void 0, + gap + }; + } else { + reframe = { width: width2, height: height2, gap }; + } + const oldScreen = { + width: screen.width, + height: screen.height, + pixels: screen.pixels + }; + screen.width = width2; + screen.height = height2; + depthBuffer.length = screen.width * screen.height; + depthBuffer.fill(Number.MAX_VALUE); + writeBuffer.length = 0; + screen.pixels = new Uint8ClampedArray(screen.width * screen.height * 4); + screen.pixels.fill(255); + setBuffer(screen); + paste({ + painting: oldScreen, + crop: new Box(0, 0, oldScreen.width, oldScreen.height) + }); + if (width2 > oldScreen.width || height2 > oldScreen.height) { + const persistentColor = getPersistentFirstLineColor(); + console.log("\u{1F3A8} Resolution function: Screen expansion detected"); + console.log("\u{1F3A8} Resolution function: Old screen:", oldScreen.width, "x", oldScreen.height); + console.log("\u{1F3A8} Resolution function: New screen:", width2, "x", height2); + console.log("\u{1F3A8} Resolution function: Persistent color found:", persistentColor); + let fillSpec = resolveBackgroundFillSpec(persistentColor); + if (!fillSpec && typeof globalKidLispInstance2?.getBackgroundFillColor === "function") { + const fallbackColor = globalKidLispInstance2.getBackgroundFillColor(); + console.log("\u{1F3A8} Post-reframe: Fallback background candidate:", fallbackColor); + fillSpec = resolveBackgroundFillSpec(fallbackColor); + } + if (fillSpec) { + if (fillSpec.type === "fade") { + console.log("\u{1F3A8} Post-reframe: Applying fade background to expansions"); + fillExpandedWithFadePixels( + screen, + width2, + height2, + oldScreen.width, + oldScreen.height, + fillSpec.fadeInfo + ); + } else { + console.log("\u{1F3A8} Post-reframe: Filling expansions with solid color", fillSpec.rgba); + fillExpandedWithSolidPixels( + screen, + width2, + height2, + oldScreen.width, + oldScreen.height, + fillSpec.rgba + ); + } + } else { + console.log("\u{1F3A8} Post-reframe: No background fill spec available for expansion"); + } + } else { + console.log("\u{1F3A8} Resolution function: No screen expansion needed"); + } +}; +var Content = class { + nodes = []; + #id = 0; + constructor() { + } + // Make a request to add new content to the DOM. + add(content) { + this.nodes.push({ id: this.#id }); + this.#id = this.nodes.length - 1; + send({ type: "content-create", content: { id: this.#id, content } }); + return this.nodes[this.nodes.length - 1]; + } + remove() { + send({ type: "content-remove" }); + this.nodes = []; + this.#id = 0; + } + receive({ id, response }) { + this.nodes[id].response = response; + } + //update({ id, msg }) { + // send({ type: "content-update", content: { id, msg } }); + //} +}; +var Microphone = class { + amplitude = 0; + waveform = []; + pitch = 0; + connected = false; + // Flips to true on a callback message from `bios`. + recording = false; + recordingPromise; + permission = ""; + permissionPending = false; + permissionChecked = false; + recordingBuffer = null; + // Live recording buffer for preview + requestPermission() { + if (this.permissionPending || this.permissionChecked) return this; + this.permissionPending = true; + send({ type: "microphone-permission-request" }); + return this; + } + // Note: can send `{monitor: true}` in `options` for audio feedback. + connect(options) { + send({ type: "microphone", content: options }); + return this; + } + disconnect() { + send({ type: "microphone", content: { detach: true } }); + } + poll() { + send({ type: "get-microphone-amplitude" }); + send({ type: "get-microphone-waveform" }); + send({ type: "get-microphone-pitch" }); + if (this.recording) { + send({ type: "get-microphone-recording-buffer" }); + } + } + // Start recording. + rec() { + this.recording = true; + send({ type: "microphone-record" }); + } + // Stop recording. + cut() { + const prom = new Promise((resolve, reject) => { + this.recordingPromise = { resolve, reject }; + }); + send({ type: "microphone-cut" }); + this.recording = false; + return prom; + } +}; +var Speaker = class { + waveforms = { left: [], right: [] }; + amplitudes = { left: [], right: [] }; + frequencies = { left: [], right: [] }; + beat = { detected: false, strength: 0, timestamp: 0 }; + // Add beat detection data + poll() { + send({ type: "get-waveforms" }); + send({ type: "get-amplitudes" }); + send({ type: "get-frequencies" }); + } +}; +var sound; +var soundId = 0n; +var soundTime; +sound = { + bpm: void 0, + sounds: [], + bubbles: [], + farts: [], + kills: [] +}; +var speaker = new Speaker(); +var microphone = new Microphone(); +var gameboy = { + frame: null, + // Current frame data (Uint8ClampedArray) + width: 160, + // Game Boy screen width + height: 144, + // Game Boy screen height + romName: null, + // Currently loaded ROM name + isPlaying: false + // Whether emulator is running +}; +async function resolveUserCode(slug) { + if (!slug || slug.length !== 9 || !slug.startsWith("ac")) return null; + if (!/^ac[0-9]{2}[a-z]{5}$/.test(slug)) return null; + try { + const res = await fetch(`/api/permahandle/${slug}`); + if (!res.ok) return null; + const data = await res.json(); + return data.handle || null; + } catch { + return null; + } +} +var originalHost; +var firstLoad = true; +var notice; +var noticeTimer; +var noticeColor; +var noticeOpts; +var overlay2D = null; +async function load(parsed, fromHistory = false, alias = false, devReload = false, loadedCallback, forceKidlisp = false, forceP5 = false) { + const loadFunctionStartTime = performance.now(); + diskTimings.loadStarted = Math.round(loadFunctionStartTime - diskTimingStart); + let fullUrl, source; + let params, search, colon, hash, path, host = originalHost, text, slug, clockShortcode = null; + if (typeof window !== "undefined" && window.acSPIDER) { + console.log("\u{1F577}\uFE0F SPIDER: Starting load() with:", { + parsed, + fromHistory, + alias, + devReload, + forceKidlisp + }); + } + checkLoadingTimeout(); + if (loading === false) { + loading = true; + loadingStartTime = Date.now(); + } else { + console.warn( + "Coudn't load:", + parsed.path || parsed.name, + "(Already loading.)" + ); + return true; + } + if (!parsed.source && store["publishable-piece"] && parsed.piece === store["publishable-piece"].slug) { + parsed.source = store["publishable-piece"].source; + parsed.name = store["publishable-piece"].slug; + parsed.ext = store["publishable-piece"].ext; + } + if (!parsed.source) { + params = parsed.params; + path = parsed.path; + search = parsed.search; + colon = parsed.colon; + hash = parsed.hash; + host = parsed.host; + slug = parsed.text; + if (typeof window !== "undefined" && window.acSPIDER) { + console.log("\u{1F577}\uFE0F SPIDER: Parsed slug/path:", { + slug, + path, + parsedText: parsed.text, + parsedPath: parsed.path, + hash, + params + }); + } + const routeChannel = (parsed.piece || slug || "").split(":")[0]; + if (routeChannel.startsWith("@") && routeChannel.indexOf("/") !== -1) { + const owned = routeChannel.slice(1); + if (owned !== pieceCodeChannel) { + pieceCodeChannel = owned; + socket?.send("code-channel:sub", pieceCodeChannel); + } + diagnostics.channel(pieceCodeChannel); + } else { + pieceCodeChannel = void 0; + diagnostics.channel(""); + } + if (slug.startsWith("@") && slug.indexOf("/") === -1) { + params = [slug, ...params]; + const hiddenSlug = "profile"; + console.log("Profile Path:", path); + path = [...path.split("/").slice(0, -1), hiddenSlug].join("/"); + } + if (slug && slug.startsWith("*") && slug.length > 1) { + clockShortcode = slug; + const cacheId = slug.slice(1); + const clockParam = `*${cacheId}`; + params = [clockParam, ...params || []]; + slug = "clock"; + path = "aesthetic.computer/disks/clock"; + parsed.text = slug; + parsed.path = path; + parsed.params = params; + } + if (host === "") host = originalHost; + loadFailure = void 0; + host = host.replace(/\/$/, ""); + const { protocol, hostname } = getSafeUrlParts(); + let baseUrl; + if (path.startsWith("aesthetic.computer/")) { + if (typeof window !== "undefined" && window.acSPIDER) console.log("\u{1F577}\uFE0F getPackMode():", getPackMode(), "window.acPACK_MODE:", window.acPACK_MODE, "checkPackMode():", checkPackMode()); + if (getPackMode()) { + baseUrl = "."; + } else { + baseUrl = getBuiltInPieceBaseUrl(); + } + } else { + baseUrl = `${protocol}//${hostname}`; + } + if (typeof window !== "undefined" && window.acSPIDER) console.log("\uFFFD\uFE0F SPIDER MODE Debug:", { protocol, hostname, baseUrl, isSandboxed: isSandboxed(), path, isDevelopment: hostname === "localhost" && typeof location !== "undefined" && location.port }); + let resolvedPath = path; + if (getPackMode() && path.startsWith("aesthetic.computer/")) { + resolvedPath = path; + } else if (baseUrl === "https://aesthetic.computer" && path.startsWith("aesthetic.computer/")) { + resolvedPath = path.substring("aesthetic.computer/".length); + } + if (path.endsWith(".lisp") || path.endsWith(".lua")) { + if (getPackMode()) { + fullUrl = "/" + resolvedPath + "?v=" + Date.now(); + } else { + fullUrl = baseUrl + "/" + resolvedPath + "?v=" + Date.now(); + } + } else { + if (getPackMode()) { + const relativePath = resolvedPath.startsWith("aesthetic.computer/") ? resolvedPath.substring("aesthetic.computer/".length) : resolvedPath; + fullUrl = "../" + relativePath + ".mjs?v=" + Date.now(); + } else { + fullUrl = baseUrl + "/" + resolvedPath + ".mjs?v=" + Date.now(); + } + } + } else { + if (devReload === true && parsed.codeChannel && parsed.codeChannel !== codeChannel && parsed.codeChannel !== pieceCodeChannel) { + console.warn( + "\u{1F645} Not reloading, code channel invalid:", + codeChannel || "N/A" + ); + return; + } + source = parsed.source; + params = parsed.params; + search = parsed.search; + colon = parsed.colon || []; + hash = parsed.hash; + host = parsed.host; + slug = parsed.name; + if (slug && slug.startsWith("*") && slug.length > 1) { + clockShortcode = slug; + const cacheId = slug.slice(1); + const clockParam = `*${cacheId}`; + params = [clockParam, ...params || []]; + slug = "clock"; + if (slug !== "(...)" && !path) path = "aesthetic.computer/disks/clock"; + parsed.name = slug; + parsed.path = path || "aesthetic.computer/disks/clock"; + parsed.params = params; + } + if (parsed.enableTrace) { + enableKidlispTrace(); + } else { + disableKidlispTrace(); + } + if (slug !== "(...)") path = parsed.path; + } + let prefetches; + let blobUrl, sourceCode, originalCode; + try { + if (slug?.split("~")[0] === currentText?.split("~")[0] && sourceCode == currentCode && !devReload) { + const blob = new Blob([currentCode], { type: "application/javascript" }); + blobUrl = URL.createObjectURL(blob); + sourceCode = currentCode; + originalCode = sourceCode; + } else { + let sourceToRun; + let fetchStartTime = performance.now(); + if (slug && slug.startsWith("$") && slug.length > 1) { + const cacheId = slug.slice(1).split(":")[0]; + currentHUDAuthor = null; + currentHUDHits = null; + const globalScope = (function() { + if (typeof window !== "undefined") return window; + if (typeof globalThis !== "undefined") return globalThis; + if (typeof global !== "undefined") return global; + if (typeof self !== "undefined") return self; + return {}; + })(); + if (globalScope.objktKidlispCodes && globalScope.objktKidlispCodes[cacheId]) { + sourceToRun = globalScope.objktKidlispCodes[cacheId]; + currentOriginalCodeId = slug; + currentHUDAuthor = null; + currentHUDHits = null; + } else { + console.log("\u{1F4BE} Loading cached kidlisp code:", cacheId); + try { + console.log("\u{1F50D} Fetching cached code from API..."); + sourceToRun = await getCachedCodeMultiLevel(cacheId); + if (!sourceToRun) { + throw new Error(`Cached code not found: ${cacheId}`); + } + currentOriginalCodeId = slug; + console.log("\u2705 Successfully loaded cached code:", cacheId, `(${sourceToRun.length} chars)`); + fetchKidlispMetadata(cacheId).then((meta2) => { + if (meta2) { + console.log(`\u{1F464} Author: ${meta2.handle || "anonymous"}, Hits: ${meta2.hits}`); + } + }).catch((err) => { + console.warn("\u26A0\uFE0F Failed to fetch KidLisp metadata:", err); + }); + } catch (error) { + console.error("\u274C Failed to load cached code:", cacheId, error); + throw new Error(`Failed to load cached code: ${cacheId}`); + } + } + } else if (fullUrl) { + const pieceSlug = slug?.split("~")[0]; + const cachedPiece = pieceSlug ? pieceCodeCache.get(pieceSlug) : null; + if (cachedPiece) { + console.log(`\u{1F384} Using cached code for: ${pieceSlug}`); + sourceToRun = cachedPiece.code; + } else { + const urlWithoutHash = fullUrl.split("#")[0]; + const filename = urlWithoutHash.split("/").pop(); + if (getPackMode() && filename.endsWith(".mjs")) { + } + let response; + if (logs.loading) console.log("\u{1F4E5} Loading from url:", fullUrl); + const fetchStartTime2 = performance.now(); + send({ + type: "boot-log", + content: `fetching: ${path}` + }); + { + const maxAttempts = 3; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + response = await fetch(fullUrl, { cache: "no-store" }); + break; + } catch (fetchErr) { + if (attempt >= maxAttempts) throw fetchErr; + console.warn(`\u{1F504} Piece fetch attempt ${attempt}/${maxAttempts} failed, retrying in ${attempt}s...`); + await new Promise((r2) => setTimeout(r2, 1e3 * attempt)); + } + } + } + const fetchEndTime = performance.now(); + diskTimings.fetchComplete = Math.round(fetchEndTime - diskTimingStart); + if (response.status === 404 || response.status === 403) { + const extension = path.endsWith(".lisp") ? ".lisp" : path.endsWith(".lua") ? ".lua" : ".mjs"; + const { protocol } = getSafeUrlParts(); + const anonUrl = protocol + "//art.aesthetic.computer/" + path.split("/").pop() + extension + "#" + Date.now(); + if (logs.loading) + console.log("\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1} Attempting to load piece from anon url:", anonUrl); + response = await fetch(anonUrl, { cache: "no-store" }); + if (response.status === 404 || response.status === 403) + throw new Error(response.status); + } + sourceToRun = await response.text(); + } + } else { + sourceToRun = source; + } + if (sourceToRun.startsWith("(") || sourceToRun.startsWith(";") || forceKidlisp || slug === "(...)" || path === "(...)" || path && path.endsWith(".lisp") || slug && slug.startsWith("$") && slug.length > 1 || // Cached codes are always kidlisp + // Pack mode KidLisp pieces - but NOT if acPACK_COLOPHON explicitly says isKidLisp: false + typeof window !== "undefined" && window.acPACK_PIECE && slug === window.acPACK_PIECE && window.acPACK_COLOPHON?.piece?.isKidLisp !== false) { + sourceCode = sourceToRun; + originalCode = sourceCode; + pieceMetadata = { + code: slug || "kidlisp", + trustLevel: "kidlisp", + anonymous: true + }; + const isPackMode = typeof window !== "undefined" && window.acPACK_MODE || typeof globalThis !== "undefined" && globalThis.acPACK_MODE; + if (!isPackMode) { + log.lisp.log("Initializing KidLisp piece..."); + } + const compileStartTime = performance.now(); + const fetchElapsed = Math.round(compileStartTime - fetchStartTime); + if (typeof send === "function") { + send({ + type: "boot-log", + content: `compiling kidlisp (fetch: ${fetchElapsed}ms)` + }); + } else { + log.lisp.warn("send function not available for boot-log"); + } + initPersistentCache(store); + imageCache.init(store); + loadedModule = module(sourceToRun, path && path.endsWith(".lisp")); + const compileEndTime = performance.now(); + const compileElapsed = Math.round(compileEndTime - compileStartTime); + diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart); + if (!isPackMode) { + log.lisp.success(`KidLisp module loaded (${compileElapsed}ms)`); + } + send({ + type: "boot-log", + content: `kidlisp compiled (${compileElapsed}ms)` + }); + send({ + type: "boot-file", + content: { filename: path, source: sourceCode.slice(0, 8e3) } + }); + if (devReload) { + store["publishable-piece"] = { + slug, + source: sourceToRun, + ext: "lisp" + }; + if (logs.loading) + console.log("\u{1F48C} Publishable:", store["publishable-piece"]); + } + } else if (forceP5 || parsed?.ext === "js" || path && path.endsWith(".js")) { + sourceCode = sourceToRun; + originalCode = sourceCode; + pieceMetadata = { + code: slug || "p5", + trustLevel: "p5", + anonymous: true + }; + const compileStartTime = performance.now(); + const fetchElapsed = Math.round(compileStartTime - fetchStartTime); + send({ + type: "boot-log", + content: `compiling p5 (fetch: ${fetchElapsed}ms)` + }); + loadedModule = await makeP5WorkerModule({ slug, source: sourceToRun }); + const compileEndTime = performance.now(); + const compileElapsed = Math.round(compileEndTime - compileStartTime); + diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart); + send({ + type: "boot-log", + content: `p5 compiled (${compileElapsed}ms)` + }); + send({ + type: "boot-file", + content: { filename: path, source: sourceCode.slice(0, 8e3) } + }); + if (devReload) { + store["publishable-piece"] = { + slug, + source: sourceToRun, + ext: "js" + }; + if (logs.loading) + console.log("\u{1F48C} Publishable:", store["publishable-piece"]); + } + } else if (parsed?.ext === "lua" || path && path.endsWith(".lua") || fullUrl && fullUrl.includes(".lua") || sourceToRun.trim().startsWith("--") && /(?:^|\n)\s*function\s+(setup|draw)\s*\(/.test(sourceToRun)) { + sourceCode = sourceToRun; + originalCode = sourceCode; + pieceMetadata = { + code: slug || "l5", + trustLevel: "l5", + anonymous: true + }; + const compileStartTime = performance.now(); + const fetchElapsed = Math.round(compileStartTime - fetchStartTime); + send({ + type: "boot-log", + content: `compiling lua (fetch: ${fetchElapsed}ms)` + }); + loadedModule = await module2(sourceToRun); + const compileEndTime = performance.now(); + const compileElapsed = Math.round(compileEndTime - compileStartTime); + diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart); + send({ + type: "boot-log", + content: `lua compiled (${compileElapsed}ms)` + }); + send({ + type: "boot-file", + content: { filename: path, source: sourceCode.slice(0, 8e3) } + }); + if (devReload) { + store["publishable-piece"] = { + slug, + source: sourceToRun, + ext: "lua" + }; + if (logs.loading) + console.log("\u{1F48C} Publishable:", store["publishable-piece"]); + } + } else { + if (devReload) { + store["publishable-piece"] = { slug, source: sourceToRun }; + if (logs.loading) + console.log("\u{1F48C} Publishable:", store["publishable-piece"].slug); + } + imageCache.init(store); + const isBuiltInDisk = path && path.includes("aesthetic.computer/disks/"); + if (isBuiltInDisk) { + pieceMetadata = { code: slug || "system", trustLevel: "trusted", anonymous: false }; + } else if (slug && !devReload) { + try { + if (pieceMetadata?.code) { + clearPermissions(pieceMetadata.code); + } + pieceMetadata = await fetchPieceMetadata(slug); + if (logs.loading) { + console.log(`\u{1F512} Piece metadata:`, { + code: pieceMetadata.code, + trustLevel: pieceMetadata.trustLevel, + anonymous: pieceMetadata.anonymous + }); + } + } catch (err) { + console.warn(`\u26A0\uFE0F Failed to fetch piece metadata, defaulting to untrusted:`, err); + pieceMetadata = { code: slug, trustLevel: "untrusted", anonymous: true }; + } + } else { + pieceMetadata = { code: slug || "unknown", trustLevel: "untrusted", anonymous: true }; + } + originalCode = sourceToRun; + const updatedCode = updateCode(sourceToRun, host, debug3); + prefetches = updatedCode.match(/"(@\w[\w.]*\/[^"]*)"/g)?.map((match) => match.slice(1, -1)); + const blob = new Blob([updatedCode], { + type: "application/javascript" + }); + blobUrl = URL.createObjectURL(blob); + const importStartTime = performance.now(); + const importFetchElapsed = Math.round(importStartTime - fetchStartTime); + send({ + type: "boot-log", + content: `importing module (fetch: ${importFetchElapsed}ms)` + }); + sourceCode = updatedCode; + { + const maxImportAttempts = 3; + for (let attempt = 1; attempt <= maxImportAttempts; attempt++) { + try { + loadedModule = await import(blobUrl); + break; + } catch (importErr) { + const isNetworkError = importErr.toString().includes("Failed to fetch"); + if (attempt < maxImportAttempts && isNetworkError) { + console.warn(`\u{1F504} Module import attempt ${attempt}/${maxImportAttempts} failed, retrying in ${1.5 * attempt}s...`); + URL.revokeObjectURL(blobUrl); + blobUrl = URL.createObjectURL(new Blob([updatedCode], { type: "application/javascript" })); + await new Promise((r2) => setTimeout(r2, 1500 * attempt)); + } else { + throw importErr; + } + } + } + } + const importEndTime = performance.now(); + const importElapsed = Math.round(importEndTime - importStartTime); + diskTimings.compileComplete = Math.round(importEndTime - diskTimingStart); + if (logs.loading) console.log(`\u2705 Module imported (${importElapsed}ms)`); + send({ + type: "boot-log", + content: `module imported (${importElapsed}ms)` + }); + send({ + type: "boot-file", + content: { filename: path, source: sourceCode.slice(0, 8e3) } + // Limit size + }); + } + } + } catch (err) { + const moduleLoadErrorTime = performance.now(); + console.log("\u{1F7E1} Error loading mjs module:", err); + const isFetchError = err.message === "404" || err.message === "403"; + const isNetworkImportError = err.toString().includes("Failed to fetch dynamically imported module"); + const isModuleImportError = isNetworkImportError || err.toString().includes("Unexpected token") || err.toString().includes("SyntaxError") || err.toString().includes("Cannot use import"); + if (fullUrl && !fullUrl.includes(".lisp") && !fullUrl.includes(".lua") && !fullUrl.includes(".js?") && isFetchError && !isModuleImportError) { + try { + const fallbackExts = [".js", ".lua", ".lisp"]; + let loadedFromFallback = false; + let fallbackError = null; + for (const fallbackExt of fallbackExts) { + let fallbackUrl = fullUrl.replace(".mjs", fallbackExt); + if (logs.loading) { + console.log(`\u{1F4E5} Loading ${fallbackExt.slice(1)} from url:`, fallbackUrl); + } + let response = await fetch(fallbackUrl, { cache: "no-store" }); + let resolvedUrl = fallbackUrl; + if (response.status === 404 || response.status === 403) { + const { protocol } = getSafeUrlParts(); + const anonUrl = protocol + "//art.aesthetic.computer/" + path.split("/").pop() + fallbackExt + "#" + Date.now(); + if (logs.loading) { + console.log("\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1} Attempting to load piece from anon url:", anonUrl); + } + response = await fetch(anonUrl, { cache: "no-store" }); + if (response.status === 404 || response.status === 403) { + fallbackError = new Error(response.status); + continue; + } + resolvedUrl = anonUrl; + } + sourceCode = await response.text(); + originalCode = sourceCode; + if (fallbackExt === ".js") { + pieceMetadata = { + code: slug || "p5", + trustLevel: "p5", + anonymous: true + }; + loadedModule = await makeP5WorkerModule({ slug, source: sourceCode }); + if (devReload) { + store["publishable-piece"] = { slug, source: sourceCode, ext: "js" }; + } + } else if (fallbackExt === ".lua") { + pieceMetadata = { + code: slug || "l5", + trustLevel: "l5", + anonymous: true + }; + loadedModule = await module2(sourceCode); + if (devReload) { + store["publishable-piece"] = { slug, source: sourceCode, ext: "lua" }; + } + } else { + pieceMetadata = { + code: slug || "kidlisp", + trustLevel: "kidlisp", + anonymous: true + }; + loadedModule = module(sourceCode, true); + if (devReload) { + store["publishable-piece"] = { slug, source: sourceCode, ext: "lisp" }; + } + } + loadedFromFallback = true; + break; + } + if (!loadedFromFallback) { + throw fallbackError || new Error("404"); + } + if (devReload && logs.loading) { + console.log("\u{1F48C} Publishable:", store["publishable-piece"]); + } + } catch (err2) { + console.error( + `\u{1F621} "${path}" load failure:`, + err2, + "\u{1F4BE} First load:", + firstLoad + ); + loadFailure = err2; + $commonApi.net.loadFailureText = err2.message + "\n" + sourceCode; + loading = false; + if (firstLoad && (err2.message === "404" || err2.message === "403")) { + const handle2 = await resolveUserCode(slug); + if (handle2) { + $commonApi.jump(`@${handle2}`); + } else { + $commonApi.jump(`404~${slug}`); + } + } else { + if (currentText !== "prompt") + $commonApi.notice(":(", ["red", "yellow"]); + } + return false; + } + } else { + if (isNetworkImportError) { + console.error( + `\u{1F621} "${path}" module import failed (network error loading dependencies after ${3} retries):`, + err + ); + } else if (isModuleImportError) { + console.error( + `\u{1F621} "${path}" module import failed (JS error in the piece):`, + err + ); + } else { + console.error( + `\u{1F621} "${path}" load failure:`, + err, + "\u{1F4BE} First load:", + firstLoad + ); + } + loadFailure = err; + $commonApi.net.loadFailureText = err.message + "\n" + (sourceCode || ""); + loading = false; + if (firstLoad && (err.message === "404" || err.message === "403")) { + const handle2 = await resolveUserCode(slug); + if (handle2) { + $commonApi.jump(`@${handle2}`); + } else { + $commonApi.jump(`404~${slug}`); + } + } else { + $commonApi.notice(":(", ["red", "yellow"]); + } + return false; + } + } + const moduleCheckTime = performance.now(); + if (loadedModule == null) { + loading = false; + leaving = false; + return false; + } + pieceHistoryIndex += fromHistory === true ? 0 : 1; + if (!debug3 && !firstLoad) { + headers($commonApi.dark); + } + $commonApi.net.devReload = devReload; + $commonApi.debug = debug3; + $commonApi.reload = function reload({ + piece, + name, + source: source2, + codeChannel: codeChannel2, + createCode, + codeId, + // The $code identifier (e.g., "inz") from kidlisp.com + authToken, + // Auth token from kidlisp.com login + enableTrace, + // Enable execution trace for kidlisp.com visualization + language, + // Optional source language (e.g., "lua") + ext, + // Optional source extension hint (e.g., "lua") + liveName + // Optional stable live piece name + } = {}) { + if (checkLoadingTimeout()) { + console.log("\u{1F534} Recovered from stuck loading state"); + } + if (loading && source2 && !name && !piece) { + const queueCount = (reload._queueCount || 0) + 1; + reload._queueCount = queueCount; + if (queueCount > 10) { + console.warn("\u{1F534} Reload queue timeout - forcing loading reset"); + loading = false; + loadingStartTime = null; + reload._queueCount = 0; + } else { + console.log("\u{1F7E1} Queueing reload until current load completes..."); + setTimeout( + () => reload({ + source: source2, + codeId, + createCode, + authToken, + enableTrace, + language, + ext, + liveName + }), + 100 + ); + return; + } + } else { + reload._queueCount = 0; + } + if (loading) { + console.log("\u{1F7E1} A piece is already loading."); + return; + } + if (piece === "*refresh*") { + send({ type: "refresh" }); + } else if (piece === "*piece-reload*") { + $commonApi.load( + { + path: currentPath, + host: currentHost, + search: currentSearch, + colon: currentColon, + params: currentParams, + hash: currentHash, + text: currentText + }, + true, + // fromHistory - don't add to history stack + alias, + true + // devReload + ); + } else if (source2 && !name && !piece) { + const isLuaReload = language === "lua" || ext === "lua"; + currentText = isLuaReload ? liveName || "l5-live" : source2; + currentPath = isLuaReload ? liveName || "l5-live" : source2; + if (isLuaReload) { + $commonApi.load( + { + source: source2, + name: liveName || "l5-live", + ext: "lua", + search: currentSearch, + colon: currentColon, + params: currentParams, + hash: currentHash + }, + true, + // fromHistory - don't add to history stack + alias, + true + // devReload + ); + return; + } + if (codeId) { + currentOriginalCodeId = `$${codeId}`; + } + if (createCode) { + $commonApi.kidlispCreateCode = true; + } else { + $commonApi.kidlispCreateCode = false; + } + if (authToken) { + $commonApi.kidlispAuthToken = authToken; + } + $commonApi.kidlispEnableTrace = enableTrace || false; + if (globalKidLispInstance2) { + globalKidLispInstance2.frameCount = 0; + } + $commonApi.load( + { + source: source2, + // Pass as source so it's used directly + name: "kidlisp-live", + // Give it a name so slug won't be undefined + search: currentSearch, + colon: currentColon, + params: currentParams, + hash: currentHash, + enableTrace + // Pass trace flag + }, + true, + // fromHistory - don't add to history stack + alias, + true, + // devReload + void 0, + // loadedCallback + true + // forceKidlisp - always treat as KidLisp code + ); + } else if (name && source2) { + const routed = currentText?.startsWith("@") && currentText.split("/")[1]?.split(":")[0] === name ? currentText : name; + $commonApi.load({ source: source2, name: routed, codeChannel: codeChannel2 }, false, false, true); + } else { + $commonApi.load( + { + path: currentPath, + host: currentHost, + search: currentSearch, + colon: currentColon, + params: currentParams, + hash: currentHash, + text: currentText + }, + // Use the existing contextual values when live-reloading in debug mode. + true, + // (fromHistory) ... never add any reload to the history stack + alias, + true + // devReload + ); + } + }; + let receiver; + const forceProd = false; + function startSocket() { + if (getPackMode() || typeof window !== "undefined" && window.acSPIDER) { + return; + } + setupRemoteLogging(); + if ( + //parsed.search?.startsWith("preview") || + //parsed.search?.startsWith("icon") + previewOrIconMode + ) { + console.log("\u{1F9E6} Sockets disabled, just grabbing screenshots. \u{1F603}"); + return; + } + socket = new Socket(debug3, send); + const monolith = "monolith"; + session(slug, forceProd, monolith).then(async (sesh) => { + if (typeof sesh === "string") throw new Error(sesh); + const url = new URL(sesh.url); + const udpUrl = new URL(sesh.udp); + let turnHost = null; + const isLocalDev = url.hostname === "localhost" || url.hostname.endsWith(".local"); + if (isLocalDev) { + try { + const devInfoRes = await fetch(`${sesh.url}/dev-info`); + if (devInfoRes.ok) { + const devInfo = await devInfoRes.json(); + if (devInfo.ip) { + turnHost = devInfo.ip; + log.socket.debug("Got TURN host from dev-info:", turnHost); + } + } + } catch (e2) { + log.socket.debug("Could not fetch dev-info for TURN host:", e2.message); + } + } + log.socket.debug("Sending udp:connect to BIOS:", { + url: `${udpUrl.protocol}//${udpUrl.hostname}`, + port: udpUrl.port || (udpUrl.protocol === "https:" ? "443" : "80"), + turnHost + }); + send({ + type: "udp:connect", + content: { + url: `${udpUrl.protocol}//${udpUrl.hostname}`, + port: udpUrl.port || (udpUrl.protocol === "https:" ? "443" : "80"), + turnHost + // Pass TURN host separately for ICE servers + } + }); + let slugBroadcastInterval; + socket?.connect( + url.host + url.pathname, + (id, type, content) => { + if (type === "scream" && socket?.id !== id) { + console.log("\u{1F631} Scream:", content, "\u2757"); + scream = content; + } + if (type === "jump") { + $commonApi.jump(content.piece); + } + if (type === "dev:identity") { + devIdentity = content; + remoteLogSocket = socket; + flushRemoteLogQueue(); + $commonApi?.needsPaint?.(); + return; + } + receiver?.(id, type, content); + }, + $commonApi.reload, + "wss", + () => { + if (USER) socket?.send("login", { user: USER }); + if (HANDLE) { + socket?.send("location:broadcast", { + handle: HANDLE, + slug: currentText, + user: USER + // Include user for identity resolution + }); + slugBroadcastInterval = setInterval(() => { + socket?.send("location:broadcast", { + handle: HANDLE, + slug: "*keep-alive*", + user: USER + // Include user for identity resolution + }); + }, 2500); + } + if (codeChannel) socket?.send("code-channel:sub", codeChannel); + if (pieceCodeChannel) + socket?.send("code-channel:sub", pieceCodeChannel); + updateHUDStatus(); + $commonApi.needsPaint(); + codeChannelAutoLoader?.(); + }, + () => { + updateHUDStatus(); + clearInterval(slugBroadcastInterval); + } + ); + }).catch((err) => { + console.error("Session connection error:", err); + }); + } + clearTimeout(socketStartDelay); + socket?.kill(); + udp?.kill(); + socket = void 0; + socketStartDelay = setTimeout(() => startSocket(), 250); + $commonApi.net.socket = function(receive2) { + receiver = receive2 || (() => { + }); + if (!socket) { + clearTimeout(socketStartDelay); + startSocket(); + } else { + setTimeout(() => { + if (socket?.id) receiver(socket.id, "connected:already"); + }, 10); + } + return socket; + }; + let meta; + if (alias === false) { + let objktContext = null; + if (checkPackMode() && typeof window !== "undefined" && window.acOBJKT_COLOPHON) { + objktContext = { author: window.acOBJKT_COLOPHON.build.author }; + } + const titleSlug = parsed.piece || slug; + const { title, desc, ogImage, twitterImage, icon: icon2 } = metadata( + location.host, + // "aesthetic.computer", + titleSlug, + loadedModule.meta?.({ + ...parsed, + num: $commonApi.num, + store: $commonApi.store + }) || inferTitleDesc(originalCode), + location.protocol, + // Pass the current protocol + objktContext + // Pass OBJKT context if available + ); + meta = { + title, + desc, + // Note: This doesn't auto-update externally hosted module descriptions, and may never need to? 22.07.19.06.00 + img: { + og: ogImage, + twitter: twitterImage, + icon: icon2 + }, + url: "https://aesthetic.computer/" + slug + }; + } + $commonApi.meta = (data) => send({ type: "meta", content: data }); + $commonApi.net.rewrite = (path2, historical = false) => { + if (historical) $commonApi.history.push(path2); + send({ type: "rewrite-url-path", content: { path: path2, historical } }); + }; + $commonApi.net.host = host; + $commonApi.net.web = (url, jumpOut) => { + send({ type: "web", content: { url, blank: jumpOut } }); + }; + $commonApi.net.refresh = () => { + send({ type: "refresh" }); + }; + $commonApi.net.waitForPreload = () => { + send({ type: "wait-for-preload", content: true }); + }; + $commonApi.net.preloaded = () => { + send({ type: "preload-ready", content: true }); + }; + $commonApi.content = new Content(); + $commonApi.dom = {}; + $commonApi.dom.clear = () => { + $commonApi.content.remove(); + }; + $commonApi.dom.html = (strings, ...vars) => { + const processed = defaultTemplateStringProcessor(strings, ...vars); + $commonApi.content.add(processed); + }; + $commonApi.dom.css = (strings, ...vars) => { + const processed = defaultTemplateStringProcessor(strings, ...vars); + $commonApi.content.add(``); + }; + $commonApi.dom.javascript = (strings, ...vars) => { + const processed = defaultTemplateStringProcessor(strings, ...vars); + $commonApi.content.add(`