skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
|
2026-04-29 14:24:15 -05:00
|
|
|
<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">
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
<style>
|
|
|
|
|
:root { color-scheme: dark; }
|
2026-04-29 14:24:15 -05:00
|
|
|
|
|
|
|
|
: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;
|
|
|
|
|
}
|
|
|
|
|
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
html, body {
|
2026-04-29 14:24:15 -05:00
|
|
|
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;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
#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);
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
#annotationLayer {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 4;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
pointer-events: none;
|
2026-04-29 14:24:15 -05:00
|
|
|
overflow: visible;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
.annot {
|
|
|
|
|
position: absolute;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
pointer-events: none;
|
2026-04-29 14:24:15 -05:00
|
|
|
will-change: opacity, transform;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
.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; }
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
2026-04-29 14:24:15 -05:00
|
|
|
<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>
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
|
|
|
|
|
<script type="module">
|
|
|
|
|
import {
|
|
|
|
|
prepareWithSegments,
|
2026-04-29 14:24:15 -05:00
|
|
|
layoutNextLineRange,
|
|
|
|
|
materializeLineRange,
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
} from "https://esm.sh/@chenglou/pretext@0.0.6";
|
2026-04-29 14:24:15 -05:00
|
|
|
import gsap from "https://esm.sh/gsap@3.12.5";
|
|
|
|
|
import GUI from "https://esm.sh/lil-gui@0.19.2";
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
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");
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
const canvas = document.getElementById("c");
|
2026-04-29 14:24:15 -05:00
|
|
|
const orbCanvas = document.getElementById("orbCanvas");
|
|
|
|
|
const ctx = canvas.getContext("2d", { alpha: true });
|
|
|
|
|
const orbCtx = orbCanvas.getContext("2d", { alpha: true });
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
let W = 0, H = 0, DPR = 1;
|
2026-04-29 14:24:15 -05:00
|
|
|
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));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
function resize() {
|
2026-04-29 14:24:15 -05:00
|
|
|
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);
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
DPR = Math.min(window.devicePixelRatio || 1, 2);
|
2026-04-29 14:24:15 -05:00
|
|
|
canvas.width = Math.floor(canvasW * DPR);
|
|
|
|
|
canvas.height = Math.floor(canvasH * DPR);
|
|
|
|
|
orbCanvas.width = Math.floor(canvasW * DPR);
|
|
|
|
|
orbCanvas.height = Math.floor(canvasH * DPR);
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
2026-04-29 14:24:15 -05:00
|
|
|
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,
|
|
|
|
|
};
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
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;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
e.preventDefault();
|
2026-04-29 14:24:15 -05:00
|
|
|
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 = "";
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}, { passive: false });
|
2026-04-29 14:24:15 -05:00
|
|
|
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 });
|
|
|
|
|
});
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
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;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
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;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
mergeObstacleRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let typedChars = 0;
|
|
|
|
|
let typedPrepared = null;
|
|
|
|
|
let typedDirty = true;
|
|
|
|
|
let typedBudget = 0;
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
|
2026-04-29 14:24:15 -05:00
|
|
|
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;
|
|
|
|
|
}
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
}
|
2026-04-29 14:24:15 -05:00
|
|
|
|
|
|
|
|
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("&", "&")
|
|
|
|
|
.replaceAll("<", "<")
|
|
|
|
|
.replaceAll(">", ">");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
})();
|
skills: add pretext creative-demos skill
Adds a 'pretext' skill under skills/creative/ for building cool browser
demos with @chenglou/pretext — the 15KB DOM-free text-layout library by
Cheng Lou.
The skill documents pretext as a creative primitive (not plumbing): text
flowing around obstacles, text-as-geometry games, proportional ASCII
surfaces, shatter/particle typography, editorial multi-column, kinetic
type, and multiline shrink-wrap. Each pattern pairs with copy-pasteable
snippets in references/patterns.md.
Two single-file HTML templates, both verified in a browser:
templates/hello-orb-flow.html
Minimal starter: long paragraph flows around a mouse-tracked orb
using layoutNextLineRange + a per-row corridor-width function.
templates/donut-orbit.html
Full 3D Sloane torus with orbit controls (drag to rotate, scroll to
zoom, idle auto-rotate). Each 'luminance pixel' is a real grapheme
sampled in reading order from a prose corpus via pretext's
prepareWithSegments + layoutWithLines + Intl.Segmenter. Amber-on-
black CRT aesthetic, z-buffer keyed by screen cell, 60fps.
Related skills: p5js, claude-design, excalidraw, architecture-diagram.
2026-04-28 23:09:52 -05:00
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|