Files
hermes-agent/skills/creative/pretext/templates/donut-orbit.html

1469 lines
52 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<title>NOUS · pretext</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&display=block" rel="stylesheet">
<style>
:root { color-scheme: dark; }
:root {
--background: color-mix(in srgb, #041C1C 100%, transparent);
--background-base: #041C1C;
--background-alpha: 1;
--background-blend: difference;
--midground: color-mix(in srgb, #ffe6cb 100%, transparent);
--midground-base: #ffe6cb;
--midground-alpha: 1;
--foreground: color-mix(in srgb, #ffffff 0%, transparent);
--foreground-base: #ffffff;
--foreground-alpha: 0;
--foreground-blend: difference;
}
html, body {
margin: 0;
width: 100%;
min-height: 100vh;
overflow: hidden;
background: #000;
}
/* literally center the composition in 100vh */
body {
display: flex;
align-items: center;
justify-content: center;
}
#stage {
position: relative;
z-index: 2;
width: min(1240px, 100vw);
height: min(720px, 100vh);
cursor: grab;
overflow: visible;
}
#stage:active { cursor: grabbing; }
#c,
#orbCanvas {
display: block;
position: absolute;
left: calc((100vw - min(1240px, 100vw)) / -2);
top: calc((100vh - min(720px, 100vh)) / -2);
width: 100vw;
height: 100vh;
pointer-events: none;
}
#c { z-index: 3; }
#orbCanvas {
z-index: 4;
opacity: 1;
}
#noiseCanvas {
display: block;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
#textLayer {
position: absolute;
inset: 0;
z-index: 2;
color: var(--midground-base);
font: 400 10px/15px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
}
#textLayer *::selection {
background: var(--selection-bg, var(--midground));
color: var(--background-base);
}
.flow-line {
position: absolute;
height: 20px;
white-space: pre;
overflow: visible;
color: var(--midground-base);
}
#annotationLayer {
position: absolute;
inset: 0;
z-index: 4;
pointer-events: none;
overflow: visible;
}
.annot {
position: absolute;
pointer-events: none;
will-change: opacity, transform;
}
.tok-keyword { color: color-mix(in srgb, var(--midground-base) 54%, #ff174d); }
.tok-string { color: color-mix(in srgb, var(--midground-base) 52%, #ff6b3d); }
.tok-comment { color: color-mix(in srgb, var(--midground-base) 42%, #7f1d1d); }
.tok-number { color: color-mix(in srgb, var(--midground-base) 50%, #ff2a6d); }
.tok-const { color: color-mix(in srgb, var(--midground-base) 48%, #ff003c); }
.layer { position: fixed; inset: 0; pointer-events: none; }
.bg-lens { z-index: 1; background-color: var(--background); mix-blend-mode: var(--background-blend); }
.vignette { z-index: 99; background: radial-gradient(ellipse at 0% 0%, rgba(255,189,56,0) 60%, rgba(255,189,56,0.35) 100%); mix-blend-mode: lighten; opacity: 0.22; }
.fg-lens { z-index: 100; background-color: var(--foreground); mix-blend-mode: var(--foreground-blend); }
.noise {
z-index: 101;
mix-blend-mode: difference;
}
#noiseCanvas {
width: 100%;
height: 100%;
display: block;
}
.terminal-cursor {
position: absolute;
width: 0.6em;
height: 1em;
background: #fff;
z-index: 6;
pointer-events: none;
mix-blend-mode: difference;
animation: term-blink 1s steps(1, end) infinite;
}
@keyframes term-blink {
0%, 60% { opacity: 1; }
60.01%, 100% { opacity: 0; }
}
.cursor-anchor {
display: inline-block;
width: 0;
height: 1em;
vertical-align: text-top;
pointer-events: none;
}
@font-face {
font-family: "Mondwest";
src: url("https://esm.sh/@nous-research/ui@0.4.0/dist/fonts/Mondwest-Regular.woff2") format("woff2");
font-weight: 400; font-style: normal; font-display: block;
}
@font-face {
font-family: "Geist Mono";
src: url("https://cdn.jsdelivr.net/npm/@fontsource/geist-mono@5.2.5/files/geist-mono-latin-400-normal.woff2") format("woff2");
font-weight: 400; font-style: normal; font-display: block;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.2.5/files/jetbrains-mono-latin-400-normal.woff2") format("woff2");
font-weight: 400; font-style: normal; font-display: block;
}
.font-primer { position: fixed; left: -9999px; top: -9999px; font: 400 26px "Mondwest", serif; }
</style>
</head>
<body>
<div id="stage"><div id="textLayer" aria-label="self-learning source stream"></div><div id="terminalCursor" class="terminal-cursor" hidden></div><canvas id="c"></canvas><canvas id="orbCanvas"></canvas></div>
<div class="layer bg-lens"></div>
<div class="layer vignette"></div>
<div class="layer fg-lens"></div>
<div class="layer noise"><canvas id="noiseCanvas"></canvas></div>
<div class="font-primer" aria-hidden="true">Aa Mondwest priming glyphs 0123456789</div>
<script type="module">
import {
prepareWithSegments,
layoutNextLineRange,
materializeLineRange,
} from "https://esm.sh/@chenglou/pretext@0.0.6";
import gsap from "https://esm.sh/gsap@3.12.5";
import GUI from "https://esm.sh/lil-gui@0.19.2";
const SELF_LEARNING_SOURCE = String.raw`
type ToolId=u16; type FeatureId=u32; type W=f32;
struct Obs{tool:ToolId,reward:f32,lat:u32,feat:&'static[FeatureId],tok:u16}
struct Belief{prior:W,conf:W,last:u64,uses:u32}
#[inline] fn sig(x:f32)->f32{1.0/(1.0+(-x).exp())}
#[inline] fn decay(now:u64,last:u64)->f32{1.0-((now-last)as f32/900.0).min(1.0)*0.22}
#[inline] fn clamp8(x:f32)->f32{x.clamp(-8.0,8.0)}
fn score(theta:&[W],b:&[Belief],o:&Obs)->f32{
let sparse=o.feat.iter().fold(0.0,|a,id|a+theta[*id as usize]);
let belief=b[o.tool as usize].prior*b[o.tool as usize].conf.max(0.15);
sparse+belief-(o.lat as f32*0.00072)-(o.tok as f32*0.00009)
}
fn update(theta:&mut[W],b:&mut[Belief],o:&Obs,now:u64,eta:f32){
let p=sig(score(theta,b,o));
let e=(o.reward-p).clamp(-1.0,1.0);
let step=eta*e/(o.feat.len().max(1)as f32).sqrt();
for id in o.feat{
let w=&mut theta[*id as usize];
*w=clamp8(*w+step-0.0003*w.signum());
}
let slot=&mut b[o.tool as usize];
let d=decay(now,slot.last);
let r=(o.reward-o.lat as f32*0.00035).clamp(-1.0,1.0);
slot.prior=slot.prior*d+r*0.14+e*0.06;
slot.conf=(slot.conf*0.93+r.abs()*0.07).min(1.0);
slot.uses=slot.uses.saturating_add(1);
slot.last=now;
}
fn consolidate(trace:&[Obs],theta:&mut[W],b:&mut[Belief],now:u64){
let eta=0.042/(1.0+(trace.len()as f32*0.003));
let mut credit=1.0;
for o in trace.iter().rev(){
update(theta,b,o,now,eta*credit);
credit*=0.985;
}
}
fn choose<'a>(theta:&[W],b:&[Belief],xs:&'a[Obs],temp:f32)->&'a Obs{
xs.iter().max_by(|a,bx|{
let sa=score(theta,b,a)/temp.max(0.05);
let sb=score(theta,b,bx)/temp.max(0.05);
sa.partial_cmp(&sb).unwrap()
}).unwrap()
}
`;
const CODE_CHUNK = SELF_LEARNING_SOURCE
.trim()
.replace(/\t/g, " ")
.replace(/\n{3,}/g, "\n\n");
const STREAM_REPEAT_COUNT = 6;
const streamCorpus = Array.from({ length: STREAM_REPEAT_COUNT }, () => CODE_CHUNK).join("\n\n");
const stage = document.getElementById("stage");
const textLayer = document.getElementById("textLayer");
const canvas = document.getElementById("c");
const orbCanvas = document.getElementById("orbCanvas");
const ctx = canvas.getContext("2d", { alpha: true });
const orbCtx = orbCanvas.getContext("2d", { alpha: true });
let W = 0, H = 0, DPR = 1;
let canvasLeft = 0, canvasTop = 0, canvasW = 0, canvasH = 0;
let BODY_FONT = '400 10px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
let BODY_LINE_H = 15;
let ASCII_FONT = '8px monospace';
let CELL_W = 4.8;
let CELL_H = 8;
const CHARS = ' ·:;+=░▒▓█';
const EMPTY = 0;
const CHARS_LAST = CHARS.length - 1;
const LOGO = ['N', 'O', 'U', 'S'].map((letter, i) => ({
letter,
rows: [
i === 0
? '██░░░██,██░░░██,███░░██,█░██░██,█░░████,█░░░███,██░░░██,██░░░██,██░░░██'
: i === 1
? '░█████░,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'
: i === 2
? '██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'
: '░█████░,██░░░██,██░░░░░,░████░░,░░░░██░,░░░░░██,██░░░██,██░░░██,░█████░'
][0].split(',')
}));
function hash(x, y) {
const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return n - Math.floor(n);
}
function charAt(t) {
return CHARS[Math.min(CHARS.length - 1, Math.max(0, Math.floor(t * CHARS.length)))];
}
function gridDist(v, step) {
return Math.abs(v / step - Math.round(v / step)) * step;
}
function rot(a, b, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [a * c - b * s, a * s + b * c];
}
function hexToRGB(h) {
const v = parseInt(h.slice(1), 16);
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}
let fontsReady = false;
let bodyPrepared = null;
function rebuildLayouts() {
bodyPrepared = prepareWithSegments(streamCorpus, BODY_FONT, { whiteSpace: "pre-wrap" });
}
const LENS_0 = { bgBlend: "difference", bgColor: "#041C1C", bgOpacity: 1, mgColor: "#ffe6cb", mgOpacity: 1, fgBlend: "difference", fgColor: "#FFFFFF", fgOpacity: 0 };
const LENS_5I = { bgBlend: "multiply", bgColor: "#ffffff", bgOpacity: 1, mgColor: "#fffff5", mgOpacity: 1, fgBlend: "difference", fgColor: "#041a1f", fgOpacity: 1 };
const LENS = { current: null, mgBase: null, depthColors: null };
function colorMix(color, alpha) { return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`; }
function precomputeDepthColors(mgColor) {
const [r, g, b] = hexToRGB(mgColor);
return Array.from({ length: 64 }, (_, i) => {
const depth = i / 63;
const m = 0.4 + depth * 0.6;
return `rgba(${(r * m) | 0},${(g * m) | 0},${(b * m) | 0},${depth})`;
});
}
function applyLens(preset) {
LENS.current = preset;
LENS.mgBase = preset.mgColor;
LENS.depthColors = precomputeDepthColors(preset.mgColor);
const s = document.documentElement.style;
for (const [name, color, alpha] of [["foreground", preset.fgColor, preset.fgOpacity], ["midground", preset.mgColor, preset.mgOpacity], ["background", preset.bgColor, preset.bgOpacity]]) {
s.setProperty(`--${name}`, colorMix(color, alpha));
s.setProperty(`--${name}-base`, color);
s.setProperty(`--${name}-alpha`, `${alpha}`);
}
s.setProperty("--background-blend", preset.bgBlend);
s.setProperty("--foreground-blend", preset.fgBlend);
}
applyLens(LENS_0);
addEventListener("keydown", e => { if (e.key === "x" || e.key === "X") applyLens(LENS.current === LENS_0 ? LENS_5I : LENS_0); });
const SELECTION_COLORS = [
"oklch(85% 0.12 330)",
"oklch(85% 0.12 300)",
"oklch(85% 0.12 270)",
"oklch(85% 0.12 230)",
"oklch(85% 0.12 180)",
"oklch(85% 0.12 150)",
"oklch(85% 0.12 120)",
"oklch(85% 0.12 90)",
"oklch(85% 0.12 60)",
"oklch(85% 0.12 30)",
"oklch(88% 0.10 80)",
];
let selectionIdx = 0;
function cycleSelectionColor() {
requestAnimationFrame(() => {
document.documentElement.style.setProperty("--selection-bg", SELECTION_COLORS[(selectionIdx = (selectionIdx + 1) % SELECTION_COLORS.length)]);
});
}
document.addEventListener("selectstart", cycleSelectionColor);
addEventListener("keydown", e => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
cycleSelectionColor();
e.preventDefault();
const selection = getSelection();
const range = document.createRange();
range.selectNodeContents(textLayer);
selection.removeAllRanges();
selection.addRange(range);
}
});
let asciiMask, asciiChar, asciiGlyph, orbMask, orbChar, orbGlyph, obstacleRows, intervals;
let cols = 0, rows = 0;
let canvasCols = 0, canvasRows = 0;
let lastFrameTime = 0;
const drawnLines = { x: [], y: [], w: [], text: [], count: 0 };
let lineNodes = [];
let terminalAnchorEl = null;
let terminalLineEl = null;
let orbShapeControl = null;
let autoCubeCall = null;
let lastTextSignature = "";
const ORB = {
panX: 0, panY: 0,
orbitX: -1.42, orbitY: 0,
radius: 0.135,
zoom: 3,
cubeScale: 1.8,
autoSpin: 0.18,
lat: 19,
lon: 30,
wire: 0.2,
shape: "sphere",
shapeMix: 0,
drag: null,
positioned: false,
staged: false,
};
const SCENE = {
orbBuildDuration: 1.45,
};
let nousStartTime = null;
const AUTO_CUBE_AFTER_NOUS_RESOLVE = 0.0;
function setOrbShape(shape, { reveal = false, syncControl = true } = {}) {
ORB.shape = shape;
if (syncControl) {
ctrls.orbShape = shape;
orbShapeControl?.updateDisplay();
}
if (reveal) {
gsap.killTweensOf(orbCanvas);
orbCanvas.style.opacity = "1";
}
gsap.killTweensOf(ORB, "shapeMix");
gsap.to(ORB, {
shapeMix: shape === "cube" ? 1 : 0,
duration: Math.max(0.01, ctrls.shapeMorphDuration ?? 0.65),
ease: "power2.inOut",
onUpdate: () => { lastTextSignature = ""; },
});
lastTextSignature = "";
}
function sceneTimeNow() {
return (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001 - sceneEpoch;
}
function armNous(atTime = sceneTimeNow()) {
if (nousStartTime !== null) return;
const startAt = Math.max(0, atTime);
nousStartTime = startAt;
const delay = Math.max(0, startAt - sceneTimeNow()) + Math.max(0, ctrls.orbContainNous ?? 1.0);
gsap.killTweensOf(orbCanvas);
gsap.to(orbCanvas, {
opacity: 0,
duration: Math.max(0.05, ctrls.orbFadeDuration ?? 0.7),
delay,
ease: "power2.out",
});
autoCubeCall?.kill();
const nousResolveAt = startAt + (LOGO.length - 1) * 0.4 + 1.0;
const cubeAt = nousResolveAt + AUTO_CUBE_AFTER_NOUS_RESOLVE;
autoCubeCall = gsap.delayedCall(Math.max(0, cubeAt - sceneTimeNow()), () => {
setOrbShape("cube", { reveal: true, syncControl: false });
});
}
function orbBuildStartTime() {
return Math.max(0, ctrls.orbLead ?? 0);
}
function codeTypingStartTime() {
return Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.62 - 0.3);
}
function logoCenterPan() {
const aspect = cols / rows / 2;
const h = 0.38;
const w = h * 2.6 * 0.55;
const halfStageX = (w / 2 / aspect) * W;
const centerX = W * 0.5;
const centerY = H * 0.5;
return {
panX: centerX / W - 0.5,
panY: centerY / H - 0.5,
left: centerX - halfStageX,
right: centerX + halfStageX,
};
}
function startSceneTimeline() {
if (ORB.staged || !cols || !rows || !W || !H) return;
ORB.staged = true;
const now = sceneTimeNow();
const startAt = Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.72 - now);
gsap.timeline({ delay: startAt })
.to(ORB, {
orbitX: 0.55,
orbitY: `+=${Math.PI * 1.85}`,
duration: 2.4,
ease: "power3.inOut",
onUpdate: () => { lastTextSignature = ""; },
}, "<")
.to(ORB, {
radius: 0.125,
duration: 2.4,
ease: "power3.inOut",
onUpdate: () => { lastTextSignature = ""; },
}, "<")
.to(ORB, {
wire: ctrls.orbWireLow,
duration: ctrls.wireDropDuration,
ease: "power2.inOut",
onUpdate: () => { lastTextSignature = ""; },
onComplete: () => {
armNous(sceneTimeNow() + Math.max(0, ctrls.nousAfterWire));
},
});
}
function resize() {
const r = stage.getBoundingClientRect();
W = Math.max(1, Math.floor(r.width));
H = Math.max(1, Math.floor(r.height));
canvasLeft = r.left;
canvasTop = r.top;
canvasW = Math.max(1, window.innerWidth);
canvasH = Math.max(1, window.innerHeight);
DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(canvasW * DPR);
canvas.height = Math.floor(canvasH * DPR);
orbCanvas.width = Math.floor(canvasW * DPR);
orbCanvas.height = Math.floor(canvasH * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
orbCtx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.font = ASCII_FONT;
orbCtx.font = ASCII_FONT;
CELL_W = ctx.measureText("M").width || CELL_W;
cols = Math.ceil(W / CELL_W);
rows = Math.ceil(H / CELL_H);
canvasCols = Math.ceil(canvasW / CELL_W);
canvasRows = Math.ceil(canvasH / CELL_H);
asciiMask = new Uint8Array(canvasCols * canvasRows);
asciiChar = new Uint8Array(canvasCols * canvasRows);
asciiGlyph = new Array(canvasCols * canvasRows);
orbMask = new Uint8Array(canvasCols * canvasRows);
orbChar = new Uint8Array(canvasCols * canvasRows);
orbGlyph = new Array(canvasCols * canvasRows);
obstacleRows = Array.from({ length: rows }, () => []);
intervals = new Float32Array(64);
if (!ORB.positioned && W > 100 && H > 100) {
ORB.panX = 0;
ORB.panY = 0;
ORB.positioned = true;
}
startSceneTimeline();
}
addEventListener("resize", () => { lastTextSignature = ""; resize(); });
function orbCenter() {
const rNorm = ORB.radius * ORB.zoom;
return {
x: (0.5 + ORB.panX) * W,
y: (0.5 + ORB.panY) * H,
canvasX: (0.5 + ORB.panX) * W + canvasLeft,
canvasY: (0.5 + ORB.panY) * H + canvasTop,
r: rNorm * H,
rNorm,
};
}
stage.addEventListener("pointerdown", e => {
const rect = stage.getBoundingClientRect();
const center = orbCenter();
const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
const dx = e.clientX - rect.left - center.x;
const dy = e.clientY - rect.top - center.y;
if (Math.hypot(dx, dy) > center.r * 1.35 * hitScale) return;
e.preventDefault();
stage.setPointerCapture(e.pointerId);
ORB.drag = {
id: e.pointerId,
mode: (e.ctrlKey || e.metaKey) ? "rotate" : "move",
dx,
dy,
lastX: e.clientX,
lastY: e.clientY,
};
});
stage.addEventListener("pointermove", e => {
if (!ORB.drag || ORB.drag.id !== e.pointerId) return;
if (ORB.drag.mode === "rotate" || e.ctrlKey || e.metaKey) {
const deltaX = e.clientX - ORB.drag.lastX;
const deltaY = e.clientY - ORB.drag.lastY;
ORB.orbitY += (deltaX / Math.max(1, W)) * Math.PI * 2;
ORB.orbitX = Math.max(-1.5, Math.min(1.5, ORB.orbitX + (deltaY / Math.max(1, H)) * 4));
ORB.drag.mode = "rotate";
ORB.drag.lastX = e.clientX;
ORB.drag.lastY = e.clientY;
return;
}
const rect = stage.getBoundingClientRect();
const cx = e.clientX - rect.left - ORB.drag.dx;
const cy = e.clientY - rect.top - ORB.drag.dy;
ORB.panX = cx / W - 0.5;
ORB.panY = cy / H - 0.5;
lastTextSignature = "";
});
stage.addEventListener("wheel", e => {
const rect = stage.getBoundingClientRect();
const center = orbCenter();
const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
const dx = e.clientX - rect.left - center.x;
const dy = e.clientY - rect.top - center.y;
if (Math.hypot(dx, dy) > center.r * 1.5 * hitScale) return;
e.preventDefault();
ORB.zoom *= Math.exp(-e.deltaY * 0.0012);
ORB.zoom = Math.max(0.25, Math.min(12, ORB.zoom));
lastTextSignature = "";
}, { passive: false });
stage.addEventListener("pointerup", e => {
if (ORB.drag?.id === e.pointerId) ORB.drag = null;
});
stage.addEventListener("pointercancel", e => {
if (ORB.drag?.id === e.pointerId) ORB.drag = null;
});
stage.addEventListener("dblclick", e => {
const rect = stage.getBoundingClientRect();
const center = orbCenter();
const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
const dx = e.clientX - rect.left - center.x;
const dy = e.clientY - rect.top - center.y;
if (Math.hypot(dx, dy) > center.r * 1.45 * hitScale) return;
e.preventDefault();
setOrbShape(ORB.shape === "sphere" ? "cube" : "sphere", { reveal: true });
});
function rasterizeNOUS(time) {
asciiMask.fill(0);
asciiGlyph.fill("");
for (const r of obstacleRows) r.length = 0;
// NOUS uses the same cell-shader/reveal language as bb-ascii's source logo.
if (nousStartTime === null || time < nousStartTime) {
mergeObstacleRows();
return;
}
const t = Math.max(0, time - nousStartTime);
const aspect = cols / rows / 2;
const h = 0.38;
const baseW = h * 2.6 * 0.55;
const letterWidth = 7;
const baseLetterPitch = 7.6;
const letterGap = Math.max(0, ctrls.nousLetterSpacing ?? 0);
const letterAdvance = baseLetterPitch + letterGap;
const baseLogoUnits = LOGO.length * baseLetterPitch - 0.6;
const logoUnits = LOGO.length * baseLetterPitch + (LOGO.length - 1) * letterGap - 0.6;
const w = baseW * (logoUnits / baseLogoUnits);
const nousRowMin = new Float32Array(rows);
const nousRowMax = new Float32Array(rows);
nousRowMin.fill(Infinity);
nousRowMax.fill(-Infinity);
for (let y = 0; y < rows; y++) {
const ny = y / rows - 0.5;
if (Math.abs(ny) > h / 2) continue;
for (let x = 0; x < cols; x++) {
const nx = (x / cols - 0.5) * aspect;
if (Math.abs(nx) > w / 2) continue;
const lx = (nx + w / 2) / w;
const ly = (ny + h / 2) / h;
const tx = lx * logoUnits;
const idx = Math.floor(tx / letterAdvance);
const cx = Math.floor(tx - idx * letterAdvance);
const cy = Math.floor(ly * 9);
if (idx < 0 || idx >= LOGO.length || cx >= letterWidth) continue;
const { letter, rows: glyphRows } = LOGO[idx];
const row = glyphRows[cy];
if (!row || cx >= row.length || row[cx] !== '█') continue;
const localT = Math.max(0, t - idx * 0.4);
const phase1 = Math.min(1, localT);
const phase2 = Math.min(1, Math.max(0, localT - 1));
const reveal = phase1 * 1.5 - hash(cx, cy + idx * 10) * 0.5;
if (phase1 < 1 && reveal <= 0) continue;
const morph = phase2 + hash(cx, cy) * 0.3;
const ch = phase1 < 1
? (reveal >= 1 ? letter : charAt(reveal))
: charAt(morph);
const drawC = Math.floor((canvasLeft + x * CELL_W + CELL_W * 0.5) / CELL_W);
const drawR = Math.floor((canvasTop + y * CELL_H + CELL_H * 0.5) / CELL_H);
let cellIdx = -1;
if (drawR >= 0 && drawR < canvasRows && drawC >= 0 && drawC < canvasCols) {
cellIdx = drawR * canvasCols + drawC;
asciiMask[cellIdx] = 1;
asciiChar[cellIdx] = Math.max(0, CHARS.indexOf(ch));
if (CHARS.indexOf(ch) < 0) asciiGlyph[cellIdx] = ch;
}
const x0 = x * CELL_W;
const x1 = x0 + CELL_W;
if (x0 < nousRowMin[y]) nousRowMin[y] = x0;
if (x1 > nousRowMax[y]) nousRowMax[y] = x1;
// bb-ascii turns late morph cells into solid dark cell backgrounds. In
// this transparent/lensed canvas, a full-block char carries that punch.
if (cellIdx >= 0 && phase1 >= 1 && morph > 0.8) asciiChar[cellIdx] = CHARS.length - 1;
}
}
// Slightly dilate/smooth the measured NOUS silhouette so surrounding text
// does not visually "kiss" sharp letter corners (notably on the S).
const smoothMin = new Float32Array(rows);
const smoothMax = new Float32Array(rows);
smoothMin.fill(Infinity);
smoothMax.fill(-Infinity);
for (let y = 0; y < rows; y++) {
if (!Number.isFinite(nousRowMin[y])) continue;
for (let oy = -1; oy <= 1; oy++) {
const yy = y + oy;
if (yy < 0 || yy >= rows) continue;
if (!Number.isFinite(nousRowMin[yy])) continue;
if (nousRowMin[yy] < smoothMin[y]) smoothMin[y] = nousRowMin[yy];
if (nousRowMax[yy] > smoothMax[y]) smoothMax[y] = nousRowMax[yy];
}
}
const pad = Math.max(4, CELL_W * 1.25);
const bleedRows = 2;
for (let y = 0; y < rows; y++) {
if (!Number.isFinite(smoothMin[y])) continue;
for (let oy = -bleedRows; oy <= bleedRows; oy++) {
const row = y + oy;
if (row < 0 || row >= rows) continue;
const bleedPad = pad + Math.abs(oy) * CELL_W * 0.28;
obstacleRows[row].push([
Math.max(0, smoothMin[y] - bleedPad),
Math.min(W, smoothMax[y] + bleedPad),
]);
}
}
mergeObstacleRows();
}
function mergeObstacleRows() {
for (const spans of obstacleRows) {
if (spans.length < 2) continue;
spans.sort((a, b) => a[0] - b[0]);
let w = 0;
for (let i = 1; i < spans.length; i++) {
const last = spans[w], cur = spans[i];
if (cur[0] <= last[1] + CELL_W * 3.1) last[1] = Math.max(last[1], cur[1]);
else spans[++w] = cur;
}
spans.length = w + 1;
}
}
const CUBE_VERTS = [
[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1],
];
const CUBE_EDGES = [
[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [3, 7],
];
function projectCubeVerts(center, radiusPx, orbitX, orbitY, scale = 1) {
const camZ = 3.3;
const projScale = radiusPx * 1.34 * scale;
return CUBE_VERTS.map(([vx, vy, vz]) => {
const [rx, ry, rz] = rotateOrbVec(vx, vy, vz, orbitX, orbitY);
const inv = 1 / (camZ + rz);
return {
x: center.canvasX + rx * projScale * inv,
y: center.canvasY + ry * projScale * inv,
d: (rz + 1) * 0.5,
};
});
}
function convexHull(points) {
if (points.length <= 2) return points.slice();
const pts = points
.map(p => ({ x: p.x, y: p.y }))
.sort((a, b) => (a.x - b.x) || (a.y - b.y));
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
const lower = [];
for (const p of pts) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
lower.push(p);
}
const upper = [];
for (let i = pts.length - 1; i >= 0; i--) {
const p = pts[i];
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
upper.push(p);
}
lower.pop();
upper.pop();
return lower.concat(upper);
}
function makeEmptySpanRows() {
return Array.from({ length: rows }, () => ({ left: Infinity, right: -Infinity }));
}
function addSpan(spanRows, row, left, right) {
if (row < 0 || row >= rows || right <= left) return;
if (left < spanRows[row].left) spanRows[row].left = left;
if (right > spanRows[row].right) spanRows[row].right = right;
}
function collectCubeObstacleRows(center, radiusPx, orbitX, orbitY, obstacleScale) {
const spanRows = makeEmptySpanRows();
if (obstacleScale <= 0.0001) return spanRows;
const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale * obstacleScale)
.map(p => ({ x: p.x - canvasLeft, y: p.y - canvasTop }));
const hull = convexHull(projected);
if (hull.length < 3) return spanRows;
let yMin = Infinity;
let yMax = -Infinity;
for (const p of hull) {
if (p.y < yMin) yMin = p.y;
if (p.y > yMax) yMax = p.y;
}
const r0 = Math.max(0, Math.floor((yMin - CELL_H) / CELL_H));
const r1 = Math.min(rows - 1, Math.ceil((yMax + CELL_H) / CELL_H));
const pad = Math.max(4, CELL_W * 1.6);
for (let row = r0; row <= r1; row++) {
const scanY = row * CELL_H + CELL_H * 0.5;
const xs = [];
for (let i = 0; i < hull.length; i++) {
const a = hull[i];
const b = hull[(i + 1) % hull.length];
const y0 = a.y;
const y1 = b.y;
if ((y0 <= scanY && scanY < y1) || (y1 <= scanY && scanY < y0)) {
const t = (scanY - y0) / (y1 - y0);
xs.push(a.x + (b.x - a.x) * t);
}
}
if (xs.length < 2) continue;
xs.sort((a, b) => a - b);
for (let i = 0; i + 1 < xs.length; i += 2) {
const left = Math.max(0, xs[i] - pad);
const right = Math.min(W, xs[i + 1] + pad);
if (right - left > CELL_W) addSpan(spanRows, row, left, right);
}
}
return spanRows;
}
function collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad) {
const spanRows = makeEmptySpanRows();
const obstacleRadiusNorm = radiusNorm * obstacleScale;
const obstaclePadNorm = (rPad / H) * obstacleScale;
if (obstacleRadiusNorm <= 0.0001) return spanRows;
const radiusPx = radiusNorm * H;
const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);
const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);
for (let r = rMin; r <= rMax; r++) {
const y = r * CELL_H + CELL_H * 0.5;
const stageY = y - canvasTop;
const pyForObstacle = stageY / H - 0.5 - ORB.panY;
if (Math.abs(pyForObstacle) > obstacleRadiusNorm + obstaclePadNorm) continue;
const halfNorm = Math.sqrt(Math.max(0, (obstacleRadiusNorm + obstaclePadNorm) ** 2 - pyForObstacle * pyForObstacle));
const halfPx = halfNorm * H;
const stageRow = Math.floor(stageY / CELL_H);
addSpan(spanRows, stageRow, center.x - halfPx, center.x + halfPx);
}
return spanRows;
}
function pushMorphedObstacleRows(sphereRows, cubeRows, mix) {
const centerX = W * (0.5 + ORB.panX);
for (let row = 0; row < rows; row++) {
const sphereValid = Number.isFinite(sphereRows[row].left);
const cubeValid = Number.isFinite(cubeRows[row].left);
if (!sphereValid && !cubeValid) continue;
const sLeft = sphereValid ? sphereRows[row].left : centerX;
const sRight = sphereValid ? sphereRows[row].right : centerX;
const cLeft = cubeValid ? cubeRows[row].left : centerX;
const cRight = cubeValid ? cubeRows[row].right : centerX;
const left = sLeft + (cLeft - sLeft) * mix;
const right = sRight + (cRight - sRight) * mix;
if (right - left > CELL_W) obstacleRows[row].push([Math.max(0, left), Math.min(W, right)]);
}
}
function rotateOrbVec(vx, vy, vz, orbitX, orbitY) {
let a = rot(vy, vz, orbitX); vy = a[0]; vz = a[1];
a = rot(vx, vz, orbitY); vx = a[0]; vz = a[1];
return [vx, vy, vz];
}
function rasterizeCubeWire(center, radiusPx, buildPhase, orbitX, orbitY, weight = 1) {
const unit = Math.max(1, Math.min(CELL_W, CELL_H));
const wirePx = Math.max(1, ORB.wire * radiusPx * 0.17);
const brush = Math.max(0, Math.ceil((wirePx - unit * 0.45) / unit));
const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale);
for (let ei = 0; ei < CUBE_EDGES.length; ei++) {
const [a, b] = CUBE_EDGES[ei];
const p0 = projected[a];
const p1 = projected[b];
const dx = p1.x - p0.x;
const dy = p1.y - p0.y;
const len = Math.hypot(dx, dy);
const steps = Math.max(2, Math.ceil(len / Math.max(1, unit * 0.45)));
for (let s = 0; s <= steps; s++) {
const t = s / steps;
const px = p0.x + dx * t;
const py = p0.y + dy * t;
const depth = p0.d + (p1.d - p0.d) * t;
const c = Math.floor(px / CELL_W);
const r = Math.floor(py / CELL_H);
for (let oy = -brush; oy <= brush; oy++) {
for (let ox = -brush; ox <= brush; ox++) {
if (brush > 0 && ox * ox + oy * oy > brush * brush + 0.25) continue;
const cc = c + ox;
const rr = r + oy;
if (rr < 0 || rr >= canvasRows || cc < 0 || cc >= canvasCols) continue;
const reveal = buildPhase * 1.45 - hash(cc + ei * 13, rr + ei * 17) * 0.5;
if (buildPhase < 1 && reveal <= 0) continue;
const idx = rr * canvasCols + cc;
const val = buildPhase < 1
? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))
: Math.min(CHARS_LAST, Math.floor((0.45 + depth * 0.55) * CHARS_LAST));
const weightedVal = Math.min(CHARS_LAST, Math.floor(val * weight));
if (weightedVal <= 0) continue;
orbMask[idx] = 1;
orbGlyph[idx] = "";
if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;
}
}
}
}
}
function rasterizeOrb(time) {
orbMask.fill(0);
orbChar.fill(0);
orbGlyph.fill("");
const buildStart = orbBuildStartTime();
if (time < buildStart) return;
const shapeMix = Math.max(0, Math.min(1, ORB.shapeMix ?? (ORB.shape === "cube" ? 1 : 0)));
const sphereWeight = 1 - shapeMix;
const cubeWeight = shapeMix;
const sphereOrbitX = ORB.orbitX;
const sphereOrbitY = ORB.orbitY + time * ORB.autoSpin;
const cubeOrbitX = ORB.orbitX + time * ORB.autoSpin * 0.72;
const cubeOrbitY = ORB.orbitY + time * ORB.autoSpin * 1.35;
const latStep = Math.PI / ORB.lat;
const lonStep = (Math.PI * 2) / ORB.lon;
const rPad = 15.5;
const center = orbCenter();
const radiusPx = center.r;
const radiusNorm = center.rNorm;
const buildPhase = Math.min(1, Math.max(0, (time - buildStart) / SCENE.orbBuildDuration));
const spacePhase = Math.min(1, Math.max(0, (time - buildStart) / Math.max(0.001, ctrls.orbSpaceDuration)));
const spaceGrow = Math.pow(spacePhase, Math.max(0.01, ctrls.orbSpaceEase));
const spaceStart = Math.max(0, Math.min(1, ctrls.orbSpaceStart));
const obstacleScale = spaceStart + (1 - spaceStart) * spaceGrow;
const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);
const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);
const sphereObstacleRows = collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad);
const cubeObstacleRows = collectCubeObstacleRows(center, radiusPx, cubeOrbitX, cubeOrbitY, obstacleScale);
pushMorphedObstacleRows(sphereObstacleRows, cubeObstacleRows, shapeMix);
if (cubeWeight > 0.001) {
rasterizeCubeWire(center, radiusPx, buildPhase, cubeOrbitX, cubeOrbitY, cubeWeight);
}
if (sphereWeight <= 0.001) {
mergeObstacleRows();
return;
}
const cMin = Math.floor((center.canvasX - radiusPx - rPad) / CELL_W);
const cMax = Math.ceil((center.canvasX + radiusPx + rPad) / CELL_W);
for (let r = rMin; r <= rMax; r++) {
const y = r * CELL_H + CELL_H * 0.5;
const stageY = y - canvasTop;
for (let c = cMin; c <= cMax; c++) {
// Direct port of bb-ascii/app/nous/sphere/page.tsx:
// px uses width/height normalization, py is row-normalized, then both
// are offset by pan. This is what makes the sphere circular on screen.
const stageX = c * CELL_W + CELL_W * 0.5 - canvasLeft;
const px = (stageX / W - 0.5 - ORB.panX) * (W / H);
const py = stageY / H - 0.5 - ORB.panY;
const d2 = px * px + py * py;
if (d2 > radiusNorm * radiusNorm) continue;
const pz = Math.sqrt(radiusNorm * radiusNorm - d2);
const depth = (pz / radiusNorm) * 0.5 + 0.5;
let sx = px / radiusNorm, sy = py / radiusNorm, sz = pz / radiusNorm;
let a = rot(sy, sz, sphereOrbitX); sy = a[0]; sz = a[1];
a = rot(sx, sz, sphereOrbitY); sx = a[0]; sz = a[1];
const latD = gridDist(Math.asin(Math.max(-1, Math.min(1, sy))) + Math.PI / 2, latStep);
const lonD = gridDist(Math.atan2(sz, sx) + Math.PI, lonStep);
const wire = ORB.wire * (0.3 + depth * 0.7);
const minD = Math.min(latD, lonD);
if (minD > wire) continue;
const edge = 1 - minD / wire;
if (r >= 0 && r < canvasRows && c >= 0 && c < canvasCols) {
const reveal = buildPhase * 1.5 - hash(c, r) * 0.5;
if (buildPhase < 1 && reveal <= 0) continue;
const idx = r * canvasCols + c;
const val = buildPhase < 1
? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))
: Math.min(CHARS_LAST, Math.floor(edge * (0.4 + depth * 0.6) * CHARS.length));
const weightedVal = Math.min(CHARS_LAST, Math.floor(val * sphereWeight));
if (weightedVal <= 0) continue;
orbMask[idx] = 1;
orbGlyph[idx] = "";
if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;
}
}
}
mergeObstacleRows();
}
let typedChars = 0;
let typedPrepared = null;
let typedDirty = true;
let typedBudget = 0;
function tickTyping(time, dt) {
if (!bodyPrepared) return;
if (time < codeTypingStartTime()) return;
const cps = Math.max(1, ctrls.typeSpeed ?? 42);
typedBudget += dt * cps;
const steps = Math.min(240, Math.floor(typedBudget));
if (steps > 0) {
typedBudget -= steps;
typedChars = Math.min(streamCorpus.length, typedChars + steps);
typedDirty = true;
}
}
function getTypedPrepared() {
if (!typedDirty && typedPrepared) return typedPrepared;
const cap = Math.max(0, Math.floor(typedChars));
const visible = streamCorpus.slice(0, cap);
typedPrepared = prepareWithSegments(visible, BODY_FONT, { whiteSpace: "pre-wrap" });
typedDirty = false;
return typedPrepared;
}
function layoutParagraphs() {
drawnLines.count = 0;
if (typedChars <= 0) return;
const prepared = getTypedPrepared();
ctx.font = BODY_FONT;
const MARGIN_X = Math.max(32, Math.min(72, W * 0.06));
const MIN_LINE_W = 120;
const yStart = Math.max(32, H * 0.08);
const yEnd = H - 32;
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
for (let y = yStart; y < yEnd; y += BODY_LINE_H) {
const row = Math.max(0, Math.min(rows - 1, Math.floor(y / CELL_H)));
const spans = obstacleRows[row] || [];
let n = 0;
let x = MARGIN_X;
for (const [a, b] of spans) {
const left = Math.max(MARGIN_X, a);
const right = Math.min(W - MARGIN_X, b);
if (left - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = left - 10; }
x = Math.max(x, right + 10);
}
if (W - MARGIN_X - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = W - MARGIN_X; }
if (!n) { intervals[n++] = MARGIN_X; intervals[n++] = W - MARGIN_X; }
for (let k = 0; k < n; k += 2) {
const ix0 = intervals[k], ix1 = intervals[k + 1];
const range = layoutNextLineRange(prepared, cursor, ix1 - ix0);
if (!range) return;
const line = materializeLineRange(prepared, range);
const i = drawnLines.count++;
drawnLines.x[i] = ix0;
drawnLines.y[i] = y;
drawnLines.w[i] = ix1 - ix0;
drawnLines.text[i] = line.text;
cursor = range.end;
}
}
}
function escapeHTML(text) {
return text
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function highlightSource(text) {
const token = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\b(?:as|break|const|else|enum|false|fn|for|if|impl|in|let|match|mut|pub|return|self|static|struct|true|type|use|where|while)\b|\b[A-Z_]{2,}\b|\b\d+(?:\.\d+)?\b)/g;
let out = "";
let last = 0;
for (const match of text.matchAll(token)) {
const value = match[0];
out += escapeHTML(text.slice(last, match.index));
const cls = value.startsWith("#")
? "tok-comment"
: value.startsWith('"') || value.startsWith("'")
? "tok-string"
: /^\d/.test(value)
? "tok-number"
: /^[A-Z_]{2,}$/.test(value)
? "tok-const"
: "tok-keyword";
out += `<span class="${cls}">${escapeHTML(value)}</span>`;
last = match.index + value.length;
}
out += escapeHTML(text.slice(last));
return out;
}
function renderTextLayer() {
const parts = [LENS.mgBase, drawnLines.count];
for (let i = 0; i < drawnLines.count; i++) {
parts.push(drawnLines.x[i] | 0, drawnLines.y[i] | 0, drawnLines.w[i] | 0, drawnLines.text[i]);
}
const signature = parts.join("|");
if (signature === lastTextSignature) return;
lastTextSignature = signature;
ctx.font = BODY_FONT;
textLayer.style.color = LENS.mgBase;
textLayer.style.setProperty("--midground-base", LENS.mgBase);
while (lineNodes.length < drawnLines.count) {
const el = document.createElement("div");
el.className = "flow-line";
textLayer.appendChild(el);
lineNodes.push(el);
}
terminalAnchorEl = null;
terminalLineEl = null;
for (let i = 0; i < lineNodes.length; i++) {
const el = lineNodes[i];
if (i >= drawnLines.count) {
el.hidden = true;
continue;
}
const text = drawnLines.text[i];
const isTail = i === drawnLines.count - 1;
el.hidden = false;
el.innerHTML = isTail
? `${highlightSource(text)}<span class="cursor-anchor" aria-hidden="true"></span>`
: highlightSource(text);
el.style.left = `${drawnLines.x[i]}px`;
el.style.top = `${drawnLines.y[i] - BODY_LINE_H + 4}px`;
el.style.width = `${drawnLines.w[i]}px`;
el.style.textAlign = "left";
el.style.wordSpacing = "0px";
if (isTail) {
terminalAnchorEl = el.querySelector(".cursor-anchor");
terminalLineEl = el;
}
}
}
let sceneEpoch = 0;
function restartScene() {
gsap.killTweensOf(ORB);
gsap.killTweensOf(orbCanvas);
autoCubeCall?.kill();
autoCubeCall = null;
ORB.panX = 0;
ORB.panY = 0;
ORB.zoom = ctrls.orbZoom;
ORB.cubeScale = ctrls.cubeScale;
ORB.shape = ctrls.orbShape;
ORB.shapeMix = ctrls.orbShape === "cube" ? 1 : 0;
ORB.orbitX = -1.42;
ORB.orbitY = 0;
ORB.wire = ctrls.orbWire;
orbCanvas.style.opacity = "1";
ORB.staged = false;
ORB.positioned = true;
typedChars = 0;
typedBudget = 0;
typedPrepared = null;
typedDirty = true;
nousStartTime = null;
sceneEpoch = (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001;
lastTextSignature = "";
startSceneTimeline();
}
function applyFontSizes() {
const bodyPx = ctrls.bodyFontSize;
const asciiPx = ctrls.asciiFontSize;
BODY_FONT = `400 ${bodyPx}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;
BODY_LINE_H = Math.round(bodyPx * 1.45);
ASCII_FONT = `${asciiPx}px monospace`;
CELL_H = asciiPx;
textLayer.style.font = `400 ${bodyPx}px/${BODY_LINE_H}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;
rebuildLayouts();
typedPrepared = null;
typedDirty = true;
resize();
lastTextSignature = "";
}
function applyColors() {
if (!LENS.current) return;
LENS.current.bgColor = ctrls.bgColor;
LENS.current.mgColor = ctrls.mgColor;
LENS.current.fgColor = ctrls.fgColor;
applyLens(LENS.current);
lastTextSignature = "";
}
const ctrls = {
bodyFontSize: 10,
asciiFontSize: 8,
autoSpin: 0.18,
orbShape: "sphere",
shapeMorphDuration: 0.65,
cubeScale: 1.8,
orbLead: 0.35,
orbZoom: 3,
orbRadius: 0.135,
orbWire: 0.2,
orbSpaceStart: 0.82,
orbSpaceDuration: 0.7,
orbSpaceEase: 1.1,
orbWireLow: 0.01,
wireDropDuration: 1.2,
orbFadeDuration: 0.85,
orbContainNous: 1.0,
nousAfterWire: 0,
nousLetterSpacing: 2.7,
typeSpeed: 816,
bgColor: "#041C1C",
mgColor: "#ffe6cb",
fgColor: "#FFFFFF",
noiseEnabled: true,
noiseColor: "#eaeaea",
noiseDensity: 0.11,
noiseOpacity: 0.25,
noiseSize: 1,
noiseBlend: "difference",
restart: () => restartScene(),
};
const DEV_MODE = new URLSearchParams(window.location.search).has("dev");
if (DEV_MODE) {
const gui = new GUI({ title: "controls" });
gui.add(ctrls, "bodyFontSize", 10, 22, 1).name("body font").onChange(applyFontSizes);
gui.add(ctrls, "asciiFontSize", 8, 20, 1).name("ascii font").onChange(applyFontSizes);
gui.add(ctrls, "autoSpin", 0, 0.6, 0.01).name("auto-spin").onChange(v => { ORB.autoSpin = v; });
orbShapeControl = gui.add(ctrls, "orbShape", ["sphere", "cube"]).name("orb shape").onChange(v => {
setOrbShape(v);
});
gui.add(ctrls, "shapeMorphDuration", 0.05, 2, 0.01).name("shape morph (s)");
gui.add(ctrls, "orbLead", 0, 6, 0.05).name("orb start (s)").onFinishChange(() => restartScene());
gui.add(ctrls, "orbZoom", 0.25, 12, 0.01).name("orb zoom").onChange(v => { ORB.zoom = v; });
gui.add(ctrls, "cubeScale", 0.8, 3.5, 0.01).name("cube scale").onChange(v => { ORB.cubeScale = v; });
gui.add(ctrls, "orbRadius", 0.08, 0.4, 0.005).name("orb radius").onChange(v => { ORB.radius = v; });
gui.add(ctrls, "orbSpaceStart", 0.4, 1, 0.01).name("space start");
gui.add(ctrls, "orbSpaceDuration", 0.2, 4, 0.05).name("space grow (s)");
gui.add(ctrls, "orbSpaceEase", 0.4, 3, 0.05).name("space ease");
gui.add(ctrls, "orbWire", 0.01, 0.2, 0.005).name("wire start").onChange(v => { ORB.wire = v; });
gui.add(ctrls, "orbWireLow", 0.005, 0.12, 0.001).name("wire low").onFinishChange(() => restartScene());
gui.add(ctrls, "wireDropDuration", 0.4, 6, 0.05).name("wire drop (s)").onFinishChange(() => restartScene());
gui.add(ctrls, "orbContainNous", 0, 3, 0.05).name("orb contain NOUS");
gui.add(ctrls, "orbFadeDuration", 0.1, 3, 0.05).name("orb fade on NOUS");
gui.add(ctrls, "nousAfterWire", 0, 4, 0.05).name("NOUS after wire").onFinishChange(() => restartScene());
gui.add(ctrls, "nousLetterSpacing", 0, 4, 0.05).name("NOUS spacing");
gui.add(ctrls, "typeSpeed", 0, 4000, 10).name("type speed");
gui.addColor(ctrls, "bgColor").name("background").onChange(applyColors);
gui.addColor(ctrls, "mgColor").name("midground").onChange(applyColors);
gui.addColor(ctrls, "fgColor").name("foreground").onChange(applyColors);
const noiseFolder = gui.addFolder("noise");
noiseFolder.add(ctrls, "noiseEnabled").name("on");
noiseFolder.addColor(ctrls, "noiseColor").name("color");
noiseFolder.add(ctrls, "noiseDensity", 0, 1, 0.01).name("density");
noiseFolder.add(ctrls, "noiseOpacity", 0, 1, 0.01).name("opacity");
noiseFolder.add(ctrls, "noiseSize", 0.1, 10, 0.1).name("size");
noiseFolder.add(ctrls, "noiseBlend", [
"normal", "multiply", "screen", "difference", "exclusion",
"color-dodge", "color-burn", "hard-light", "soft-light", "overlay", "lighten",
]).name("blend").onChange(v => { document.querySelector(".noise").style.mixBlendMode = v; });
gui.add(ctrls, "restart").name("restart anim");
}
const terminalCursorEl = document.getElementById("terminalCursor");
function getLastCharRect(lineEl) {
if (!lineEl) return null;
const walker = document.createTreeWalker(lineEl, NodeFilter.SHOW_TEXT);
let lastTextNode = null;
let node = walker.nextNode();
while (node) {
if (node.nodeValue && node.nodeValue.length) lastTextNode = node;
node = walker.nextNode();
}
if (!lastTextNode) return null;
const end = lastTextNode.nodeValue.length;
if (!end) return null;
const range = document.createRange();
range.setStart(lastTextNode, end - 1);
range.setEnd(lastTextNode, end);
const rects = range.getClientRects();
const rect = rects.length ? rects[rects.length - 1] : range.getBoundingClientRect();
return rect && (rect.width || rect.height) ? rect : null;
}
function updateTerminalCursor() {
if (!drawnLines.count || !terminalAnchorEl || !terminalLineEl) {
terminalCursorEl.hidden = true;
return;
}
const stageRect = stage.getBoundingClientRect();
const charRect = getLastCharRect(terminalLineEl);
const anchorRect = terminalAnchorEl.getBoundingClientRect();
if (!anchorRect.width && !anchorRect.height) {
terminalCursorEl.hidden = true;
return;
}
const fontSize = parseInt(BODY_FONT.match(/(\d+)px/)?.[1] || "12", 10);
const fallbackW = Math.max(6, Math.round(fontSize * 0.6));
const leftPx = (charRect?.left ?? (anchorRect.left - fallbackW)) - stageRect.left;
const topPx = (charRect?.top ?? anchorRect.top) - stageRect.top;
const widthPx = Math.max(1, Math.round(charRect?.width || fallbackW));
const heightPx = Math.max(1, Math.round(charRect?.height || (BODY_LINE_H - 4)));
const left = Math.round(leftPx);
const top = Math.round(topPx);
terminalCursorEl.style.left = `${left}px`;
terminalCursorEl.style.top = `${top}px`;
terminalCursorEl.style.height = `${heightPx}px`;
terminalCursorEl.style.width = `${widthPx}px`;
terminalCursorEl.style.fontSize = `${fontSize}px`;
terminalCursorEl.hidden = false;
}
const noiseCanvas = document.getElementById("noiseCanvas");
const noiseGL = noiseCanvas.getContext("webgl", { alpha: true, premultipliedAlpha: true })
|| noiseCanvas.getContext("experimental-webgl", { alpha: true, premultipliedAlpha: true });
let noiseProgram = null;
let noiseLocs = null;
function initNoise() {
if (!noiseGL) return;
const vertSrc = `
attribute vec2 aPos;
varying vec2 vUv;
void main() {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const fragSrc = `
precision mediump float;
varying vec2 vUv;
uniform vec2 uRes;
uniform float uDpr;
uniform float uSize;
uniform float uDensity;
uniform float uOpacity;
uniform vec3 uColor;
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
float n = hash(floor(vUv * uRes / (uSize * uDpr)));
gl_FragColor = vec4(uColor, step(1.0 - uDensity, n)) * uOpacity;
}
`;
function compile(type, src) {
const sh = noiseGL.createShader(type);
noiseGL.shaderSource(sh, src);
noiseGL.compileShader(sh);
return sh;
}
const vs = compile(noiseGL.VERTEX_SHADER, vertSrc);
const fs = compile(noiseGL.FRAGMENT_SHADER, fragSrc);
noiseProgram = noiseGL.createProgram();
noiseGL.attachShader(noiseProgram, vs);
noiseGL.attachShader(noiseProgram, fs);
noiseGL.linkProgram(noiseProgram);
noiseGL.useProgram(noiseProgram);
const buf = noiseGL.createBuffer();
noiseGL.bindBuffer(noiseGL.ARRAY_BUFFER, buf);
noiseGL.bufferData(noiseGL.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), noiseGL.STATIC_DRAW);
const aPos = noiseGL.getAttribLocation(noiseProgram, "aPos");
noiseGL.enableVertexAttribArray(aPos);
noiseGL.vertexAttribPointer(aPos, 2, noiseGL.FLOAT, false, 0, 0);
noiseLocs = {
uRes: noiseGL.getUniformLocation(noiseProgram, "uRes"),
uDpr: noiseGL.getUniformLocation(noiseProgram, "uDpr"),
uSize: noiseGL.getUniformLocation(noiseProgram, "uSize"),
uDensity: noiseGL.getUniformLocation(noiseProgram, "uDensity"),
uOpacity: noiseGL.getUniformLocation(noiseProgram, "uOpacity"),
uColor: noiseGL.getUniformLocation(noiseProgram, "uColor"),
};
noiseGL.enable(noiseGL.BLEND);
noiseGL.blendFunc(noiseGL.ONE, noiseGL.ONE_MINUS_SRC_ALPHA);
noiseGL.clearColor(0, 0, 0, 0);
}
function resizeNoise() {
if (!noiseGL) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
noiseCanvas.width = innerWidth * dpr;
noiseCanvas.height = innerHeight * dpr;
noiseGL.viewport(0, 0, noiseCanvas.width, noiseCanvas.height);
if (noiseLocs) {
noiseGL.uniform2f(noiseLocs.uRes, noiseCanvas.width, noiseCanvas.height);
noiseGL.uniform1f(noiseLocs.uDpr, dpr);
}
}
addEventListener("resize", resizeNoise);
initNoise();
resizeNoise();
function drawNoiseFrame() {
if (!noiseGL || !noiseProgram) return;
noiseGL.clear(noiseGL.COLOR_BUFFER_BIT);
if (!ctrls.noiseEnabled) return;
noiseGL.useProgram(noiseProgram);
noiseGL.uniform1f(noiseLocs.uSize, ctrls.noiseSize);
noiseGL.uniform1f(noiseLocs.uDensity, ctrls.noiseDensity);
noiseGL.uniform1f(noiseLocs.uOpacity, ctrls.noiseOpacity);
const [r, g, b] = hexToRGB(ctrls.noiseColor);
noiseGL.uniform3f(noiseLocs.uColor, r / 255, g / 255, b / 255);
noiseGL.drawArrays(noiseGL.TRIANGLE_STRIP, 0, 4);
}
function draw(time) {
if (!fontsReady || !bodyPrepared) { requestAnimationFrame(draw); return; }
const t = time * 0.001 - sceneEpoch;
const dt = lastFrameTime ? Math.min(0.05, (time - lastFrameTime) / 1000) : 0.016;
lastFrameTime = time;
ctx.clearRect(0, 0, canvasW, canvasH);
orbCtx.clearRect(0, 0, canvasW, canvasH);
rasterizeNOUS(t);
rasterizeOrb(t);
tickTyping(t, dt);
layoutParagraphs();
renderTextLayer();
updateTerminalCursor();
drawNoiseFrame();
ctx.font = ASCII_FONT;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = LENS.mgBase;
orbCtx.font = ASCII_FONT;
orbCtx.textBaseline = "middle";
orbCtx.textAlign = "center";
orbCtx.fillStyle = LENS.mgBase;
for (let r = 0; r < canvasRows; r++) {
const base = r * canvasCols;
for (let c = 0; c < canvasCols; c++) {
const idx = base + c;
if (asciiMask[idx] === EMPTY) continue;
ctx.fillText(asciiGlyph[idx] || CHARS[asciiChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);
}
}
for (let r = 0; r < canvasRows; r++) {
const base = r * canvasCols;
for (let c = 0; c < canvasCols; c++) {
const idx = base + c;
if (orbMask[idx] === EMPTY) continue;
orbCtx.fillText(orbGlyph[idx] || CHARS[orbChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);
}
}
requestAnimationFrame(draw);
}
resize();
const DS_CDN = "https://esm.sh/@nous-research/ui@0.4.0/dist/fonts";
const FACES = [new FontFace("Mondwest", `url(${DS_CDN}/Mondwest-Regular.woff2) format("woff2")`, { weight: "400", display: "block" })];
(async () => {
try {
const loaded = await Promise.all(FACES.map(f => f.load()));
for (const f of loaded) document.fonts.add(f);
await document.fonts.load(BODY_FONT, "Aa");
await document.fonts.load(ASCII_FONT, "Aa");
} catch (err) {
console.warn("DS font load failed, using fallbacks:", err);
}
rebuildLayouts();
fontsReady = true;
requestAnimationFrame(draw);
})();
</script>
</body>
</html>