Add Go CDN and playground server

Build Core and Handles into in-memory CDN modules, add the marketing site and tutorial docs, and provide a SQLite-backed WebGPU playground with TypeScript tooling and profiling.

Amp-Thread-ID: https://ampcode.com/threads/T-01a02485-5574-707c-bff4-5668d83bee8a
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-21 16:42:02 +00:00
co-authored by heaust
parent 53a53a8f3f
commit 7070799862
27 changed files with 3402 additions and 7 deletions
+625
View File
@@ -0,0 +1,625 @@
const defaultSource = `import {
ArcRotateCamera,
ColorGrading,
FXAA,
Mesh,
PBRMaterial,
PointLight,
Scene,
} from "@yawn/handles";
const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;
const material = new PBRMaterial(scene, {
baseColor: [0.12, 0.58, 1, 1],
metallic: 0.15,
roughness: 0.35,
});
await material.ready;
const mesh = new Mesh(scene, {
material,
vertexData: {
positions: [-0.72, -0.62, 0, 0.72, -0.62, 0, 0, 0.76, 0],
normals: [0, 0, 1, 0, 0, 1, 0, 0, 1],
indices: [0, 1, 2],
},
});
await mesh.ready;
log("Scene ready — drag your pointer across the preview.");
const move = (event: PointerEvent) => {
const bounds = canvas.getBoundingClientRect();
mesh.position.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.2;
mesh.position.y = (0.5 - (event.clientY - bounds.top) / bounds.height) * 0.8;
};
canvas.addEventListener("pointermove", move);
export default {
scene,
mesh,
dispose() {
canvas.removeEventListener("pointermove", move);
scene.dispose();
},
};`;
const snippets = {
camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
const camera = new ArcRotateCamera(scene, {
target: mesh,
alpha: 0,
beta: Math.PI / 2,
radius: 3,
controls: { element: canvas, pointer: true },
});
await camera.ready;`,
light: `// A warm point light backed by shared rows.
const light = new PointLight(scene, {
position: [0, 0.7, 1],
color: [1, 0.68, 0.4],
intensity: 12,
range: 8,
});
await light.ready;`,
instances: `// Clones share geometry until one clone changes its vertex data.
for (let x = -2; x <= 2; x++) {
const instance = mesh.clone({ position: [x * 0.35, 0, 0] });
await instance.ready;
}`,
rows: `// Application-owned hot data in the same shared arena.
const velocity = await scene.ensureRows("app.velocity", 1024, 16, "f32");
velocity.row(0).set([1, 0, 0, 0]);
log("velocity[0]", velocity.read(0));`,
post: `// Batch graph changes so the loadout is rebuilt once.
await scene.batchGraphUpdates(async () => {
const grade = new ColorGrading(scene, { toneMap: "aces", amount: 1 });
const fxaa = new FXAA(scene);
await Promise.all([grade.ready, fxaa.ready]);
});`,
};
const elements = Object.fromEntries(
[
"canvas",
"clear-console",
"console-count",
"console-empty",
"console-lines",
"cursor-status",
"dirty-indicator",
"editor",
"editor-diagnostics",
"format-button",
"fps-status",
"fullscreen-button",
"inspector",
"inspector-button",
"preview",
"preview-message",
"profile-adapter",
"profile-empty",
"profile-latency",
"profile-passes",
"profile-results",
"profile-total",
"project-title",
"reset-button",
"resize-handle",
"resolution-status",
"revision-label",
"run-button",
"run-status",
"save-button",
"share-button",
"snippets-button",
"snippets-dialog",
"tab-dirty",
"toast",
"type-status",
"workbench",
].map((id) => [id, document.getElementById(id)]),
);
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const packages = Promise.all([import("/pkg/handles.js"), import("/pkg/core.js")]).then(
([handles, core]) => ({ ...handles, ...core }),
);
let monaco;
let editor;
let model;
let current;
let currentID = "";
let currentRevision = 0;
let initialSource = defaultSource;
let generation = 0;
let dirty = false;
let activePanel = "console";
let stopProfile;
let profileCore;
let toastTimer;
let frameRequest;
let sampledFrame = 0;
let sampledAt = 0;
function savedLocation() {
const match = location.pathname.match(/^\/playground\/([a-z0-9]{6,24})\/(\d+)\/?$/);
return match ? { id: match[1], revision: Number(match[2]) } : undefined;
}
async function loadInitialPlayground() {
const saved = savedLocation();
if (saved) {
try {
const response = await fetch(`/api/playgrounds/${saved.id}/${saved.revision}`);
if (!response.ok) throw new Error("This saved playground could not be found.");
const result = await response.json();
currentID = result.id;
currentRevision = result.revision;
elements["project-title"].value = result.title;
elements["revision-label"].textContent = `Revision ${result.revision}`;
elements["share-button"].hidden = false;
initialSource = result.code;
document.title = `${result.title} — Yawn Playground`;
return result.code;
} catch (error) {
history.replaceState(null, "", "/playground");
setRunStatus(error.message, "failed");
showToast(error.message);
}
}
return localStorage.getItem("yawn:playground:draft") || defaultSource;
}
function loadMonaco() {
return new Promise((resolve, reject) => {
if (!window.require?.config) {
reject(new Error("The TypeScript editor could not be loaded."));
return;
}
const monacoBase = `${window.location.origin}/assets/monaco/`;
const workerSource = `
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoBase)} };
importScripts(${JSON.stringify(`${monacoBase}vs/base/worker/workerMain.js`)});
`;
const workerURL = URL.createObjectURL(
new Blob([workerSource], { type: "text/javascript" }),
);
window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
window.require.config({ paths: { vs: `${monacoBase}vs` } });
window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
});
}
async function createEditor(source) {
monaco = await loadMonaco();
const types = await fetch("/assets/yawn.d.ts").then((response) => response.text());
const defaults = monaco.languages.typescript.typescriptDefaults;
defaults.setEagerModelSync(true);
defaults.setCompilerOptions({
allowNonTsExtensions: true,
module: monaco.languages.typescript.ModuleKind.ESNext,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
noEmit: false,
strict: true,
target: monaco.languages.typescript.ScriptTarget.ESNext,
});
defaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false });
defaults.addExtraLib(types, "file:///types/yawn.d.ts");
monaco.editor.defineTheme("yawn", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "comment", foreground: "658198", fontStyle: "italic" },
{ token: "keyword", foreground: "C792EA" },
{ token: "string", foreground: "8FD5B6" },
{ token: "number", foreground: "F6AD7B" },
{ token: "type.identifier", foreground: "69C7F4" },
],
colors: {
"editor.background": "#0B1927",
"editor.foreground": "#C8D8E5",
"editor.lineHighlightBackground": "#102235",
"editor.selectionBackground": "#1D5B8066",
"editorCursor.foreground": "#55BDF4",
"editorGutter.background": "#0B1927",
"editorLineNumber.foreground": "#405B70",
"editorLineNumber.activeForeground": "#89A4B8",
"editorIndentGuide.background1": "#1B3143",
"editorWidget.background": "#102538",
"editorSuggestWidget.background": "#102538",
},
});
elements.editor.textContent = "";
model = monaco.editor.createModel(source, "typescript", monaco.Uri.parse("file:///playground/scene.ts"));
editor = monaco.editor.create(elements.editor, {
model,
theme: "yawn",
automaticLayout: true,
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
fontLigatures: true,
fontSize: 12,
lineHeight: 20,
minimap: { enabled: false },
padding: { top: 12, bottom: 12 },
renderLineHighlight: "all",
scrollBeyondLastLine: false,
smoothScrolling: true,
tabSize: 2,
wordWrap: "off",
});
editor.addAction({
id: "yawn.run",
label: "Run playground",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run,
});
editor.onDidChangeCursorPosition(({ position }) => {
elements["cursor-status"].textContent = `Ln ${position.lineNumber}, Col ${position.column}`;
});
model.onDidChangeContent(() => {
markDirty(true);
if (!currentID) localStorage.setItem("yawn:playground:draft", model.getValue());
});
monaco.editor.onDidChangeMarkers(([resource]) => {
if (resource.toString() === model.uri.toString()) updateDiagnostics();
});
elements["type-status"].textContent = "TypeScript language service ready";
updateDiagnostics();
}
function updateDiagnostics() {
if (!monaco || !model) return;
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
const errors = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Error).length;
const warnings = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Warning).length;
const total = errors + warnings;
elements["editor-diagnostics"].classList.toggle("has-errors", errors > 0);
elements["editor-diagnostics"].lastChild.textContent = total
? ` ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`
: " No problems";
elements["type-status"].textContent = errors
? `${errors} TypeScript error${errors === 1 ? "" : "s"}`
: "TypeScript language service ready";
}
function markDirty(value) {
dirty = value;
elements["dirty-indicator"].classList.toggle("visible", value);
elements["tab-dirty"].classList.toggle("visible", value);
}
async function transpile() {
const worker = await monaco.languages.typescript.getTypeScriptWorker();
const client = await worker(model.uri);
const emitted = await client.getEmitOutput(model.uri.toString());
const output = emitted.outputFiles.find((file) => file.name.endsWith(".js"));
if (emitted.emitSkipped || !output) throw new Error("TypeScript could not emit this scene.");
return output.text
.replace(/^\s*import\s+(?:[\s\S]*?\s+from\s+)?["'][^"']+["'];?\s*$/gm, "")
.replace(/^\s*export\s*\{\s*\};?\s*$/gm, "")
.replace(/\bexport\s+default\s+/, "return ");
}
async function disposeCurrent() {
stopProfile?.();
stopProfile = undefined;
if (profileCore) {
try {
await profileCore.setProfiler(false);
} catch {
// A disposed core cannot disable a profiler that is already gone.
}
profileCore = undefined;
}
const value = current;
current = undefined;
delete window.__yawnPlayground;
if (!value) return;
if (typeof value === "function") await value();
else if (typeof value.dispose === "function") await value.dispose();
else if (value.scene?.dispose) value.scene.dispose();
}
function replaceCanvas() {
const previous = elements.canvas;
const canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.setAttribute("aria-label", "Yawn WebGPU preview");
const scale = Math.min(devicePixelRatio || 1, 2);
canvas.width = Math.max(1, Math.round(elements.preview.clientWidth * scale));
canvas.height = Math.max(1, Math.round(elements.preview.clientHeight * scale));
previous.replaceWith(canvas);
elements.canvas = canvas;
elements["resolution-status"].textContent = `${canvas.width} × ${canvas.height}`;
return canvas;
}
async function run() {
if (!editor || elements["run-button"].disabled) return;
const runID = ++generation;
elements["run-button"].disabled = true;
elements["run-button"].lastChild.textContent = " Running";
clearConsole();
setRunStatus("Compiling", "running");
showPreviewMessage("Compiling TypeScript", "Checking types and preparing the render graph.");
try {
if (!crossOriginIsolated) throw new Error("Cross-origin isolation is disabled on this page.");
if (!navigator.gpu) throw new Error("WebGPU is not available in this browser.");
const [code, api] = await Promise.all([transpile(), packages]);
await disposeCurrent();
const canvas = replaceCanvas();
const names = Object.keys(api);
const started = performance.now();
const result = await new AsyncFunction(...names, "canvas", "log", code)(
...names.map((name) => api[name]),
canvas,
(...values) => appendConsole(values.map(formatValue).join(" ")),
);
if (runID !== generation) {
if (result?.dispose) await result.dispose();
return;
}
current = result;
window.__yawnPlayground = result;
elements["preview-message"].hidden = true;
setRunStatus(`Running · ${Math.round(performance.now() - started)} ms`, "ready");
if (activePanel === "profile") await attachProfiler();
} catch (error) {
if (runID === generation) {
const message = error instanceof Error ? error.message : String(error);
appendConsole(message, true);
setRunStatus("Run failed", "failed");
showPreviewMessage("Scene could not start", message);
openInspector("console");
}
} finally {
if (runID === generation) {
elements["run-button"].disabled = false;
elements["run-button"].innerHTML = '<span aria-hidden="true">▶</span> Run <kbd>Ctrl ↵</kbd>';
}
}
}
function currentCore() {
return current?.scene?.core ?? current?.core;
}
async function attachProfiler() {
stopProfile?.();
stopProfile = undefined;
profileCore = currentCore();
if (!profileCore?.onProfile || !profileCore?.setProfiler) {
setProfileMessage("Run a scene to begin profiling.");
return;
}
stopProfile = profileCore.onProfile(renderProfile);
const supported = await profileCore.setProfiler(true);
if (!supported) setProfileMessage("Timestamp queries are not supported by this GPU adapter.");
}
function setProfileMessage(message) {
elements["profile-empty"].querySelector("strong").textContent = message;
elements["profile-empty"].hidden = false;
elements["profile-results"].hidden = true;
}
function renderProfile(profile) {
elements["profile-empty"].hidden = true;
elements["profile-results"].hidden = false;
elements["profile-total"].textContent = `${profile.milliseconds.toFixed(2)} ms`;
elements["profile-latency"].textContent = `${profile.readbackMilliseconds.toFixed(2)} ms`;
elements["profile-adapter"].textContent = profile.adapter || "WebGPU adapter";
elements["profile-adapter"].title = profile.adapter || "WebGPU adapter";
elements["profile-passes"].replaceChildren(
...profile.passes.map((pass) => {
const row = document.createElement("tr");
const name = document.createElement("td");
const time = document.createElement("td");
name.textContent = pass.name;
time.textContent = `${pass.milliseconds.toFixed(2)} ms`;
row.append(name, time);
return row;
}),
);
}
function formatValue(value) {
if (typeof value === "string") return value;
if (value instanceof Error) return value.message;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function appendConsole(message, error = false) {
elements["console-empty"].hidden = true;
const line = document.createElement("li");
line.textContent = message;
line.classList.toggle("error", error);
elements["console-lines"].append(line);
elements["console-count"].textContent = elements["console-lines"].children.length;
line.scrollIntoView({ block: "nearest" });
}
function clearConsole() {
elements["console-lines"].replaceChildren();
elements["console-empty"].hidden = false;
elements["console-count"].textContent = "0";
}
function setRunStatus(message, state) {
elements["run-status"].className = state === "running" ? "running" : state === "failed" ? "failed" : "";
elements["run-status"].lastChild.textContent = ` ${message}`;
}
function showPreviewMessage(title, detail) {
elements["preview-message"].hidden = false;
elements["preview-message"].querySelector("strong").textContent = title;
elements["preview-message"].querySelector("small").textContent = detail;
}
function openInspector(panel) {
activePanel = panel;
elements.inspector.classList.remove("collapsed");
elements["inspector-button"].setAttribute("aria-pressed", "true");
for (const button of document.querySelectorAll(".inspector-tabs [data-panel]")) {
const active = button.dataset.panel === panel;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
}
for (const target of document.querySelectorAll(".inspector-panel")) {
target.classList.toggle("active", target.id === `${panel}-panel`);
}
if (panel === "profile") void attachProfiler();
else if (profileCore) {
stopProfile?.();
stopProfile = undefined;
void profileCore.setProfiler(false).catch(() => undefined);
profileCore = undefined;
}
}
async function save() {
if (!editor || elements["save-button"].disabled) return;
elements["save-button"].disabled = true;
try {
const response = await fetch("/api/playgrounds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: currentID,
title: elements["project-title"].value.trim() || "Untitled playground",
code: model.getValue(),
}),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || "The playground could not be saved.");
currentID = result.id;
currentRevision = result.revision;
initialSource = result.code;
history.pushState(null, "", `/playground/${result.id}/${result.revision}`);
elements["revision-label"].textContent = `Revision ${result.revision}`;
elements["share-button"].hidden = false;
localStorage.removeItem("yawn:playground:draft");
markDirty(false);
showToast(`Saved revision ${result.revision}`);
} catch (error) {
showToast(error instanceof Error ? error.message : String(error));
} finally {
elements["save-button"].disabled = false;
}
}
function showToast(message) {
clearTimeout(toastTimer);
elements.toast.textContent = message;
elements.toast.classList.add("visible");
toastTimer = setTimeout(() => elements.toast.classList.remove("visible"), 2400);
}
function insertSnippet(name) {
const selection = editor.getSelection();
const prefix = model.getValueInRange(selection).endsWith("\n") ? "" : "\n\n";
editor.executeEdits("snippet", [{ range: selection, text: `${prefix}${snippets[name]}\n`, forceMoveMarkers: true }]);
elements["snippets-dialog"].close();
editor.focus();
}
function sampleFPS(now = performance.now()) {
try {
const core = currentCore();
if (!core) throw new Error();
const frame = Number(core.array("signals").row(0)[1]);
if (!sampledAt || frame < sampledFrame) {
sampledAt = now;
sampledFrame = frame;
} else if (now - sampledAt >= 500) {
const fps = ((frame - sampledFrame) * 1000) / (now - sampledAt);
elements["fps-status"].textContent = `${fps < 10 ? fps.toFixed(1) : Math.round(fps)} FPS`;
sampledAt = now;
sampledFrame = frame;
}
} catch {
sampledAt = now;
sampledFrame = 0;
elements["fps-status"].textContent = "0 FPS";
}
frameRequest = requestAnimationFrame(sampleFPS);
}
elements["run-button"].addEventListener("click", run);
elements["save-button"].addEventListener("click", save);
elements["share-button"].addEventListener("click", async () => {
await navigator.clipboard.writeText(location.href);
showToast("Playground URL copied");
});
elements["project-title"].addEventListener("input", () => markDirty(true));
elements["reset-button"].addEventListener("click", () => {
model?.setValue(initialSource);
void run();
});
elements["format-button"].addEventListener("click", () => editor?.getAction("editor.action.formatDocument").run());
elements["snippets-button"].addEventListener("click", () => elements["snippets-dialog"].showModal());
elements["snippets-dialog"].querySelector("[data-close-dialog]").addEventListener("click", () => elements["snippets-dialog"].close());
elements["snippets-dialog"].addEventListener("click", (event) => {
if (event.target === elements["snippets-dialog"]) elements["snippets-dialog"].close();
});
for (const button of document.querySelectorAll("[data-snippet]")) {
button.addEventListener("click", () => insertSnippet(button.dataset.snippet));
}
elements["snippets-dialog"].querySelector("input").addEventListener("input", (event) => {
const query = event.target.value.trim().toLowerCase();
for (const button of document.querySelectorAll("[data-snippet]")) {
button.hidden = Boolean(query) && !button.textContent.toLowerCase().includes(query);
}
});
for (const button of document.querySelectorAll(".inspector-tabs [data-panel]")) {
button.addEventListener("click", () => openInspector(button.dataset.panel));
}
elements["clear-console"].addEventListener("click", clearConsole);
elements["inspector-button"].addEventListener("click", () => {
const collapsed = elements.inspector.classList.toggle("collapsed");
elements["inspector-button"].setAttribute("aria-pressed", String(!collapsed));
});
elements["fullscreen-button"].addEventListener("click", () => {
if (document.fullscreenElement) void document.exitFullscreen();
else void document.documentElement.requestFullscreen();
});
elements["resize-handle"].addEventListener("pointerdown", (event) => {
if (innerWidth <= 800) return;
elements["resize-handle"].setPointerCapture(event.pointerId);
elements["resize-handle"].classList.add("dragging");
});
elements["resize-handle"].addEventListener("pointermove", (event) => {
if (!elements["resize-handle"].hasPointerCapture(event.pointerId)) return;
const bounds = elements.workbench.getBoundingClientRect();
const percentage = Math.min(70, Math.max(30, ((event.clientX - bounds.left) / bounds.width) * 100));
elements.workbench.style.setProperty("--editor-width", `${percentage}%`);
});
elements["resize-handle"].addEventListener("pointerup", (event) => {
elements["resize-handle"].releasePointerCapture(event.pointerId);
elements["resize-handle"].classList.remove("dragging");
});
addEventListener("popstate", () => location.reload());
addEventListener("beforeunload", () => {
cancelAnimationFrame(frameRequest);
void disposeCurrent();
});
try {
const source = await loadInitialPlayground();
await createEditor(source);
markDirty(false);
sampleFPS();
await run();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
elements.editor.textContent = message;
setRunStatus("Editor failed", "failed");
showPreviewMessage("Editor could not start", message);
}