templates/donut-orbit.html
templates/donut-orbit.htmlBrowse 4 files
17,078 tokens
53,211 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1<!doctype html>2<html lang="en">3<head>4<meta charset="utf-8" />5<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />6<title>NOUS · pretext</title>7<link rel="preconnect" href="https://fonts.googleapis.com">8<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>9<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&display=block" rel="stylesheet">10<style>11 :root { color-scheme: dark; }12 13 :root {14 --background: color-mix(in srgb, #041C1C 100%, transparent);15 --background-base: #041C1C;16 --background-alpha: 1;17 --background-blend: difference;18 --midground: color-mix(in srgb, #ffe6cb 100%, transparent);19 --midground-base: #ffe6cb;20 --midground-alpha: 1;21 --foreground: color-mix(in srgb, #ffffff 0%, transparent);22 --foreground-base: #ffffff;23 --foreground-alpha: 0;24 --foreground-blend: difference;25 }26 27 html, body {28 margin: 0;29 width: 100%;30 min-height: 100vh;31 overflow: hidden;32 background: #000;33 }34 35 /* literally center the composition in 100vh */36 body {37 display: flex;38 align-items: center;39 justify-content: center;40 }41 42 #stage {43 position: relative;44 z-index: 2;45 width: min(1240px, 100vw);46 height: min(720px, 100vh);47 cursor: grab;48 overflow: visible;49 }50 #stage:active { cursor: grabbing; }51 52 #c,53 #orbCanvas {54 display: block;55 position: absolute;56 left: calc((100vw - min(1240px, 100vw)) / -2);57 top: calc((100vh - min(720px, 100vh)) / -2);58 width: 100vw;59 height: 100vh;60 pointer-events: none;61 }62 #c { z-index: 3; }63 #orbCanvas {64 z-index: 4;65 opacity: 1;66 }67 #noiseCanvas {68 display: block;69 position: absolute;70 inset: 0;71 width: 100%;72 height: 100%;73 pointer-events: none;74 }75 #textLayer {76 position: absolute;77 inset: 0;78 z-index: 2;79 color: var(--midground-base);80 font: 400 10px/15px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;81 pointer-events: auto;82 user-select: text;83 -webkit-user-select: text;84 }85 #textLayer *::selection {86 background: var(--selection-bg, var(--midground));87 color: var(--background-base);88 }89 .flow-line {90 position: absolute;91 height: 20px;92 white-space: pre;93 overflow: visible;94 color: var(--midground-base);95 }96 #annotationLayer {97 position: absolute;98 inset: 0;99 z-index: 4;100 pointer-events: none;101 overflow: visible;102 }103 .annot {104 position: absolute;105 pointer-events: none;106 will-change: opacity, transform;107 }108 .tok-keyword { color: color-mix(in srgb, var(--midground-base) 54%, #ff174d); }109 .tok-string { color: color-mix(in srgb, var(--midground-base) 52%, #ff6b3d); }110 .tok-comment { color: color-mix(in srgb, var(--midground-base) 42%, #7f1d1d); }111 .tok-number { color: color-mix(in srgb, var(--midground-base) 50%, #ff2a6d); }112 .tok-const { color: color-mix(in srgb, var(--midground-base) 48%, #ff003c); }113 114 .layer { position: fixed; inset: 0; pointer-events: none; }115 .bg-lens { z-index: 1; background-color: var(--background); mix-blend-mode: var(--background-blend); }116 .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; }117 .fg-lens { z-index: 100; background-color: var(--foreground); mix-blend-mode: var(--foreground-blend); }118 .noise {119 z-index: 101;120 mix-blend-mode: difference;121 }122 #noiseCanvas {123 width: 100%;124 height: 100%;125 display: block;126 }127 .terminal-cursor {128 position: absolute;129 width: 0.6em;130 height: 1em;131 background: #fff;132 z-index: 6;133 pointer-events: none;134 mix-blend-mode: difference;135 animation: term-blink 1s steps(1, end) infinite;136 }137 @keyframes term-blink {138 0%, 60% { opacity: 1; }139 60.01%, 100% { opacity: 0; }140 }141 .cursor-anchor {142 display: inline-block;143 width: 0;144 height: 1em;145 vertical-align: text-top;146 pointer-events: none;147 }148 149 @font-face {150 font-family: "Mondwest";151 src: url("https://esm.sh/@nous-research/ui@0.4.0/dist/fonts/Mondwest-Regular.woff2") format("woff2");152 font-weight: 400; font-style: normal; font-display: block;153 }154 @font-face {155 font-family: "Geist Mono";156 src: url("https://cdn.jsdelivr.net/npm/@fontsource/geist-mono@5.2.5/files/geist-mono-latin-400-normal.woff2") format("woff2");157 font-weight: 400; font-style: normal; font-display: block;158 }159 @font-face {160 font-family: "JetBrains Mono";161 src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.2.5/files/jetbrains-mono-latin-400-normal.woff2") format("woff2");162 font-weight: 400; font-style: normal; font-display: block;163 }164 165 .font-primer { position: fixed; left: -9999px; top: -9999px; font: 400 26px "Mondwest", serif; }166</style>167</head>168<body>169<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>170<div class="layer bg-lens"></div>171<div class="layer vignette"></div>172<div class="layer fg-lens"></div>173<div class="layer noise"><canvas id="noiseCanvas"></canvas></div>174<div class="font-primer" aria-hidden="true">Aa Mondwest priming glyphs 0123456789</div>175 176<script type="module">177import {178 prepareWithSegments,179 layoutNextLineRange,180 materializeLineRange,181} from "https://esm.sh/@chenglou/pretext@0.0.6";182import gsap from "https://esm.sh/gsap@3.12.5";183import GUI from "https://esm.sh/lil-gui@0.19.2";184 185const SELF_LEARNING_SOURCE = String.raw`186type ToolId=u16; type FeatureId=u32; type W=f32;187struct Obs{tool:ToolId,reward:f32,lat:u32,feat:&'static[FeatureId],tok:u16}188struct Belief{prior:W,conf:W,last:u64,uses:u32}189 190#[inline] fn sig(x:f32)->f32{1.0/(1.0+(-x).exp())}191#[inline] fn decay(now:u64,last:u64)->f32{1.0-((now-last)as f32/900.0).min(1.0)*0.22}192#[inline] fn clamp8(x:f32)->f32{x.clamp(-8.0,8.0)}193 194fn score(theta:&[W],b:&[Belief],o:&Obs)->f32{195 let sparse=o.feat.iter().fold(0.0,|a,id|a+theta[*id as usize]);196 let belief=b[o.tool as usize].prior*b[o.tool as usize].conf.max(0.15);197 sparse+belief-(o.lat as f32*0.00072)-(o.tok as f32*0.00009)198}199 200fn update(theta:&mut[W],b:&mut[Belief],o:&Obs,now:u64,eta:f32){201 let p=sig(score(theta,b,o));202 let e=(o.reward-p).clamp(-1.0,1.0);203 let step=eta*e/(o.feat.len().max(1)as f32).sqrt();204 205 for id in o.feat{206 let w=&mut theta[*id as usize];207 *w=clamp8(*w+step-0.0003*w.signum());208 }209 210 let slot=&mut b[o.tool as usize];211 let d=decay(now,slot.last);212 let r=(o.reward-o.lat as f32*0.00035).clamp(-1.0,1.0);213 slot.prior=slot.prior*d+r*0.14+e*0.06;214 slot.conf=(slot.conf*0.93+r.abs()*0.07).min(1.0);215 slot.uses=slot.uses.saturating_add(1);216 slot.last=now;217}218 219fn consolidate(trace:&[Obs],theta:&mut[W],b:&mut[Belief],now:u64){220 let eta=0.042/(1.0+(trace.len()as f32*0.003));221 let mut credit=1.0;222 for o in trace.iter().rev(){223 update(theta,b,o,now,eta*credit);224 credit*=0.985;225 }226}227 228fn choose<'a>(theta:&[W],b:&[Belief],xs:&'a[Obs],temp:f32)->&'a Obs{229 xs.iter().max_by(|a,bx|{230 let sa=score(theta,b,a)/temp.max(0.05);231 let sb=score(theta,b,bx)/temp.max(0.05);232 sa.partial_cmp(&sb).unwrap()233 }).unwrap()234}235`;236const CODE_CHUNK = SELF_LEARNING_SOURCE237 .trim()238 .replace(/\t/g, " ")239 .replace(/\n{3,}/g, "\n\n");240const STREAM_REPEAT_COUNT = 6;241const streamCorpus = Array.from({ length: STREAM_REPEAT_COUNT }, () => CODE_CHUNK).join("\n\n");242 243const stage = document.getElementById("stage");244const textLayer = document.getElementById("textLayer");245const canvas = document.getElementById("c");246const orbCanvas = document.getElementById("orbCanvas");247const ctx = canvas.getContext("2d", { alpha: true });248const orbCtx = orbCanvas.getContext("2d", { alpha: true });249let W = 0, H = 0, DPR = 1;250let canvasLeft = 0, canvasTop = 0, canvasW = 0, canvasH = 0;251 252let BODY_FONT = '400 10px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';253let BODY_LINE_H = 15;254let ASCII_FONT = '8px monospace';255let CELL_W = 4.8;256let CELL_H = 8;257const CHARS = ' ·:;+=░▒▓█';258const EMPTY = 0;259const CHARS_LAST = CHARS.length - 1;260 261const LOGO = ['N', 'O', 'U', 'S'].map((letter, i) => ({262 letter,263 rows: [264 i === 0265 ? '██░░░██,██░░░██,███░░██,█░██░██,█░░████,█░░░███,██░░░██,██░░░██,██░░░██'266 : i === 1267 ? '░█████░,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'268 : i === 2269 ? '██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'270 : '░█████░,██░░░██,██░░░░░,░████░░,░░░░██░,░░░░░██,██░░░██,██░░░██,░█████░'271 ][0].split(',')272}));273 274function hash(x, y) {275 const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;276 return n - Math.floor(n);277}278function charAt(t) {279 return CHARS[Math.min(CHARS.length - 1, Math.max(0, Math.floor(t * CHARS.length)))];280}281function gridDist(v, step) {282 return Math.abs(v / step - Math.round(v / step)) * step;283}284function rot(a, b, angle) {285 const c = Math.cos(angle), s = Math.sin(angle);286 return [a * c - b * s, a * s + b * c];287}288function hexToRGB(h) {289 const v = parseInt(h.slice(1), 16);290 return [(v >> 16) & 255, (v >> 8) & 255, v & 255];291}292 293let fontsReady = false;294let bodyPrepared = null;295function rebuildLayouts() {296 bodyPrepared = prepareWithSegments(streamCorpus, BODY_FONT, { whiteSpace: "pre-wrap" });297}298 299const LENS_0 = { bgBlend: "difference", bgColor: "#041C1C", bgOpacity: 1, mgColor: "#ffe6cb", mgOpacity: 1, fgBlend: "difference", fgColor: "#FFFFFF", fgOpacity: 0 };300const LENS_5I = { bgBlend: "multiply", bgColor: "#ffffff", bgOpacity: 1, mgColor: "#fffff5", mgOpacity: 1, fgBlend: "difference", fgColor: "#041a1f", fgOpacity: 1 };301const LENS = { current: null, mgBase: null, depthColors: null };302function colorMix(color, alpha) { return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`; }303function precomputeDepthColors(mgColor) {304 const [r, g, b] = hexToRGB(mgColor);305 return Array.from({ length: 64 }, (_, i) => {306 const depth = i / 63;307 const m = 0.4 + depth * 0.6;308 return `rgba(${(r * m) | 0},${(g * m) | 0},${(b * m) | 0},${depth})`;309 });310}311function applyLens(preset) {312 LENS.current = preset;313 LENS.mgBase = preset.mgColor;314 LENS.depthColors = precomputeDepthColors(preset.mgColor);315 const s = document.documentElement.style;316 for (const [name, color, alpha] of [["foreground", preset.fgColor, preset.fgOpacity], ["midground", preset.mgColor, preset.mgOpacity], ["background", preset.bgColor, preset.bgOpacity]]) {317 s.setProperty(`--${name}`, colorMix(color, alpha));318 s.setProperty(`--${name}-base`, color);319 s.setProperty(`--${name}-alpha`, `${alpha}`);320 }321 s.setProperty("--background-blend", preset.bgBlend);322 s.setProperty("--foreground-blend", preset.fgBlend);323}324applyLens(LENS_0);325addEventListener("keydown", e => { if (e.key === "x" || e.key === "X") applyLens(LENS.current === LENS_0 ? LENS_5I : LENS_0); });326const SELECTION_COLORS = [327 "oklch(85% 0.12 330)",328 "oklch(85% 0.12 300)",329 "oklch(85% 0.12 270)",330 "oklch(85% 0.12 230)",331 "oklch(85% 0.12 180)",332 "oklch(85% 0.12 150)",333 "oklch(85% 0.12 120)",334 "oklch(85% 0.12 90)",335 "oklch(85% 0.12 60)",336 "oklch(85% 0.12 30)",337 "oklch(88% 0.10 80)",338];339let selectionIdx = 0;340function cycleSelectionColor() {341 requestAnimationFrame(() => {342 document.documentElement.style.setProperty("--selection-bg", SELECTION_COLORS[(selectionIdx = (selectionIdx + 1) % SELECTION_COLORS.length)]);343 });344}345document.addEventListener("selectstart", cycleSelectionColor);346addEventListener("keydown", e => {347 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {348 cycleSelectionColor();349 e.preventDefault();350 const selection = getSelection();351 const range = document.createRange();352 range.selectNodeContents(textLayer);353 selection.removeAllRanges();354 selection.addRange(range);355 }356});357 358let asciiMask, asciiChar, asciiGlyph, orbMask, orbChar, orbGlyph, obstacleRows, intervals;359let cols = 0, rows = 0;360let canvasCols = 0, canvasRows = 0;361let lastFrameTime = 0;362const drawnLines = { x: [], y: [], w: [], text: [], count: 0 };363let lineNodes = [];364let terminalAnchorEl = null;365let terminalLineEl = null;366let orbShapeControl = null;367let autoCubeCall = null;368let lastTextSignature = "";369const ORB = {370 panX: 0, panY: 0,371 orbitX: -1.42, orbitY: 0,372 radius: 0.135,373 zoom: 3,374 cubeScale: 1.8,375 autoSpin: 0.18,376 lat: 19,377 lon: 30,378 wire: 0.2,379 shape: "sphere",380 shapeMix: 0,381 drag: null,382 positioned: false,383 staged: false,384};385const SCENE = {386 orbBuildDuration: 1.45,387};388 389let nousStartTime = null;390const AUTO_CUBE_AFTER_NOUS_RESOLVE = 0.0;391function setOrbShape(shape, { reveal = false, syncControl = true } = {}) {392 ORB.shape = shape;393 if (syncControl) {394 ctrls.orbShape = shape;395 orbShapeControl?.updateDisplay();396 }397 if (reveal) {398 gsap.killTweensOf(orbCanvas);399 orbCanvas.style.opacity = "1";400 }401 gsap.killTweensOf(ORB, "shapeMix");402 gsap.to(ORB, {403 shapeMix: shape === "cube" ? 1 : 0,404 duration: Math.max(0.01, ctrls.shapeMorphDuration ?? 0.65),405 ease: "power2.inOut",406 onUpdate: () => { lastTextSignature = ""; },407 });408 lastTextSignature = "";409}410function sceneTimeNow() {411 return (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001 - sceneEpoch;412}413function armNous(atTime = sceneTimeNow()) {414 if (nousStartTime !== null) return;415 const startAt = Math.max(0, atTime);416 nousStartTime = startAt;417 const delay = Math.max(0, startAt - sceneTimeNow()) + Math.max(0, ctrls.orbContainNous ?? 1.0);418 gsap.killTweensOf(orbCanvas);419 gsap.to(orbCanvas, {420 opacity: 0,421 duration: Math.max(0.05, ctrls.orbFadeDuration ?? 0.7),422 delay,423 ease: "power2.out",424 });425 autoCubeCall?.kill();426 const nousResolveAt = startAt + (LOGO.length - 1) * 0.4 + 1.0;427 const cubeAt = nousResolveAt + AUTO_CUBE_AFTER_NOUS_RESOLVE;428 autoCubeCall = gsap.delayedCall(Math.max(0, cubeAt - sceneTimeNow()), () => {429 setOrbShape("cube", { reveal: true, syncControl: false });430 });431}432function orbBuildStartTime() {433 return Math.max(0, ctrls.orbLead ?? 0);434}435function codeTypingStartTime() {436 return Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.62 - 0.3);437}438 439function logoCenterPan() {440 const aspect = cols / rows / 2;441 const h = 0.38;442 const w = h * 2.6 * 0.55;443 const halfStageX = (w / 2 / aspect) * W;444 const centerX = W * 0.5;445 const centerY = H * 0.5;446 return {447 panX: centerX / W - 0.5,448 panY: centerY / H - 0.5,449 left: centerX - halfStageX,450 right: centerX + halfStageX,451 };452}453 454function startSceneTimeline() {455 if (ORB.staged || !cols || !rows || !W || !H) return;456 ORB.staged = true;457 const now = sceneTimeNow();458 const startAt = Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.72 - now);459 gsap.timeline({ delay: startAt })460 .to(ORB, {461 orbitX: 0.55,462 orbitY: `+=${Math.PI * 1.85}`,463 duration: 2.4,464 ease: "power3.inOut",465 onUpdate: () => { lastTextSignature = ""; },466 }, "<")467 .to(ORB, {468 radius: 0.125,469 duration: 2.4,470 ease: "power3.inOut",471 onUpdate: () => { lastTextSignature = ""; },472 }, "<")473 .to(ORB, {474 wire: ctrls.orbWireLow,475 duration: ctrls.wireDropDuration,476 ease: "power2.inOut",477 onUpdate: () => { lastTextSignature = ""; },478 onComplete: () => {479 armNous(sceneTimeNow() + Math.max(0, ctrls.nousAfterWire));480 },481 });482}483 484function resize() {485 const r = stage.getBoundingClientRect();486 W = Math.max(1, Math.floor(r.width));487 H = Math.max(1, Math.floor(r.height));488 canvasLeft = r.left;489 canvasTop = r.top;490 canvasW = Math.max(1, window.innerWidth);491 canvasH = Math.max(1, window.innerHeight);492 DPR = Math.min(window.devicePixelRatio || 1, 2);493 canvas.width = Math.floor(canvasW * DPR);494 canvas.height = Math.floor(canvasH * DPR);495 orbCanvas.width = Math.floor(canvasW * DPR);496 orbCanvas.height = Math.floor(canvasH * DPR);497 ctx.setTransform(DPR, 0, 0, DPR, 0, 0);498 orbCtx.setTransform(DPR, 0, 0, DPR, 0, 0);499 ctx.font = ASCII_FONT;500 orbCtx.font = ASCII_FONT;501 CELL_W = ctx.measureText("M").width || CELL_W;502 cols = Math.ceil(W / CELL_W);503 rows = Math.ceil(H / CELL_H);504 canvasCols = Math.ceil(canvasW / CELL_W);505 canvasRows = Math.ceil(canvasH / CELL_H);506 asciiMask = new Uint8Array(canvasCols * canvasRows);507 asciiChar = new Uint8Array(canvasCols * canvasRows);508 asciiGlyph = new Array(canvasCols * canvasRows);509 orbMask = new Uint8Array(canvasCols * canvasRows);510 orbChar = new Uint8Array(canvasCols * canvasRows);511 orbGlyph = new Array(canvasCols * canvasRows);512 obstacleRows = Array.from({ length: rows }, () => []);513 intervals = new Float32Array(64);514 if (!ORB.positioned && W > 100 && H > 100) {515 ORB.panX = 0;516 ORB.panY = 0;517 ORB.positioned = true;518 }519 startSceneTimeline();520}521addEventListener("resize", () => { lastTextSignature = ""; resize(); });522function orbCenter() {523 const rNorm = ORB.radius * ORB.zoom;524 return {525 x: (0.5 + ORB.panX) * W,526 y: (0.5 + ORB.panY) * H,527 canvasX: (0.5 + ORB.panX) * W + canvasLeft,528 canvasY: (0.5 + ORB.panY) * H + canvasTop,529 r: rNorm * H,530 rNorm,531 };532}533 534stage.addEventListener("pointerdown", e => {535 const rect = stage.getBoundingClientRect();536 const center = orbCenter();537 const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;538 const dx = e.clientX - rect.left - center.x;539 const dy = e.clientY - rect.top - center.y;540 if (Math.hypot(dx, dy) > center.r * 1.35 * hitScale) return;541 e.preventDefault();542 stage.setPointerCapture(e.pointerId);543 ORB.drag = {544 id: e.pointerId,545 mode: (e.ctrlKey || e.metaKey) ? "rotate" : "move",546 dx,547 dy,548 lastX: e.clientX,549 lastY: e.clientY,550 };551});552stage.addEventListener("pointermove", e => {553 if (!ORB.drag || ORB.drag.id !== e.pointerId) return;554 if (ORB.drag.mode === "rotate" || e.ctrlKey || e.metaKey) {555 const deltaX = e.clientX - ORB.drag.lastX;556 const deltaY = e.clientY - ORB.drag.lastY;557 ORB.orbitY += (deltaX / Math.max(1, W)) * Math.PI * 2;558 ORB.orbitX = Math.max(-1.5, Math.min(1.5, ORB.orbitX + (deltaY / Math.max(1, H)) * 4));559 ORB.drag.mode = "rotate";560 ORB.drag.lastX = e.clientX;561 ORB.drag.lastY = e.clientY;562 return;563 }564 const rect = stage.getBoundingClientRect();565 const cx = e.clientX - rect.left - ORB.drag.dx;566 const cy = e.clientY - rect.top - ORB.drag.dy;567 ORB.panX = cx / W - 0.5;568 ORB.panY = cy / H - 0.5;569 lastTextSignature = "";570});571stage.addEventListener("wheel", e => {572 const rect = stage.getBoundingClientRect();573 const center = orbCenter();574 const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;575 const dx = e.clientX - rect.left - center.x;576 const dy = e.clientY - rect.top - center.y;577 if (Math.hypot(dx, dy) > center.r * 1.5 * hitScale) return;578 e.preventDefault();579 ORB.zoom *= Math.exp(-e.deltaY * 0.0012);580 ORB.zoom = Math.max(0.25, Math.min(12, ORB.zoom));581 lastTextSignature = "";582}, { passive: false });583stage.addEventListener("pointerup", e => {584 if (ORB.drag?.id === e.pointerId) ORB.drag = null;585});586stage.addEventListener("pointercancel", e => {587 if (ORB.drag?.id === e.pointerId) ORB.drag = null;588});589stage.addEventListener("dblclick", e => {590 const rect = stage.getBoundingClientRect();591 const center = orbCenter();592 const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;593 const dx = e.clientX - rect.left - center.x;594 const dy = e.clientY - rect.top - center.y;595 if (Math.hypot(dx, dy) > center.r * 1.45 * hitScale) return;596 e.preventDefault();597 setOrbShape(ORB.shape === "sphere" ? "cube" : "sphere", { reveal: true });598});599 600function rasterizeNOUS(time) {601 asciiMask.fill(0);602 asciiGlyph.fill("");603 for (const r of obstacleRows) r.length = 0;604 605 // NOUS uses the same cell-shader/reveal language as bb-ascii's source logo.606 if (nousStartTime === null || time < nousStartTime) {607 mergeObstacleRows();608 return;609 }610 const t = Math.max(0, time - nousStartTime);611 const aspect = cols / rows / 2;612 const h = 0.38;613 const baseW = h * 2.6 * 0.55;614 const letterWidth = 7;615 const baseLetterPitch = 7.6;616 const letterGap = Math.max(0, ctrls.nousLetterSpacing ?? 0);617 const letterAdvance = baseLetterPitch + letterGap;618 const baseLogoUnits = LOGO.length * baseLetterPitch - 0.6;619 const logoUnits = LOGO.length * baseLetterPitch + (LOGO.length - 1) * letterGap - 0.6;620 const w = baseW * (logoUnits / baseLogoUnits);621 const nousRowMin = new Float32Array(rows);622 const nousRowMax = new Float32Array(rows);623 nousRowMin.fill(Infinity);624 nousRowMax.fill(-Infinity);625 626 for (let y = 0; y < rows; y++) {627 const ny = y / rows - 0.5;628 if (Math.abs(ny) > h / 2) continue;629 630 for (let x = 0; x < cols; x++) {631 const nx = (x / cols - 0.5) * aspect;632 if (Math.abs(nx) > w / 2) continue;633 634 const lx = (nx + w / 2) / w;635 const ly = (ny + h / 2) / h;636 const tx = lx * logoUnits;637 const idx = Math.floor(tx / letterAdvance);638 const cx = Math.floor(tx - idx * letterAdvance);639 const cy = Math.floor(ly * 9);640 641 if (idx < 0 || idx >= LOGO.length || cx >= letterWidth) continue;642 643 const { letter, rows: glyphRows } = LOGO[idx];644 const row = glyphRows[cy];645 if (!row || cx >= row.length || row[cx] !== '█') continue;646 647 const localT = Math.max(0, t - idx * 0.4);648 const phase1 = Math.min(1, localT);649 const phase2 = Math.min(1, Math.max(0, localT - 1));650 const reveal = phase1 * 1.5 - hash(cx, cy + idx * 10) * 0.5;651 652 if (phase1 < 1 && reveal <= 0) continue;653 654 const morph = phase2 + hash(cx, cy) * 0.3;655 const ch = phase1 < 1656 ? (reveal >= 1 ? letter : charAt(reveal))657 : charAt(morph);658 const drawC = Math.floor((canvasLeft + x * CELL_W + CELL_W * 0.5) / CELL_W);659 const drawR = Math.floor((canvasTop + y * CELL_H + CELL_H * 0.5) / CELL_H);660 let cellIdx = -1;661 if (drawR >= 0 && drawR < canvasRows && drawC >= 0 && drawC < canvasCols) {662 cellIdx = drawR * canvasCols + drawC;663 asciiMask[cellIdx] = 1;664 asciiChar[cellIdx] = Math.max(0, CHARS.indexOf(ch));665 if (CHARS.indexOf(ch) < 0) asciiGlyph[cellIdx] = ch;666 }667 const x0 = x * CELL_W;668 const x1 = x0 + CELL_W;669 if (x0 < nousRowMin[y]) nousRowMin[y] = x0;670 if (x1 > nousRowMax[y]) nousRowMax[y] = x1;671 // bb-ascii turns late morph cells into solid dark cell backgrounds. In672 // this transparent/lensed canvas, a full-block char carries that punch.673 if (cellIdx >= 0 && phase1 >= 1 && morph > 0.8) asciiChar[cellIdx] = CHARS.length - 1;674 }675 }676 677 // Slightly dilate/smooth the measured NOUS silhouette so surrounding text678 // does not visually "kiss" sharp letter corners (notably on the S).679 const smoothMin = new Float32Array(rows);680 const smoothMax = new Float32Array(rows);681 smoothMin.fill(Infinity);682 smoothMax.fill(-Infinity);683 for (let y = 0; y < rows; y++) {684 if (!Number.isFinite(nousRowMin[y])) continue;685 for (let oy = -1; oy <= 1; oy++) {686 const yy = y + oy;687 if (yy < 0 || yy >= rows) continue;688 if (!Number.isFinite(nousRowMin[yy])) continue;689 if (nousRowMin[yy] < smoothMin[y]) smoothMin[y] = nousRowMin[yy];690 if (nousRowMax[yy] > smoothMax[y]) smoothMax[y] = nousRowMax[yy];691 }692 }693 694 const pad = Math.max(4, CELL_W * 1.25);695 const bleedRows = 2;696 for (let y = 0; y < rows; y++) {697 if (!Number.isFinite(smoothMin[y])) continue;698 for (let oy = -bleedRows; oy <= bleedRows; oy++) {699 const row = y + oy;700 if (row < 0 || row >= rows) continue;701 const bleedPad = pad + Math.abs(oy) * CELL_W * 0.28;702 obstacleRows[row].push([703 Math.max(0, smoothMin[y] - bleedPad),704 Math.min(W, smoothMax[y] + bleedPad),705 ]);706 }707 }708 709 mergeObstacleRows();710}711 712function mergeObstacleRows() {713 for (const spans of obstacleRows) {714 if (spans.length < 2) continue;715 spans.sort((a, b) => a[0] - b[0]);716 let w = 0;717 for (let i = 1; i < spans.length; i++) {718 const last = spans[w], cur = spans[i];719 if (cur[0] <= last[1] + CELL_W * 3.1) last[1] = Math.max(last[1], cur[1]);720 else spans[++w] = cur;721 }722 spans.length = w + 1;723 }724}725 726const CUBE_VERTS = [727 [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],728 [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1],729];730const CUBE_EDGES = [731 [0, 1], [1, 2], [2, 3], [3, 0],732 [4, 5], [5, 6], [6, 7], [7, 4],733 [0, 4], [1, 5], [2, 6], [3, 7],734];735function projectCubeVerts(center, radiusPx, orbitX, orbitY, scale = 1) {736 const camZ = 3.3;737 const projScale = radiusPx * 1.34 * scale;738 return CUBE_VERTS.map(([vx, vy, vz]) => {739 const [rx, ry, rz] = rotateOrbVec(vx, vy, vz, orbitX, orbitY);740 const inv = 1 / (camZ + rz);741 return {742 x: center.canvasX + rx * projScale * inv,743 y: center.canvasY + ry * projScale * inv,744 d: (rz + 1) * 0.5,745 };746 });747}748function convexHull(points) {749 if (points.length <= 2) return points.slice();750 const pts = points751 .map(p => ({ x: p.x, y: p.y }))752 .sort((a, b) => (a.x - b.x) || (a.y - b.y));753 const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);754 const lower = [];755 for (const p of pts) {756 while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();757 lower.push(p);758 }759 const upper = [];760 for (let i = pts.length - 1; i >= 0; i--) {761 const p = pts[i];762 while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();763 upper.push(p);764 }765 lower.pop();766 upper.pop();767 return lower.concat(upper);768}769function makeEmptySpanRows() {770 return Array.from({ length: rows }, () => ({ left: Infinity, right: -Infinity }));771}772function addSpan(spanRows, row, left, right) {773 if (row < 0 || row >= rows || right <= left) return;774 if (left < spanRows[row].left) spanRows[row].left = left;775 if (right > spanRows[row].right) spanRows[row].right = right;776}777function collectCubeObstacleRows(center, radiusPx, orbitX, orbitY, obstacleScale) {778 const spanRows = makeEmptySpanRows();779 if (obstacleScale <= 0.0001) return spanRows;780 const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale * obstacleScale)781 .map(p => ({ x: p.x - canvasLeft, y: p.y - canvasTop }));782 const hull = convexHull(projected);783 if (hull.length < 3) return spanRows;784 785 let yMin = Infinity;786 let yMax = -Infinity;787 for (const p of hull) {788 if (p.y < yMin) yMin = p.y;789 if (p.y > yMax) yMax = p.y;790 }791 const r0 = Math.max(0, Math.floor((yMin - CELL_H) / CELL_H));792 const r1 = Math.min(rows - 1, Math.ceil((yMax + CELL_H) / CELL_H));793 const pad = Math.max(4, CELL_W * 1.6);794 795 for (let row = r0; row <= r1; row++) {796 const scanY = row * CELL_H + CELL_H * 0.5;797 const xs = [];798 for (let i = 0; i < hull.length; i++) {799 const a = hull[i];800 const b = hull[(i + 1) % hull.length];801 const y0 = a.y;802 const y1 = b.y;803 if ((y0 <= scanY && scanY < y1) || (y1 <= scanY && scanY < y0)) {804 const t = (scanY - y0) / (y1 - y0);805 xs.push(a.x + (b.x - a.x) * t);806 }807 }808 if (xs.length < 2) continue;809 xs.sort((a, b) => a - b);810 for (let i = 0; i + 1 < xs.length; i += 2) {811 const left = Math.max(0, xs[i] - pad);812 const right = Math.min(W, xs[i + 1] + pad);813 if (right - left > CELL_W) addSpan(spanRows, row, left, right);814 }815 }816 return spanRows;817}818function collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad) {819 const spanRows = makeEmptySpanRows();820 const obstacleRadiusNorm = radiusNorm * obstacleScale;821 const obstaclePadNorm = (rPad / H) * obstacleScale;822 if (obstacleRadiusNorm <= 0.0001) return spanRows;823 824 const radiusPx = radiusNorm * H;825 const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);826 const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);827 for (let r = rMin; r <= rMax; r++) {828 const y = r * CELL_H + CELL_H * 0.5;829 const stageY = y - canvasTop;830 const pyForObstacle = stageY / H - 0.5 - ORB.panY;831 if (Math.abs(pyForObstacle) > obstacleRadiusNorm + obstaclePadNorm) continue;832 const halfNorm = Math.sqrt(Math.max(0, (obstacleRadiusNorm + obstaclePadNorm) ** 2 - pyForObstacle * pyForObstacle));833 const halfPx = halfNorm * H;834 const stageRow = Math.floor(stageY / CELL_H);835 addSpan(spanRows, stageRow, center.x - halfPx, center.x + halfPx);836 }837 return spanRows;838}839function pushMorphedObstacleRows(sphereRows, cubeRows, mix) {840 const centerX = W * (0.5 + ORB.panX);841 for (let row = 0; row < rows; row++) {842 const sphereValid = Number.isFinite(sphereRows[row].left);843 const cubeValid = Number.isFinite(cubeRows[row].left);844 if (!sphereValid && !cubeValid) continue;845 const sLeft = sphereValid ? sphereRows[row].left : centerX;846 const sRight = sphereValid ? sphereRows[row].right : centerX;847 const cLeft = cubeValid ? cubeRows[row].left : centerX;848 const cRight = cubeValid ? cubeRows[row].right : centerX;849 const left = sLeft + (cLeft - sLeft) * mix;850 const right = sRight + (cRight - sRight) * mix;851 if (right - left > CELL_W) obstacleRows[row].push([Math.max(0, left), Math.min(W, right)]);852 }853}854function rotateOrbVec(vx, vy, vz, orbitX, orbitY) {855 let a = rot(vy, vz, orbitX); vy = a[0]; vz = a[1];856 a = rot(vx, vz, orbitY); vx = a[0]; vz = a[1];857 return [vx, vy, vz];858}859function rasterizeCubeWire(center, radiusPx, buildPhase, orbitX, orbitY, weight = 1) {860 const unit = Math.max(1, Math.min(CELL_W, CELL_H));861 const wirePx = Math.max(1, ORB.wire * radiusPx * 0.17);862 const brush = Math.max(0, Math.ceil((wirePx - unit * 0.45) / unit));863 const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale);864 865 for (let ei = 0; ei < CUBE_EDGES.length; ei++) {866 const [a, b] = CUBE_EDGES[ei];867 const p0 = projected[a];868 const p1 = projected[b];869 const dx = p1.x - p0.x;870 const dy = p1.y - p0.y;871 const len = Math.hypot(dx, dy);872 const steps = Math.max(2, Math.ceil(len / Math.max(1, unit * 0.45)));873 for (let s = 0; s <= steps; s++) {874 const t = s / steps;875 const px = p0.x + dx * t;876 const py = p0.y + dy * t;877 const depth = p0.d + (p1.d - p0.d) * t;878 const c = Math.floor(px / CELL_W);879 const r = Math.floor(py / CELL_H);880 for (let oy = -brush; oy <= brush; oy++) {881 for (let ox = -brush; ox <= brush; ox++) {882 if (brush > 0 && ox * ox + oy * oy > brush * brush + 0.25) continue;883 const cc = c + ox;884 const rr = r + oy;885 if (rr < 0 || rr >= canvasRows || cc < 0 || cc >= canvasCols) continue;886 const reveal = buildPhase * 1.45 - hash(cc + ei * 13, rr + ei * 17) * 0.5;887 if (buildPhase < 1 && reveal <= 0) continue;888 const idx = rr * canvasCols + cc;889 const val = buildPhase < 1890 ? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))891 : Math.min(CHARS_LAST, Math.floor((0.45 + depth * 0.55) * CHARS_LAST));892 const weightedVal = Math.min(CHARS_LAST, Math.floor(val * weight));893 if (weightedVal <= 0) continue;894 orbMask[idx] = 1;895 orbGlyph[idx] = "";896 if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;897 }898 }899 }900 }901}902 903function rasterizeOrb(time) {904 orbMask.fill(0);905 orbChar.fill(0);906 orbGlyph.fill("");907 const buildStart = orbBuildStartTime();908 if (time < buildStart) return;909 const shapeMix = Math.max(0, Math.min(1, ORB.shapeMix ?? (ORB.shape === "cube" ? 1 : 0)));910 const sphereWeight = 1 - shapeMix;911 const cubeWeight = shapeMix;912 const sphereOrbitX = ORB.orbitX;913 const sphereOrbitY = ORB.orbitY + time * ORB.autoSpin;914 const cubeOrbitX = ORB.orbitX + time * ORB.autoSpin * 0.72;915 const cubeOrbitY = ORB.orbitY + time * ORB.autoSpin * 1.35;916 const latStep = Math.PI / ORB.lat;917 const lonStep = (Math.PI * 2) / ORB.lon;918 const rPad = 15.5;919 const center = orbCenter();920 const radiusPx = center.r;921 const radiusNorm = center.rNorm;922 const buildPhase = Math.min(1, Math.max(0, (time - buildStart) / SCENE.orbBuildDuration));923 const spacePhase = Math.min(1, Math.max(0, (time - buildStart) / Math.max(0.001, ctrls.orbSpaceDuration)));924 const spaceGrow = Math.pow(spacePhase, Math.max(0.01, ctrls.orbSpaceEase));925 const spaceStart = Math.max(0, Math.min(1, ctrls.orbSpaceStart));926 const obstacleScale = spaceStart + (1 - spaceStart) * spaceGrow;927 const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);928 const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);929 930 const sphereObstacleRows = collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad);931 const cubeObstacleRows = collectCubeObstacleRows(center, radiusPx, cubeOrbitX, cubeOrbitY, obstacleScale);932 pushMorphedObstacleRows(sphereObstacleRows, cubeObstacleRows, shapeMix);933 934 if (cubeWeight > 0.001) {935 rasterizeCubeWire(center, radiusPx, buildPhase, cubeOrbitX, cubeOrbitY, cubeWeight);936 }937 if (sphereWeight <= 0.001) {938 mergeObstacleRows();939 return;940 }941 942 const cMin = Math.floor((center.canvasX - radiusPx - rPad) / CELL_W);943 const cMax = Math.ceil((center.canvasX + radiusPx + rPad) / CELL_W);944 for (let r = rMin; r <= rMax; r++) {945 const y = r * CELL_H + CELL_H * 0.5;946 const stageY = y - canvasTop;947 for (let c = cMin; c <= cMax; c++) {948 // Direct port of bb-ascii/app/nous/sphere/page.tsx:949 // px uses width/height normalization, py is row-normalized, then both950 // are offset by pan. This is what makes the sphere circular on screen.951 const stageX = c * CELL_W + CELL_W * 0.5 - canvasLeft;952 const px = (stageX / W - 0.5 - ORB.panX) * (W / H);953 const py = stageY / H - 0.5 - ORB.panY;954 const d2 = px * px + py * py;955 if (d2 > radiusNorm * radiusNorm) continue;956 957 const pz = Math.sqrt(radiusNorm * radiusNorm - d2);958 const depth = (pz / radiusNorm) * 0.5 + 0.5;959 960 let sx = px / radiusNorm, sy = py / radiusNorm, sz = pz / radiusNorm;961 let a = rot(sy, sz, sphereOrbitX); sy = a[0]; sz = a[1];962 a = rot(sx, sz, sphereOrbitY); sx = a[0]; sz = a[1];963 964 const latD = gridDist(Math.asin(Math.max(-1, Math.min(1, sy))) + Math.PI / 2, latStep);965 const lonD = gridDist(Math.atan2(sz, sx) + Math.PI, lonStep);966 const wire = ORB.wire * (0.3 + depth * 0.7);967 const minD = Math.min(latD, lonD);968 if (minD > wire) continue;969 970 const edge = 1 - minD / wire;971 if (r >= 0 && r < canvasRows && c >= 0 && c < canvasCols) {972 const reveal = buildPhase * 1.5 - hash(c, r) * 0.5;973 if (buildPhase < 1 && reveal <= 0) continue;974 const idx = r * canvasCols + c;975 const val = buildPhase < 1976 ? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))977 : Math.min(CHARS_LAST, Math.floor(edge * (0.4 + depth * 0.6) * CHARS.length));978 const weightedVal = Math.min(CHARS_LAST, Math.floor(val * sphereWeight));979 if (weightedVal <= 0) continue;980 orbMask[idx] = 1;981 orbGlyph[idx] = "";982 if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;983 }984 }985 }986 987 mergeObstacleRows();988}989 990let typedChars = 0;991let typedPrepared = null;992let typedDirty = true;993let typedBudget = 0;994 995function tickTyping(time, dt) {996 if (!bodyPrepared) return;997 if (time < codeTypingStartTime()) return;998 const cps = Math.max(1, ctrls.typeSpeed ?? 42);999 typedBudget += dt * cps;1000 const steps = Math.min(240, Math.floor(typedBudget));1001 if (steps > 0) {1002 typedBudget -= steps;1003 typedChars = Math.min(streamCorpus.length, typedChars + steps);1004 typedDirty = true;1005 }1006}1007 1008function getTypedPrepared() {1009 if (!typedDirty && typedPrepared) return typedPrepared;1010 const cap = Math.max(0, Math.floor(typedChars));1011 const visible = streamCorpus.slice(0, cap);1012 typedPrepared = prepareWithSegments(visible, BODY_FONT, { whiteSpace: "pre-wrap" });1013 typedDirty = false;1014 return typedPrepared;1015}1016 1017function layoutParagraphs() {1018 drawnLines.count = 0;1019 if (typedChars <= 0) return;1020 const prepared = getTypedPrepared();1021 ctx.font = BODY_FONT;1022 const MARGIN_X = Math.max(32, Math.min(72, W * 0.06));1023 const MIN_LINE_W = 120;1024 const yStart = Math.max(32, H * 0.08);1025 const yEnd = H - 32;1026 let cursor = { segmentIndex: 0, graphemeIndex: 0 };1027 1028 for (let y = yStart; y < yEnd; y += BODY_LINE_H) {1029 const row = Math.max(0, Math.min(rows - 1, Math.floor(y / CELL_H)));1030 const spans = obstacleRows[row] || [];1031 let n = 0;1032 let x = MARGIN_X;1033 1034 for (const [a, b] of spans) {1035 const left = Math.max(MARGIN_X, a);1036 const right = Math.min(W - MARGIN_X, b);1037 if (left - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = left - 10; }1038 x = Math.max(x, right + 10);1039 }1040 if (W - MARGIN_X - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = W - MARGIN_X; }1041 if (!n) { intervals[n++] = MARGIN_X; intervals[n++] = W - MARGIN_X; }1042 1043 for (let k = 0; k < n; k += 2) {1044 const ix0 = intervals[k], ix1 = intervals[k + 1];1045 const range = layoutNextLineRange(prepared, cursor, ix1 - ix0);1046 if (!range) return;1047 const line = materializeLineRange(prepared, range);1048 const i = drawnLines.count++;1049 drawnLines.x[i] = ix0;1050 drawnLines.y[i] = y;1051 drawnLines.w[i] = ix1 - ix0;1052 drawnLines.text[i] = line.text;1053 cursor = range.end;1054 }1055 }1056}1057 1058function escapeHTML(text) {1059 return text1060 .replaceAll("&", "&")1061 .replaceAll("<", "<")1062 .replaceAll(">", ">");1063}1064 1065function highlightSource(text) {1066 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;1067 let out = "";1068 let last = 0;1069 for (const match of text.matchAll(token)) {1070 const value = match[0];1071 out += escapeHTML(text.slice(last, match.index));1072 const cls = value.startsWith("#")1073 ? "tok-comment"1074 : value.startsWith('"') || value.startsWith("'")1075 ? "tok-string"1076 : /^\d/.test(value)1077 ? "tok-number"1078 : /^[A-Z_]{2,}$/.test(value)1079 ? "tok-const"1080 : "tok-keyword";1081 out += `<span class="${cls}">${escapeHTML(value)}</span>`;1082 last = match.index + value.length;1083 }1084 out += escapeHTML(text.slice(last));1085 return out;1086}1087 1088function renderTextLayer() {1089 const parts = [LENS.mgBase, drawnLines.count];1090 for (let i = 0; i < drawnLines.count; i++) {1091 parts.push(drawnLines.x[i] | 0, drawnLines.y[i] | 0, drawnLines.w[i] | 0, drawnLines.text[i]);1092 }1093 const signature = parts.join("|");1094 if (signature === lastTextSignature) return;1095 lastTextSignature = signature;1096 1097 ctx.font = BODY_FONT;1098 textLayer.style.color = LENS.mgBase;1099 textLayer.style.setProperty("--midground-base", LENS.mgBase);1100 1101 while (lineNodes.length < drawnLines.count) {1102 const el = document.createElement("div");1103 el.className = "flow-line";1104 textLayer.appendChild(el);1105 lineNodes.push(el);1106 }1107 terminalAnchorEl = null;1108 terminalLineEl = null;1109 for (let i = 0; i < lineNodes.length; i++) {1110 const el = lineNodes[i];1111 if (i >= drawnLines.count) {1112 el.hidden = true;1113 continue;1114 }1115 const text = drawnLines.text[i];1116 const isTail = i === drawnLines.count - 1;1117 el.hidden = false;1118 el.innerHTML = isTail1119 ? `${highlightSource(text)}<span class="cursor-anchor" aria-hidden="true"></span>`1120 : highlightSource(text);1121 el.style.left = `${drawnLines.x[i]}px`;1122 el.style.top = `${drawnLines.y[i] - BODY_LINE_H + 4}px`;1123 el.style.width = `${drawnLines.w[i]}px`;1124 el.style.textAlign = "left";1125 el.style.wordSpacing = "0px";1126 if (isTail) {1127 terminalAnchorEl = el.querySelector(".cursor-anchor");1128 terminalLineEl = el;1129 }1130 }1131}1132 1133 1134let sceneEpoch = 0;1135 1136function restartScene() {1137 gsap.killTweensOf(ORB);1138 gsap.killTweensOf(orbCanvas);1139 autoCubeCall?.kill();1140 autoCubeCall = null;1141 ORB.panX = 0;1142 ORB.panY = 0;1143 ORB.zoom = ctrls.orbZoom;1144 ORB.cubeScale = ctrls.cubeScale;1145 ORB.shape = ctrls.orbShape;1146 ORB.shapeMix = ctrls.orbShape === "cube" ? 1 : 0;1147 ORB.orbitX = -1.42;1148 ORB.orbitY = 0;1149 ORB.wire = ctrls.orbWire;1150 orbCanvas.style.opacity = "1";1151 ORB.staged = false;1152 ORB.positioned = true;1153 typedChars = 0;1154 typedBudget = 0;1155 typedPrepared = null;1156 typedDirty = true;1157 nousStartTime = null;1158 sceneEpoch = (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001;1159 lastTextSignature = "";1160 startSceneTimeline();1161}1162 1163function applyFontSizes() {1164 const bodyPx = ctrls.bodyFontSize;1165 const asciiPx = ctrls.asciiFontSize;1166 BODY_FONT = `400 ${bodyPx}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;1167 BODY_LINE_H = Math.round(bodyPx * 1.45);1168 ASCII_FONT = `${asciiPx}px monospace`;1169 CELL_H = asciiPx;1170 textLayer.style.font = `400 ${bodyPx}px/${BODY_LINE_H}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;1171 rebuildLayouts();1172 typedPrepared = null;1173 typedDirty = true;1174 resize();1175 lastTextSignature = "";1176}1177 1178function applyColors() {1179 if (!LENS.current) return;1180 LENS.current.bgColor = ctrls.bgColor;1181 LENS.current.mgColor = ctrls.mgColor;1182 LENS.current.fgColor = ctrls.fgColor;1183 applyLens(LENS.current);1184 lastTextSignature = "";1185}1186 1187const ctrls = {1188 bodyFontSize: 10,1189 asciiFontSize: 8,1190 autoSpin: 0.18,1191 orbShape: "sphere",1192 shapeMorphDuration: 0.65,1193 cubeScale: 1.8,1194 orbLead: 0.35,1195 orbZoom: 3,1196 orbRadius: 0.135,1197 orbWire: 0.2,1198 orbSpaceStart: 0.82,1199 orbSpaceDuration: 0.7,1200 orbSpaceEase: 1.1,1201 orbWireLow: 0.01,1202 wireDropDuration: 1.2,1203 orbFadeDuration: 0.85,1204 orbContainNous: 1.0,1205 nousAfterWire: 0,1206 nousLetterSpacing: 2.7,1207 typeSpeed: 816,1208 bgColor: "#041C1C",1209 mgColor: "#ffe6cb",1210 fgColor: "#FFFFFF",1211 noiseEnabled: true,1212 noiseColor: "#eaeaea",1213 noiseDensity: 0.11,1214 noiseOpacity: 0.25,1215 noiseSize: 1,1216 noiseBlend: "difference",1217 restart: () => restartScene(),1218};1219 1220const DEV_MODE = new URLSearchParams(window.location.search).has("dev");1221if (DEV_MODE) {1222 const gui = new GUI({ title: "controls" });1223 gui.add(ctrls, "bodyFontSize", 10, 22, 1).name("body font").onChange(applyFontSizes);1224 gui.add(ctrls, "asciiFontSize", 8, 20, 1).name("ascii font").onChange(applyFontSizes);1225 gui.add(ctrls, "autoSpin", 0, 0.6, 0.01).name("auto-spin").onChange(v => { ORB.autoSpin = v; });1226 orbShapeControl = gui.add(ctrls, "orbShape", ["sphere", "cube"]).name("orb shape").onChange(v => {1227 setOrbShape(v);1228 });1229 gui.add(ctrls, "shapeMorphDuration", 0.05, 2, 0.01).name("shape morph (s)");1230 gui.add(ctrls, "orbLead", 0, 6, 0.05).name("orb start (s)").onFinishChange(() => restartScene());1231 gui.add(ctrls, "orbZoom", 0.25, 12, 0.01).name("orb zoom").onChange(v => { ORB.zoom = v; });1232 gui.add(ctrls, "cubeScale", 0.8, 3.5, 0.01).name("cube scale").onChange(v => { ORB.cubeScale = v; });1233 gui.add(ctrls, "orbRadius", 0.08, 0.4, 0.005).name("orb radius").onChange(v => { ORB.radius = v; });1234 gui.add(ctrls, "orbSpaceStart", 0.4, 1, 0.01).name("space start");1235 gui.add(ctrls, "orbSpaceDuration", 0.2, 4, 0.05).name("space grow (s)");1236 gui.add(ctrls, "orbSpaceEase", 0.4, 3, 0.05).name("space ease");1237 gui.add(ctrls, "orbWire", 0.01, 0.2, 0.005).name("wire start").onChange(v => { ORB.wire = v; });1238 gui.add(ctrls, "orbWireLow", 0.005, 0.12, 0.001).name("wire low").onFinishChange(() => restartScene());1239 gui.add(ctrls, "wireDropDuration", 0.4, 6, 0.05).name("wire drop (s)").onFinishChange(() => restartScene());1240 gui.add(ctrls, "orbContainNous", 0, 3, 0.05).name("orb contain NOUS");1241 gui.add(ctrls, "orbFadeDuration", 0.1, 3, 0.05).name("orb fade on NOUS");1242 gui.add(ctrls, "nousAfterWire", 0, 4, 0.05).name("NOUS after wire").onFinishChange(() => restartScene());1243 gui.add(ctrls, "nousLetterSpacing", 0, 4, 0.05).name("NOUS spacing");1244 gui.add(ctrls, "typeSpeed", 0, 4000, 10).name("type speed");1245 gui.addColor(ctrls, "bgColor").name("background").onChange(applyColors);1246 gui.addColor(ctrls, "mgColor").name("midground").onChange(applyColors);1247 gui.addColor(ctrls, "fgColor").name("foreground").onChange(applyColors);1248 const noiseFolder = gui.addFolder("noise");1249 noiseFolder.add(ctrls, "noiseEnabled").name("on");1250 noiseFolder.addColor(ctrls, "noiseColor").name("color");1251 noiseFolder.add(ctrls, "noiseDensity", 0, 1, 0.01).name("density");1252 noiseFolder.add(ctrls, "noiseOpacity", 0, 1, 0.01).name("opacity");1253 noiseFolder.add(ctrls, "noiseSize", 0.1, 10, 0.1).name("size");1254 noiseFolder.add(ctrls, "noiseBlend", [1255 "normal", "multiply", "screen", "difference", "exclusion",1256 "color-dodge", "color-burn", "hard-light", "soft-light", "overlay", "lighten",1257 ]).name("blend").onChange(v => { document.querySelector(".noise").style.mixBlendMode = v; });1258 gui.add(ctrls, "restart").name("restart anim");1259}1260 1261const terminalCursorEl = document.getElementById("terminalCursor");1262function getLastCharRect(lineEl) {1263 if (!lineEl) return null;1264 const walker = document.createTreeWalker(lineEl, NodeFilter.SHOW_TEXT);1265 let lastTextNode = null;1266 let node = walker.nextNode();1267 while (node) {1268 if (node.nodeValue && node.nodeValue.length) lastTextNode = node;1269 node = walker.nextNode();1270 }1271 if (!lastTextNode) return null;1272 const end = lastTextNode.nodeValue.length;1273 if (!end) return null;1274 const range = document.createRange();1275 range.setStart(lastTextNode, end - 1);1276 range.setEnd(lastTextNode, end);1277 const rects = range.getClientRects();1278 const rect = rects.length ? rects[rects.length - 1] : range.getBoundingClientRect();1279 return rect && (rect.width || rect.height) ? rect : null;1280}1281function updateTerminalCursor() {1282 if (!drawnLines.count || !terminalAnchorEl || !terminalLineEl) {1283 terminalCursorEl.hidden = true;1284 return;1285 }1286 const stageRect = stage.getBoundingClientRect();1287 const charRect = getLastCharRect(terminalLineEl);1288 const anchorRect = terminalAnchorEl.getBoundingClientRect();1289 if (!anchorRect.width && !anchorRect.height) {1290 terminalCursorEl.hidden = true;1291 return;1292 }1293 const fontSize = parseInt(BODY_FONT.match(/(\d+)px/)?.[1] || "12", 10);1294 const fallbackW = Math.max(6, Math.round(fontSize * 0.6));1295 const leftPx = (charRect?.left ?? (anchorRect.left - fallbackW)) - stageRect.left;1296 const topPx = (charRect?.top ?? anchorRect.top) - stageRect.top;1297 const widthPx = Math.max(1, Math.round(charRect?.width || fallbackW));1298 const heightPx = Math.max(1, Math.round(charRect?.height || (BODY_LINE_H - 4)));1299 const left = Math.round(leftPx);1300 const top = Math.round(topPx);1301 terminalCursorEl.style.left = `${left}px`;1302 terminalCursorEl.style.top = `${top}px`;1303 terminalCursorEl.style.height = `${heightPx}px`;1304 terminalCursorEl.style.width = `${widthPx}px`;1305 terminalCursorEl.style.fontSize = `${fontSize}px`;1306 terminalCursorEl.hidden = false;1307}1308 1309const noiseCanvas = document.getElementById("noiseCanvas");1310const noiseGL = noiseCanvas.getContext("webgl", { alpha: true, premultipliedAlpha: true })1311 || noiseCanvas.getContext("experimental-webgl", { alpha: true, premultipliedAlpha: true });1312let noiseProgram = null;1313let noiseLocs = null;1314function initNoise() {1315 if (!noiseGL) return;1316 const vertSrc = `1317 attribute vec2 aPos;1318 varying vec2 vUv;1319 void main() {1320 vUv = aPos * 0.5 + 0.5;1321 gl_Position = vec4(aPos, 0.0, 1.0);1322 }1323 `;1324 const fragSrc = `1325 precision mediump float;1326 varying vec2 vUv;1327 uniform vec2 uRes;1328 uniform float uDpr;1329 uniform float uSize;1330 uniform float uDensity;1331 uniform float uOpacity;1332 uniform vec3 uColor;1333 float hash(vec2 p) {1334 vec3 p3 = fract(vec3(p.xyx) * 0.1031);1335 p3 += dot(p3, p3.yzx + 33.33);1336 return fract((p3.x + p3.y) * p3.z);1337 }1338 void main() {1339 float n = hash(floor(vUv * uRes / (uSize * uDpr)));1340 gl_FragColor = vec4(uColor, step(1.0 - uDensity, n)) * uOpacity;1341 }1342 `;1343 function compile(type, src) {1344 const sh = noiseGL.createShader(type);1345 noiseGL.shaderSource(sh, src);1346 noiseGL.compileShader(sh);1347 return sh;1348 }1349 const vs = compile(noiseGL.VERTEX_SHADER, vertSrc);1350 const fs = compile(noiseGL.FRAGMENT_SHADER, fragSrc);1351 noiseProgram = noiseGL.createProgram();1352 noiseGL.attachShader(noiseProgram, vs);1353 noiseGL.attachShader(noiseProgram, fs);1354 noiseGL.linkProgram(noiseProgram);1355 noiseGL.useProgram(noiseProgram);1356 1357 const buf = noiseGL.createBuffer();1358 noiseGL.bindBuffer(noiseGL.ARRAY_BUFFER, buf);1359 noiseGL.bufferData(noiseGL.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), noiseGL.STATIC_DRAW);1360 const aPos = noiseGL.getAttribLocation(noiseProgram, "aPos");1361 noiseGL.enableVertexAttribArray(aPos);1362 noiseGL.vertexAttribPointer(aPos, 2, noiseGL.FLOAT, false, 0, 0);1363 1364 noiseLocs = {1365 uRes: noiseGL.getUniformLocation(noiseProgram, "uRes"),1366 uDpr: noiseGL.getUniformLocation(noiseProgram, "uDpr"),1367 uSize: noiseGL.getUniformLocation(noiseProgram, "uSize"),1368 uDensity: noiseGL.getUniformLocation(noiseProgram, "uDensity"),1369 uOpacity: noiseGL.getUniformLocation(noiseProgram, "uOpacity"),1370 uColor: noiseGL.getUniformLocation(noiseProgram, "uColor"),1371 };1372 1373 noiseGL.enable(noiseGL.BLEND);1374 noiseGL.blendFunc(noiseGL.ONE, noiseGL.ONE_MINUS_SRC_ALPHA);1375 noiseGL.clearColor(0, 0, 0, 0);1376}1377 1378function resizeNoise() {1379 if (!noiseGL) return;1380 const dpr = Math.min(window.devicePixelRatio || 1, 2);1381 noiseCanvas.width = innerWidth * dpr;1382 noiseCanvas.height = innerHeight * dpr;1383 noiseGL.viewport(0, 0, noiseCanvas.width, noiseCanvas.height);1384 if (noiseLocs) {1385 noiseGL.uniform2f(noiseLocs.uRes, noiseCanvas.width, noiseCanvas.height);1386 noiseGL.uniform1f(noiseLocs.uDpr, dpr);1387 }1388}1389addEventListener("resize", resizeNoise);1390initNoise();1391resizeNoise();1392 1393function drawNoiseFrame() {1394 if (!noiseGL || !noiseProgram) return;1395 noiseGL.clear(noiseGL.COLOR_BUFFER_BIT);1396 if (!ctrls.noiseEnabled) return;1397 noiseGL.useProgram(noiseProgram);1398 noiseGL.uniform1f(noiseLocs.uSize, ctrls.noiseSize);1399 noiseGL.uniform1f(noiseLocs.uDensity, ctrls.noiseDensity);1400 noiseGL.uniform1f(noiseLocs.uOpacity, ctrls.noiseOpacity);1401 const [r, g, b] = hexToRGB(ctrls.noiseColor);1402 noiseGL.uniform3f(noiseLocs.uColor, r / 255, g / 255, b / 255);1403 noiseGL.drawArrays(noiseGL.TRIANGLE_STRIP, 0, 4);1404}1405 1406function draw(time) {1407 if (!fontsReady || !bodyPrepared) { requestAnimationFrame(draw); return; }1408 const t = time * 0.001 - sceneEpoch;1409 const dt = lastFrameTime ? Math.min(0.05, (time - lastFrameTime) / 1000) : 0.016;1410 lastFrameTime = time;1411 ctx.clearRect(0, 0, canvasW, canvasH);1412 orbCtx.clearRect(0, 0, canvasW, canvasH);1413 1414 rasterizeNOUS(t);1415 rasterizeOrb(t);1416 tickTyping(t, dt);1417 layoutParagraphs();1418 renderTextLayer();1419 updateTerminalCursor();1420 drawNoiseFrame();1421 1422 ctx.font = ASCII_FONT;1423 ctx.textBaseline = "middle";1424 ctx.textAlign = "center";1425 ctx.fillStyle = LENS.mgBase;1426 orbCtx.font = ASCII_FONT;1427 orbCtx.textBaseline = "middle";1428 orbCtx.textAlign = "center";1429 orbCtx.fillStyle = LENS.mgBase;1430 for (let r = 0; r < canvasRows; r++) {1431 const base = r * canvasCols;1432 for (let c = 0; c < canvasCols; c++) {1433 const idx = base + c;1434 if (asciiMask[idx] === EMPTY) continue;1435 ctx.fillText(asciiGlyph[idx] || CHARS[asciiChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);1436 }1437 }1438 for (let r = 0; r < canvasRows; r++) {1439 const base = r * canvasCols;1440 for (let c = 0; c < canvasCols; c++) {1441 const idx = base + c;1442 if (orbMask[idx] === EMPTY) continue;1443 orbCtx.fillText(orbGlyph[idx] || CHARS[orbChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);1444 }1445 }1446 1447 requestAnimationFrame(draw);1448}1449 1450resize();1451const DS_CDN = "https://esm.sh/@nous-research/ui@0.4.0/dist/fonts";1452const FACES = [new FontFace("Mondwest", `url(${DS_CDN}/Mondwest-Regular.woff2) format("woff2")`, { weight: "400", display: "block" })];1453(async () => {1454 try {1455 const loaded = await Promise.all(FACES.map(f => f.load()));1456 for (const f of loaded) document.fonts.add(f);1457 await document.fonts.load(BODY_FONT, "Aa");1458 await document.fonts.load(ASCII_FONT, "Aa");1459 } catch (err) {1460 console.warn("DS font load failed, using fallbacks:", err);1461 }1462 rebuildLayouts();1463 fontsReady = true;1464 requestAnimationFrame(draw);1465})();1466</script>1467</body>1468</html>1469 Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 148.SKILL.mdView in source ↗148See `templates/donut-orbit.html` and `templates/hello-orb-flow.html` for working single-file starters.
Source excerpt starting at line 154.154 - `templates/hello-orb-flow.html` — text reflowing around a moving orb (reflow-around-obstacle pattern)155 - `templates/donut-orbit.html` — advanced example: measured ASCII logo obstacles, draggable wire sphere/cube, morphing shape fields, selectable DOM text, and dev-only controls156 - `write_file` to a new `.html` in `/tmp/` or the user's workspace.