From 12f2cd64e1239064cb406cf8f164edca4fe8dd8b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 14 Sep 2026 10:33:20 -0400 Subject: [PATCH] hud: the corner label drifts letter by letter on hover, instead of shaking Each glyph rides its own slow sway and upward hop, phase-scattered by the golden ratio so no two move in step; the whole thing eases in over 220ms and settles over 380ms rather than snapping, and the green tint eases with it. The hit box never moves. Labels that truly wrap keep the tint and the text.box path. Worker bundle rebuilt. Co-Authored-By: Claude Fable 5.1 --- .../lib/disk-worker-manifest.json | 8 +- system/public/aesthetic.computer/lib/disk.mjs | 196 +- .../lib/disk.worker.b683a4de659b.mjs | 47892 ++++++++++++++++ 3 files changed, 48063 insertions(+), 33 deletions(-) create mode 100644 system/public/aesthetic.computer/lib/disk.worker.b683a4de659b.mjs diff --git a/system/public/aesthetic.computer/lib/disk-worker-manifest.json b/system/public/aesthetic.computer/lib/disk-worker-manifest.json index fc988726cb..49cda8f6ab 100644 --- 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.a39976bede6e.mjs", - "sha256": "a39976bede6e9247aa30aee604458dc8dbebfc5a4319243704e3b69391d4141e", - "bytes": 1674886, - "sourceSha256": "421c3656a892eb104c6fa85c910771e2e93492a45a70e11c4f2dd09b709b7600", + "filename": "disk.worker.b683a4de659b.mjs", + "sha256": "b683a4de659b3ee774c486b293475dc2775b1007c7f96c484eabf5ee6d1d79c7", + "bytes": 1678367, + "sourceSha256": "1431511503c292d93b21496963624d014d051dca7b2cbcd3a90020c47668632d", "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 index ecb8e0e2b2..3c483cb2f1 100644 --- a/system/public/aesthetic.computer/lib/disk.mjs +++ b/system/public/aesthetic.computer/lib/disk.mjs @@ -374,6 +374,56 @@ const HUD_LABEL_TEXT_MARGIN = 0; const KIDLISP_HUD_WRAP_CHARACTER_LIMIT = 28; const MIN_HUD_WRAP_WIDTH = 120; +// ✨ Corner label hover drift. Each character wanders on its own soft spring — +// a slow sideways sway plus an upward hop — so the word reads as alive and +// pressable while staying legible. Amplitudes are px, speeds are rad/ms +// (periods of 1.5–3s), and the whole thing scales by an eased 0..1 amount so +// it wakes up and settles instead of snapping. The hit box never moves. +const HUD_DRIFT_X = 1.5; +const HUD_DRIFT_Y = 2; +const HUD_DRIFT_EASE_IN_MS = 220; +const HUD_DRIFT_EASE_OUT_MS = 380; +// Golden-ratio scattered phases, so neighbouring letters never move in step. +const HUD_DRIFT_PHASES = Float32Array.from( + { length: 64 }, + (_, i) => ((i * 0.6180339887) % 1) * Math.PI * 2, +); + +function hudDriftEase(amount) { + return amount * amount * (3 - 2 * amount); // smoothstep +} + +function hudDriftX(i, t, ease) { + const p = HUD_DRIFT_PHASES[i & 63]; + return ( + HUD_DRIFT_X * + ease * + (0.65 * Math.sin(t * 0.0021 + p) + 0.35 * Math.sin(t * 0.0037 + p * 1.7)) + ); +} + +function hudDriftY(i, t, ease) { + const p = HUD_DRIFT_PHASES[(i + 17) & 63]; + // |sin| hops: sharp at the floor, soft at the top — the bounce. + return -HUD_DRIFT_Y * ease * Math.abs(Math.sin(t * 0.0028 + p)); +} + +// Resolve a `\code\` (or a shadow's "r,g,b") the way `write` does; `reset` +// falls back to the label's base color. +function hudCodeColor(code, base) { + if (!code) return base; + if (Array.isArray(code)) return code; + const s = code.trim(); + const lower = s.toLowerCase(); + if (lower === "reset" || lower === "default" || lower === "base") return base; + if (s.includes(",")) { + const parts = s.split(",").map((n) => parseInt(n.trim(), 10) || 0); + while (parts.length < 3) parts.push(0); + return parts.slice(0, 4); + } + return graph.findColor(s) ?? base; +} + function resolveTypefaceInstance(typefaceRef) { if (!typefaceRef) return undefined; if (typefaceRef instanceof Typeface) return typefaceRef; @@ -704,6 +754,61 @@ function drawHudLabelText( ); } +// Hover twin of drawHudLabelText: one `write` per character so each can sit +// on its own drift offset. Two passes — all shadows first, then all glyphs — +// so no letter's shadow lands on a drifted neighbour. Color codes are walked +// with splitColorCodes, and `\n` starts a new line; wrapped text doesn't come +// here. `i` counts drawn characters and picks the phase. +function drawHudLabelDrift( + $, + text, + { x, y, typefaceName, typeface, textColor, lineStep, t, ease }, +) { + if (!text) return; + const parts = splitColorCodes(text); + const perCodeShadows = parts.length > 1; // plain labels keep a flat black shadow + const blockWidth = typeface?.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; + for (let pass = 0; pass < 2; pass++) { + const shadow = pass === 0; + let color = textColor; + let cx = x, + cy = y, + i = 0; + for (let p = 0; p < parts.length; p++) { + if (p % 2 === 1) { + color = 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(i, t, ease)); + const dy = Math.round(cy + hudDriftY(i, t, ease)); + if (shadow) { + $.ink( + perCodeShadows + ? hudCodeColor(getShadowColorForText(color), "black") + : "black", + ); + $.write(ch, { x: dx + 1, y: dy + 1 }, undefined, undefined, false, typefaceName); + } else { + $.ink(color); + $.write(ch, { x: dx, y: dy }, undefined, undefined, false, typefaceName); + } + } + cx += typeface?.getAdvance?.(ch) ?? blockWidth; + i++; + } + } + } +} + export const noWorker = { onMessage: undefined, postMessage: undefined }; let ROOT_PIECE = "prompt"; // This gets set straight from the host html file for the ac. @@ -1934,6 +2039,8 @@ let currentPath, currentHUDButtonActive = false, // Global flag to block other button interactions when HUD is active currentHUDButtonDirectTouch = false, // Track if HUD button was directly tapped (not rolled over) currentHUDHovered = false, // Pointer is over the corner label (passive, no press) + currentHUDHoverAmount = 0, // 0..1 — how far the hover drift has eased in + currentHUDHoverClock = 0, // performance.now() of the last ease step currentHUDScrub = 0, currentHUDScrubReadyState = null, // null | "share" | "edit" currentHUDLabelFontName, @@ -10164,6 +10271,8 @@ async function load( currentHUDTextColor = undefined; currentHUDStatusColor = "red"; //undefined; currentHUDButton = undefined; + currentHUDHovered = false; + currentHUDHoverAmount = 0; currentHUDScrub = 0; currentHUDLeftPad = 0; currentHUDQR = null; // Reset QR code when loading new piece @@ -15011,8 +15120,18 @@ async function makeFrame({ data: { type, content } }) { hudAnimationState.labelHeight = bufferH; h = bufferH; - // While hovered the label shakes, so it needs a fresh frame each tick. - if (currentHUDHovered) $api.needsPaint(); + // Ease the hover drift in and out (clamped dt so a stalled tab can't + // leap), then keep painting until it has fully settled. + { + const now = performance.now(); + const dt = Math.min(50, now - currentHUDHoverClock); + currentHUDHoverClock = now; + currentHUDHoverAmount = + currentHUDHovered && !currentHUDButton?.down + ? Math.min(1, currentHUDHoverAmount + dt / HUD_DRIFT_EASE_IN_MS) + : Math.max(0, currentHUDHoverAmount - dt / HUD_DRIFT_EASE_OUT_MS); + } + if (currentHUDHovered || currentHUDHoverAmount > 0) $api.needsPaint(); label = $api.painting(bufferW, bufferH, ($) => { // Ensure label renders with clean pan state @@ -15070,42 +15189,61 @@ async function makeFrame({ data: { type, content } }) { } const baseX = currentHUDLeftPad + qrOffset; - // ✨ Rollover: tint toward the press green and shake ±1px — - // the same "I'm alive" cue a TextButton gives before a press. - const hudHovering = currentHUDHovered && !currentHUDButton?.down; - const shakeX = hudHovering ? Math.round((Math.random() - 0.5) * 2) : 0; - const shakeY = hudHovering ? Math.round((Math.random() - 0.5) * 2) : 0; + // ✨ Rollover: tint toward the press green and let each character + // drift on its own spring (hudDriftX/Y) — the "I'm alive" cue a + // TextButton gives before a press, loosened up per letter. + const hoverEase = hudDriftEase(currentHUDHoverAmount); // Keep text fixed while scrubbing left; only right-scrub shifts label content. - const hudTextX = - baseX + HUD_LABEL_TEXT_MARGIN + Math.max(0, currentHUDScrub) + shakeX; - const hudTextY = 4 + shakeY; // Aligned with the QR (corner-radius safe) + const hudTextX = baseX + HUD_LABEL_TEXT_MARGIN + Math.max(0, currentHUDScrub); + const hudTextY = 4; // Aligned with the QR (corner-radius safe) const typefaceNameForWrite = selectedHudFont; const hasColorCodes = textContainsColorCodes(text); - const baseTextColor = hudHovering - ? num.shiftRGB( - graph.findColor(currentHUDTextColor || "white"), - [0, 255, 0], - 0.4, - ) - : currentHUDTextColor || "white"; + const baseTextColor = + hoverEase > 0 + ? num.shiftRGB( + graph.findColor(currentHUDTextColor || "white"), + [0, 255, 0], + 0.4 * hoverEase, + ) + : currentHUDTextColor || "white"; // 🎨 Character wrapping for KidLisp HUD prompts // Wrap based on screen width minus padding (6px total: 2px margin + 4px padding) // Subtract the left margin since text starts at x=HUD_LABEL_TEXT_MARGIN const wrapBounds = isKidlispPiece ? ($api.screen.width - 6 - HUD_LABEL_TEXT_MARGIN) : undefined; - drawHudLabelText($, text, { - x: hudTextX, - y: hudTextY, - typefaceName: typefaceNameForWrite, - textColor: baseTextColor, - shadowColor: "black", - shadowOffsetX: 1, - shadowOffsetY: 1, - preserveColors: hasColorCodes, - bounds: wrapBounds, - // wordWrap defaults to true when bounds is set, enabling character wrapping - }); + // Drift places glyphs itself, so it takes any label that fits on + // one line — including kidlisp-flagged ones (a piece named after + // a kidlisp word, like `line`, gets wrapBounds too). A label that + // really wraps keeps the tint and lets text.box break its lines. + const fitsOneLine = + !text?.includes("\n") && + (selectedLayout?.wrappedLines?.length ?? 1) <= 1; + if (hoverEase > 0 && (wrapBounds === undefined || fitsOneLine)) { + drawHudLabelDrift($, text, { + x: hudTextX, + y: hudTextY, + typefaceName: typefaceNameForWrite, + typeface: selectedTypeface, + textColor: baseTextColor, + lineStep: hudBlockHeight + 1, + t: currentHUDHoverClock, + ease: hoverEase, + }); + } else { + drawHudLabelText($, text, { + x: hudTextX, + y: hudTextY, + typefaceName: typefaceNameForWrite, + textColor: baseTextColor, + shadowColor: "black", + shadowOffsetX: 1, + shadowOffsetY: 1, + preserveColors: hasColorCodes, + bounds: wrapBounds, + // wordWrap defaults to true when bounds is set, enabling character wrapping + }); + } // ✨ Custom Superscript (e.g. ".com") - draw after the text in cyan if (showCustomSuperscript) { diff --git a/system/public/aesthetic.computer/lib/disk.worker.b683a4de659b.mjs b/system/public/aesthetic.computer/lib/disk.worker.b683a4de659b.mjs new file mode 100644 index 0000000000..c776d59522 --- /dev/null +++ b/system/public/aesthetic.computer/lib/disk.worker.b683a4de659b.mjs @@ -0,0 +1,47892 @@ +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"); + $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; + 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) => { + const prom = new Promise((resolve, reject) => { + serverUpload = { resolve, reject }; + }); + serverUploadProgressReporter = progress; + serverUploadProgressReporter?.(0); + console.log("Painting data:", data); + send({ type: "upload", content: { filename, data, bucket, recordingSlug } }); + 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. + 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) { + 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(`