hermes-agent/skills/creative/pretext/templates/donut-orbit.html
Brooklyn Nicholson 165d766891 skills: refine pretext creative demo guidance
Capture the reusable layout and animation lessons from the advanced Pretext demo so the skill teaches measured obstacle fields, morphing geometry, and polished browser examples.
2026-04-29 14:24:15 -05:00

1468 lines
52 KiB
HTML

<!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>