Enhance CDN packages and playground

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-22 06:21:39 +00:00
co-authored by heaust
parent 7070799862
commit d95e099fa4
17 changed files with 1870 additions and 247 deletions
+19 -7
View File
@@ -4,7 +4,8 @@ import process from "node:process";
import { build } from "esbuild";
const [root, output] = process.argv.slice(2);
if (!root || !output) throw new Error("usage: node build.mjs <repository> <output>");
if (!root || !output)
throw new Error("usage: node build.mjs <repository> <output>");
const browser = {
bundle: true,
@@ -17,8 +18,14 @@ const browser = {
const workerFiles = new Map([
[path.join(root, "core", "index.js"), "./worker.js"],
[path.join(root, "addons", "handles", "src", "importers", "gltf.ts"), "./importer-worker.js"],
[path.join(root, "addons", "handles", "src", "bvh", "picking.ts"), "./bvh-worker.js"],
[
path.join(root, "addons", "handles", "src", "importers", "gltf.ts"),
"./importer-worker.js",
],
[
path.join(root, "addons", "handles", "src", "bvh", "picking.ts"),
"./bvh-worker.js",
],
]);
const workerURLs = {
@@ -32,7 +39,8 @@ const workerURLs = {
/new Worker\(new URL\("\.\/worker\.(?:js|ts)", import\.meta\.url\),/,
`createYawnWorker(new URL(${JSON.stringify(worker)}, import.meta.url),`,
);
if (transformed === contents) throw new Error(`worker constructor not found in ${source}`);
if (transformed === contents)
throw new Error(`worker constructor not found in ${source}`);
contents = `
function createYawnWorker(url, options) {
if (url.origin === globalThis.location.origin) return new Worker(url, options);
@@ -57,8 +65,8 @@ await Promise.all([
}),
build({
...browser,
alias: { "@yawn/core": path.join(root, "core", "index.js") },
entryPoints: [path.join(root, "addons", "handles", "src", "index.ts")],
external: ["@yawn/core"],
outfile: path.join(output, "handles.js"),
plugins: [workerURLs],
}),
@@ -73,12 +81,16 @@ await Promise.all([
}),
build({
...browser,
entryPoints: [path.join(root, "addons", "handles", "src", "importers", "worker.ts")],
entryPoints: [
path.join(root, "addons", "handles", "src", "importers", "worker.ts"),
],
outfile: path.join(output, "importer-worker.js"),
}),
build({
...browser,
entryPoints: [path.join(root, "addons", "handles", "src", "bvh", "worker.ts")],
entryPoints: [
path.join(root, "addons", "handles", "src", "bvh", "worker.ts"),
],
outfile: path.join(output, "bvh-worker.js"),
}),
]);
+84
View File
@@ -0,0 +1,84 @@
<!doctype html>
<!-- The host serving this page must send COOP: same-origin and COEP: require-corp headers. -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Yawn CDN example</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
color: #15304a;
background: #f4faff;
font:
16px/1.5 system-ui,
sans-serif;
}
main {
width: min(720px, calc(100% - 32px));
}
canvas {
width: 100%;
aspect-ratio: 16 / 9;
display: block;
background: white;
}
#status {
margin: 12px 0 0;
}
</style>
<script type="importmap">
{
"imports": {
"@yawn/core": "https://yawn.heaust.org/pkg/core.js",
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
}
}
</script>
</head>
<body>
<main>
<canvas id="view" width="1280" height="720"></canvas>
<p id="status">Starting Yawn…</p>
</main>
<script type="module">
import { YawnCore } from "@yawn/core";
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
const canvas = document.querySelector("#view");
const status = document.querySelector("#status");
const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;
if (!(scene.core instanceof YawnCore)) {
throw new Error(
"Handles did not use the separately imported Core module.",
);
}
const material = new PBRMaterial(scene, {
baseColor: [0.12, 0.58, 1, 1],
metallic: 0.15,
roughness: 0.35,
});
await material.ready;
const triangle = 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 triangle.ready;
status.textContent = "Scene ready — Core and Handles loaded separately.";
document.documentElement.dataset.ready = "true";
</script>
</body>
</html>
+255 -75
View File
@@ -1,60 +1,98 @@
const defaultSource = `import {
AmbientLight,
ArcRotateCamera,
ColorGrading,
FXAA,
Mesh,
Icosphere,
PBRMaterial,
Plane,
PointLight,
Scene,
SpotLight,
} from "@yawn/handles";
const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;
await scene.reserve({ nodes: 6, materials: 2 });
const material = new PBRMaterial(scene, {
baseColor: [0.12, 0.58, 1, 1],
metallic: 0.15,
roughness: 0.35,
const objectMaterial = new PBRMaterial(scene, {
baseColor: [0.82, 0.86, 0.9, 1],
metallic: 0.12,
roughness: 0.32,
});
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],
},
const groundMaterial = new PBRMaterial(scene, {
baseColor: [0.24, 0.28, 0.34, 1],
metallic: 0,
roughness: 0.82,
});
await mesh.ready;
await Promise.all([objectMaterial.ready, groundMaterial.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);
const icosphere = new Icosphere(scene, {
material: objectMaterial,
subdivisions: 2,
position: [0, 0.2, 0],
});
const ground = new Plane(scene, {
material: groundMaterial,
size: 2.6,
position: [0, -0.55, 0],
});
await Promise.all([icosphere.ready, ground.ready]);
icosphere.rotateY(Math.PI / 5);
const camera = new ArcRotateCamera(scene, {
target: icosphere,
alpha: Math.PI / 4,
beta: Math.PI / 3,
radius: 4,
aspect: canvas.width / canvas.height,
controls: { element: canvas, pointer: true },
});
await camera.ready;
const lights = [
new AmbientLight(scene, { color: [0.02, 0.45, 0.48], intensity: 0.5 }),
new SpotLight(scene, {
position: [-2.8, 3, -2.8],
rotor: [-0.628, -0.628, 0, 0.4597],
color: [1, 0.22, 0.03],
intensity: 220,
range: 7,
innerAngle: 0.32,
outerAngle: 0.72,
}),
new PointLight(scene, {
position: [2.2, 0.9, 1.8],
color: [1, 0.02, 0.45],
intensity: 16,
range: 6,
}),
];
await Promise.all(lights.map((light) => light.ready));
log("Icosphere ready — teal ambience, orange rim, and magenta accent are active.");
export default {
scene,
mesh,
icosphere,
ground,
lights,
dispose() {
canvas.removeEventListener("pointermove", move);
camera.detachControls();
scene.dispose();
},
};`;
const snippets = {
camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
const camera = new ArcRotateCamera(scene, {
target: mesh,
const orbitCamera = new ArcRotateCamera(scene, {
target: icosphere,
alpha: 0,
beta: Math.PI / 2,
radius: 3,
controls: { element: canvas, pointer: true },
});
await camera.ready;`,
await orbitCamera.ready;`,
light: `// A warm point light backed by shared rows.
const light = new PointLight(scene, {
position: [0, 0.7, 1],
@@ -65,7 +103,7 @@ const light = new PointLight(scene, {
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] });
const instance = icosphere.clone({ position: [x * 0.7, 0.2, 0] });
await instance.ready;
}`,
rows: `// Application-owned hot data in the same shared arena.
@@ -123,9 +161,59 @@ const elements = Object.fromEntries(
);
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const packages = Promise.all([import("/pkg/handles.js"), import("/pkg/core.js")]).then(
([handles, core]) => ({ ...handles, ...core }),
);
const nativeSetTimeout = window.setTimeout.bind(window);
const nativeSetInterval = window.setInterval.bind(window);
const nativeClearTimeout = window.clearTimeout.bind(window);
const nativeClearInterval = window.clearInterval.bind(window);
const playgroundTimerIDs = new Set();
function trackedSetTimeout(callback, delay, ...arguments_) {
let id;
const wrapped =
typeof callback === "function"
? (...callbackArguments) => {
playgroundTimerIDs.delete(id);
callback.apply(window, callbackArguments);
}
: callback;
id = nativeSetTimeout(wrapped, delay, ...arguments_);
playgroundTimerIDs.add(id);
return id;
}
function trackedSetInterval(callback, delay, ...arguments_) {
const id = nativeSetInterval(callback, delay, ...arguments_);
playgroundTimerIDs.add(id);
return id;
}
function trackedClearTimer(id) {
playgroundTimerIDs.delete(id);
nativeClearTimeout(id);
nativeClearInterval(id);
}
function clearPlaygroundTimers() {
for (const id of playgroundTimerIDs) {
nativeClearTimeout(id);
nativeClearInterval(id);
}
playgroundTimerIDs.clear();
}
window.__yawnPlaygroundTimers = {
ids: playgroundTimerIDs,
setTimeout: trackedSetTimeout,
setInterval: trackedSetInterval,
clearTimeout: trackedClearTimer,
clearInterval: trackedClearTimer,
clear: clearPlaygroundTimers,
};
const packages = Promise.all([
import("@yawn/handles"),
import("@yawn/core"),
]).then(([handles, core]) => ({ ...handles, ...core }));
let monaco;
let editor;
let model;
@@ -143,8 +231,14 @@ let frameRequest;
let sampledFrame = 0;
let sampledAt = 0;
addEventListener("yawn-theme-change", (event) => {
monaco?.editor.setTheme(event.detail.resolved === "dark" ? "yawn" : "vs");
});
function savedLocation() {
const match = location.pathname.match(/^\/playground\/([a-z0-9]{6,24})\/(\d+)\/?$/);
const match = location.pathname.match(
/^\/playground\/([a-z0-9]{6,24})\/(\d+)\/?$/,
);
return match ? { id: match[1], revision: Number(match[2]) } : undefined;
}
@@ -152,8 +246,11 @@ 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 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;
@@ -188,13 +285,19 @@ importScripts(${JSON.stringify(`${monacoBase}vs/base/worker/workerMain.js`)});
);
window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
window.require.config({ paths: { vs: `${monacoBase}vs` } });
window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
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 types = await fetch("/assets/yawn.d.ts").then((response) =>
response.text(),
);
const defaults = monaco.languages.typescript.typescriptDefaults;
defaults.setEagerModelSync(true);
defaults.setCompilerOptions({
@@ -205,7 +308,10 @@ async function createEditor(source) {
strict: true,
target: monaco.languages.typescript.ScriptTarget.ESNext,
});
defaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false });
defaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
});
defaults.addExtraLib(types, "file:///types/yawn.d.ts");
monaco.editor.defineTheme("yawn", {
base: "vs-dark",
@@ -232,12 +338,17 @@ async function createEditor(source) {
},
});
elements.editor.textContent = "";
model = monaco.editor.createModel(source, "typescript", monaco.Uri.parse("file:///playground/scene.ts"));
model = monaco.editor.createModel(
source,
"typescript",
monaco.Uri.parse("file:///playground/scene.ts"),
);
editor = monaco.editor.create(elements.editor, {
model,
theme: "yawn",
theme: document.documentElement.dataset.theme === "dark" ? "yawn" : "vs",
automaticLayout: true,
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
fontLigatures: true,
fontSize: 12,
lineHeight: 20,
@@ -256,11 +367,13 @@ async function createEditor(source) {
run,
});
editor.onDidChangeCursorPosition(({ position }) => {
elements["cursor-status"].textContent = `Ln ${position.lineNumber}, Col ${position.column}`;
elements["cursor-status"].textContent =
`Ln ${position.lineNumber}, Col ${position.column}`;
});
model.onDidChangeContent(() => {
markDirty(true);
if (!currentID) localStorage.setItem("yawn:playground:draft", model.getValue());
if (!currentID)
localStorage.setItem("yawn:playground:draft", model.getValue());
});
monaco.editor.onDidChangeMarkers(([resource]) => {
if (resource.toString() === model.uri.toString()) updateDiagnostics();
@@ -272,8 +385,12 @@ async function createEditor(source) {
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 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
@@ -295,7 +412,8 @@ async function transpile() {
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.");
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, "")
@@ -329,33 +447,56 @@ function replaceCanvas() {
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));
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}`;
elements["resolution-status"].textContent =
`${canvas.width} × ${canvas.height}`;
return canvas;
}
async function run() {
if (!editor || elements["run-button"].disabled) return;
clearPlaygroundTimers();
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.");
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.");
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)(
const result = await new AsyncFunction(
...names,
"canvas",
"log",
"setTimeout",
"setInterval",
"clearTimeout",
"clearInterval",
code,
)(
...names.map((name) => api[name]),
canvas,
(...values) => appendConsole(values.map(formatValue).join(" ")),
trackedSetTimeout,
trackedSetInterval,
trackedClearTimer,
trackedClearTimer,
);
if (runID !== generation) {
if (result?.dispose) await result.dispose();
@@ -364,7 +505,10 @@ async function run() {
current = result;
window.__yawnPlayground = result;
elements["preview-message"].hidden = true;
setRunStatus(`Running · ${Math.round(performance.now() - started)} ms`, "ready");
setRunStatus(
`Running · ${Math.round(performance.now() - started)} ms`,
"ready",
);
if (activePanel === "profile") await attachProfiler();
} catch (error) {
if (runID === generation) {
@@ -377,7 +521,8 @@ async function run() {
} finally {
if (runID === generation) {
elements["run-button"].disabled = false;
elements["run-button"].innerHTML = '<span aria-hidden="true">▶</span> Run <kbd>Ctrl ↵</kbd>';
elements["run-button"].innerHTML =
'<span aria-hidden="true">▶</span> Run <kbd>Ctrl ↵</kbd>';
}
}
}
@@ -396,7 +541,10 @@ async function attachProfiler() {
}
stopProfile = profileCore.onProfile(renderProfile);
const supported = await profileCore.setProfiler(true);
if (!supported) setProfileMessage("Timestamp queries are not supported by this GPU adapter.");
if (!supported)
setProfileMessage(
"Timestamp queries are not supported by this GPU adapter.",
);
}
function setProfileMessage(message) {
@@ -408,8 +556,10 @@ function setProfileMessage(message) {
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-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(
@@ -441,7 +591,8 @@ function appendConsole(message, error = false) {
line.textContent = message;
line.classList.toggle("error", error);
elements["console-lines"].append(line);
elements["console-count"].textContent = elements["console-lines"].children.length;
elements["console-count"].textContent =
elements["console-lines"].children.length;
line.scrollIntoView({ block: "nearest" });
}
@@ -452,7 +603,8 @@ function clearConsole() {
}
function setRunStatus(message, state) {
elements["run-status"].className = state === "running" ? "running" : state === "failed" ? "failed" : "";
elements["run-status"].className =
state === "running" ? "running" : state === "failed" ? "failed" : "";
elements["run-status"].lastChild.textContent = ` ${message}`;
}
@@ -466,7 +618,9 @@ 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]")) {
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));
@@ -497,7 +651,8 @@ async function save() {
}),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || "The playground could not be saved.");
if (!response.ok)
throw new Error(result.error || "The playground could not be saved.");
currentID = result.id;
currentRevision = result.revision;
initialSource = result.code;
@@ -518,13 +673,22 @@ function showToast(message) {
clearTimeout(toastTimer);
elements.toast.textContent = message;
elements.toast.classList.add("visible");
toastTimer = setTimeout(() => elements.toast.classList.remove("visible"), 2400);
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 }]);
editor.executeEdits("snippet", [
{
range: selection,
text: `${prefix}${snippets[name]}\n`,
forceMoveMarkers: true,
},
]);
elements["snippets-dialog"].close();
editor.focus();
}
@@ -539,7 +703,8 @@ function sampleFPS(now = performance.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`;
elements["fps-status"].textContent =
`${fps < 10 ? fps.toFixed(1) : Math.round(fps)} FPS`;
sampledAt = now;
sampledFrame = frame;
}
@@ -562,22 +727,34 @@ 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["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();
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]")) {
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);
@@ -598,7 +775,10 @@ elements["resize-handle"].addEventListener("pointerdown", (event) => {
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));
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) => {
+457
View File
@@ -0,0 +1,457 @@
.theme-select {
width: 72px;
height: 32px;
padding: 0 7px;
color: #53667c;
border: 1px solid #d4e1ea;
border-radius: 6px;
outline: 0;
background: #fff;
cursor: pointer;
font-size: 11px;
font-weight: 650;
}
.theme-select:hover {
color: #177fc2;
border-color: #add5ee;
}
.theme-select:focus-visible {
border-color: #62b8eb;
box-shadow: 0 0 0 3px rgb(50 166 232 / 14%);
}
.tool-context {
font-size: 10px;
}
.stage-meta,
.editor-status {
font-size: 9px;
}
html[data-theme="dark"] {
--ink: #d9e9f7;
--muted: #91a6b9;
--line: #20384b;
--line-strong: #315069;
--sky-25: #0a1927;
--sky-50: #0d2132;
--sky-100: #12324a;
--sky-200: #1d4c6a;
--navy: #06111d;
}
html[data-theme="dark"] body {
color: var(--ink);
background: #081522;
}
html[data-theme="dark"] .theme-select {
color: #b4c8d8;
border-color: #29465d;
background: #0e2232;
}
html[data-theme="dark"] .site-header {
border-color: var(--line);
}
html[data-theme="dark"] .wordmark {
color: #e1eff9;
}
html[data-theme="dark"] .site-nav a,
html[data-theme="dark"] .site-footer nav a {
color: #91a7ba;
}
html[data-theme="dark"] .button-quiet {
color: #b9d1e2;
border-color: #315069;
background: #0d2030;
}
html[data-theme="dark"] .button-quiet:hover {
color: #72c8f7;
background: #102a3e;
}
html[data-theme="dark"] .hero::before {
background: radial-gradient(circle, rgb(30 102 148 / 35%), transparent 67%);
}
html[data-theme="dark"] .hero h1 {
color: #edf7ff;
}
html[data-theme="dark"] .hero-note {
color: #718ba0;
}
html[data-theme="dark"] .hero-demo {
border-color: #2a4a61;
background: #0a1927;
box-shadow: 0 30px 70px rgb(0 0 0 / 28%);
}
html[data-theme="dark"] .demo-toolbar {
color: #819bb0;
border-color: #284257;
background: #0d2131;
}
html[data-theme="dark"] .hero-demo pre {
color: #b6cad9;
background: #091827;
}
html[data-theme="dark"] .trust-strip {
background: #0a1927;
}
html[data-theme="dark"] .trust-strip strong {
color: #d7e8f5;
}
html[data-theme="dark"] .trust-strip span {
color: #8298ab;
}
html[data-theme="dark"] .gear-card {
border-color: #20394c;
background: #0b1b29;
box-shadow: 0 8px 30px rgb(0 0 0 / 16%);
}
html[data-theme="dark"] .gear-card-featured {
border-color: #285876;
background: linear-gradient(145deg, #0e2940, #0b1b29);
}
html[data-theme="dark"] .gear-number {
color: #52728a;
}
html[data-theme="dark"] .gear-icon {
border-color: #285675;
background: #0e2a40;
}
html[data-theme="dark"] .gear-card h3 {
color: #d9eaf6;
}
html[data-theme="dark"] .gear-card p {
color: #91a5b6;
}
html[data-theme="dark"] .graph-icon::before,
html[data-theme="dark"] .graph-icon::after {
background: #0e2a40;
}
html[data-theme="dark"] .steps h3 {
color: #d5e6f3;
}
html[data-theme="dark"] .steps p {
color: #8fa4b6;
}
html[data-theme="dark"] .steps code {
color: #6fc3f1;
border-color: #29465c;
background: #0c2030;
}
html[data-theme="dark"] .step-index {
border-color: #285b7a;
background: #0e2a40;
}
html[data-theme="dark"] .site-footer p {
color: #738da1;
}
html[data-theme="dark"] .docs-body {
background: #081522;
}
html[data-theme="dark"] .docs-header {
border-color: #20384b;
background: rgb(8 21 34 / 94%);
}
html[data-theme="dark"] .docs-product {
color: #8097aa;
border-color: #294257;
}
html[data-theme="dark"] .docs-search {
color: #7891a5;
border-color: #29465b;
background: #0d2030;
}
html[data-theme="dark"] .docs-search input {
color: #d6e7f4;
}
html[data-theme="dark"] .docs-search kbd {
color: #8299ab;
border-color: #304c61;
background: #102536;
}
html[data-theme="dark"] .docs-sidebar {
border-color: #20384b;
}
html[data-theme="dark"] .sidebar-group > strong,
html[data-theme="dark"] .docs-toc > strong {
color: #708ba0;
}
html[data-theme="dark"] .sidebar-group a {
color: #91a5b7;
}
html[data-theme="dark"] .sidebar-group a:hover {
color: #69c4f5;
background: #0d2436;
}
html[data-theme="dark"] .sidebar-group a.active {
color: #70cbfb;
background: #10304a;
}
html[data-theme="dark"] .sidebar-playground {
color: #8bd4fa;
border-color: #285572;
background: #0d2940;
}
html[data-theme="dark"] .sidebar-playground small {
color: #7697ad;
}
html[data-theme="dark"] .docs-main section + section {
border-color: #20384b;
}
html[data-theme="dark"] .docs-main h1,
html[data-theme="dark"] .docs-main h2,
html[data-theme="dark"] .docs-main h3 {
color: #e0edf7;
}
html[data-theme="dark"] .docs-main p,
html[data-theme="dark"] .docs-main li {
color: #9aafc0;
}
html[data-theme="dark"] .docs-main strong {
color: #c4d8e7;
}
html[data-theme="dark"] .doc-lede {
color: #a9bdcc !important;
}
html[data-theme="dark"] .docs-main :not(pre) > code {
color: #71c6f3;
border-color: #28506a;
background: #0d293d;
}
html[data-theme="dark"] .section-kicker span {
border-color: #285a78;
background: #0e2b42;
}
html[data-theme="dark"] .callout {
border-color: #294052;
background: #0c1c2a;
}
html[data-theme="dark"] .callout-blue {
border-color: #285672;
background: #0d293e;
}
html[data-theme="dark"] .explain-grid > div {
border-color: #263f52;
background: #0b1c2a;
}
html[data-theme="dark"] .inline-play {
color: #8bd5fb;
border-color: #285974;
background: #0e2c43;
}
html[data-theme="dark"] .inline-play strong {
color: #a2defb;
}
html[data-theme="dark"] .inline-play small {
color: #789bb1;
}
html[data-theme="dark"] .mental-model > div {
border-color: #284356;
background: #0b1d2b;
}
html[data-theme="dark"] .mental-model strong {
color: #b9d0e0;
}
html[data-theme="dark"] .mental-model .mental-shared {
border-color: #2d6585;
background: #0e2c43;
}
html[data-theme="dark"] .deploy-list li {
border-color: #20384b;
}
html[data-theme="dark"] .docs-toc a {
color: #768ea1;
border-color: #294257;
}
html[data-theme="dark"] .playground-app {
background: #07131f;
}
html[data-theme="dark"] .playground-header {
border-color: #21384b;
background: #0b1b29;
}
html[data-theme="dark"] .playground-brand {
color: #dcebf6;
}
html[data-theme="dark"] .header-divider {
background: #294154;
}
html[data-theme="dark"] .project-name input {
color: #c3d6e4;
}
html[data-theme="dark"] .project-name input:hover {
background: #102536;
}
html[data-theme="dark"] .project-name input:focus {
border-color: #2d7198;
background: #0e2638;
}
html[data-theme="dark"] #revision-label {
color: #8298aa;
border-color: #294356;
background: #0e2131;
}
html[data-theme="dark"] .playground-nav a {
color: #8da3b5;
}
html[data-theme="dark"] .tool-button {
color: #a7bdcd;
border-color: #29465c;
background: #0e2232;
}
html[data-theme="dark"] .tool-button:hover {
color: #67c4f4;
border-color: #326484;
background: #102a3d;
}
html[data-theme="dark"] .playground-tools {
border-color: #233b4e;
background: #0c1d2b;
}
html[data-theme="dark"] .tool-group button {
color: #8fa6b8;
}
html[data-theme="dark"] .tool-group button:hover,
html[data-theme="dark"] .tool-group button[aria-pressed="true"] {
color: #63c3f5;
border-color: #2b4d64;
background: #102536;
}
html[data-theme="dark"] .tool-context,
html[data-theme="dark"] .stage-meta,
html[data-theme="dark"] .editor-status {
color: #aec1cf;
}
html[data-theme="dark"] .context-divider {
background: #294154;
}
html[data-theme="dark"] .snippets-dialog {
color: #d3e3ee;
border-color: #2a4a60;
background: #0b1b29;
}
html[data-theme="dark"] .dialog-heading p {
color: #8399aa;
}
html[data-theme="dark"] .snippet-search {
color: #7890a3;
border-color: #29465b;
background: #0d2131;
}
html[data-theme="dark"] .snippet-search input {
color: #c9dae7;
}
html[data-theme="dark"] .snippet-grid > button:hover {
border-color: #2d5772;
background: #0e293d;
}
html[data-theme="dark"] .snippet-icon {
border-color: #2a5a77;
background: #0e2c43;
}
html[data-theme="dark"] .snippet-grid strong {
color: #c9dce9;
}
html[data-theme="dark"] .snippet-grid small {
color: #8298aa;
}
html[data-theme="dark"] .snippet-grid code {
color: #6bc4f2;
border-color: #29475d;
background: #0d2131;
}
html[data-theme="light"] .workbench {
background: #d7e4ed;
}
html[data-theme="light"] .pane-tabs,
html[data-theme="light"] .stage-toolbar {
border-color: #cfdee8;
background: #edf4f8;
}
html[data-theme="light"] .pane-tab {
color: #60778a;
border-color: #d5e2ea;
background: #e8f0f5;
}
html[data-theme="light"] .pane-tab.active {
color: #29465e;
background: #f8fbfd;
}
html[data-theme="light"] .editor {
background: #fff;
}
html[data-theme="light"] .monaco-editor .margin {
background-color: #fff !important;
}
html[data-theme="light"] .editor-status {
color: #718698;
border-color: #d2e0e9;
background: #eff5f8;
}
html[data-theme="light"] .resize-handle {
background: #c8d8e3;
}
html[data-theme="light"] .resize-handle::after {
background: #8fa9bb;
}
html[data-theme="light"] .stage-pane {
background: #e7f0f5;
}
html[data-theme="light"] .stage-tabs button {
color: #60778a;
}
html[data-theme="light"] .stage-tabs button.active {
color: #284961;
background: #f8fbfd;
}
html[data-theme="light"] .stage-meta {
color: #6a8194;
}
html[data-theme="light"] .stage-divider {
background: #c5d7e2;
}
html[data-theme="light"] .inspector {
border-color: #cddde7;
background: #f7fafc;
}
html[data-theme="light"] .inspector-tabs {
border-color: #cfdee7;
background: #edf4f8;
}
html[data-theme="light"] .inspector-tabs button {
color: #687f91;
}
html[data-theme="light"] .inspector-tabs button.active {
color: #287aa9;
}
html[data-theme="light"] .inspector-tabs button span {
background: #dce9f1;
}
html[data-theme="light"] .console-empty,
html[data-theme="light"] .profile-empty {
color: #6f8799;
}
html[data-theme="light"] #console-lines {
color: #526c80;
}
html[data-theme="light"] #console-lines li {
border-color: rgb(190 207 218 / 60%);
}
html[data-theme="light"] .profile-summary > div {
border-color: #d3e1e9;
background: #eef5f8;
}
html[data-theme="light"] .profile-summary strong {
color: #3c6e8d;
}
html[data-theme="light"] .profile-results th,
html[data-theme="light"] .profile-results td {
border-color: #d5e2e9;
}
@media (max-width: 700px) {
.site-header .theme-select {
width: 64px;
}
}
+42
View File
@@ -0,0 +1,42 @@
(() => {
const key = "yawn:theme";
const preference = matchMedia("(prefers-color-scheme: dark)");
const saved = localStorage.getItem(key);
let mode = ["auto", "light", "dark"].includes(saved) ? saved : "auto";
const apply = () => {
const resolved =
mode === "auto" ? (preference.matches ? "dark" : "light") : mode;
document.documentElement.dataset.theme = resolved;
document.documentElement.dataset.themeMode = mode;
document.documentElement.style.colorScheme = resolved;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", resolved === "dark" ? "#081522" : "#f7fbff");
for (const select of document.querySelectorAll("[data-theme-select]"))
select.value = mode;
dispatchEvent(
new CustomEvent("yawn-theme-change", { detail: { mode, resolved } }),
);
};
const bind = () => {
for (const select of document.querySelectorAll("[data-theme-select]")) {
select.value = mode;
select.addEventListener("change", () => {
mode = select.value;
if (mode === "auto") localStorage.removeItem(key);
else localStorage.setItem(key, mode);
apply();
});
}
};
preference.addEventListener("change", () => {
if (mode === "auto") apply();
});
apply();
if (document.readyState === "loading")
addEventListener("DOMContentLoaded", bind, { once: true });
else bind();
})();
+188 -25
View File
@@ -30,10 +30,25 @@ declare module "@yawn/core" {
passes: Array<{ name: string; milliseconds: number }>;
}
export class YawnCore {
constructor(canvas: HTMLCanvasElement, options?: { arenaBytes?: number; debug?: boolean });
constructor(
canvas: HTMLCanvasElement,
options?: { arenaBytes?: number; debug?: boolean },
);
readonly ready: Promise<void>;
createRows(options: { name: string; rows: number; stride: number; format: RowFormat }): Promise<SharedRows>;
createRowsBatch(rows: Array<{ name: string; rows: number; stride: number; format: RowFormat }>): Promise<SharedRows[]>;
createRows(options: {
name: string;
rows: number;
stride: number;
format: RowFormat;
}): Promise<SharedRows>;
createRowsBatch(
rows: Array<{
name: string;
rows: number;
stride: number;
format: RowFormat;
}>,
): Promise<SharedRows[]>;
deleteRows(name: string): Promise<void>;
allocateObject(name: string): Promise<number>;
deleteObject(name: string, id: number): Promise<void>;
@@ -52,23 +67,44 @@ declare module "@yawn/core" {
declare module "@yawn/handles" {
import { SharedRows, YawnCore } from "@yawn/core";
export interface SceneOptions { arenaBytes?: number; debug?: boolean; fps?: number; hdr?: boolean }
export interface SceneOptions {
arenaBytes?: number;
debug?: boolean;
fps?: number;
hdr?: boolean;
}
export class Scene {
constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
readonly core: YawnCore;
readonly ready: Promise<this>;
readonly hdr: boolean;
array(name: string): SharedRows;
ensureRows(name: string, rows: number, stride: number, format: "f32" | "u32" | "i32"): Promise<SharedRows>;
ensureRows(
name: string,
rows: number,
stride: number,
format: "f32" | "u32" | "i32",
): Promise<SharedRows>;
reserve(additional: { nodes?: number; materials?: number }): Promise<void>;
batchWrites<T>(operation: () => T): T;
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
dispose(): void;
}
export interface Vector3View { x: number; y: number; z: number; set(values: ArrayLike<number>): void }
export interface RotorView extends Vector3View { w: number }
export interface NodeOptions { position?: ArrayLike<number>; rotor?: ArrayLike<number>; scale?: ArrayLike<number> }
export interface Vector3View {
x: number;
y: number;
z: number;
set(values: ArrayLike<number>): void;
}
export interface RotorView extends Vector3View {
w: number;
}
export interface NodeOptions {
position?: ArrayLike<number>;
rotor?: ArrayLike<number>;
scale?: ArrayLike<number>;
}
export class Node {
constructor(scene: Scene, options?: NodeOptions);
readonly scene: Scene;
@@ -103,7 +139,13 @@ declare module "@yawn/handles" {
dispose(): Promise<void>;
}
export type VertexKind = "positions" | "normals" | "tangents" | "uvs" | "colors" | "indices";
export type VertexKind =
| "positions"
| "normals"
| "tangents"
| "uvs"
| "colors"
| "indices";
export interface MeshOptions extends NodeOptions {
geometryId?: number;
material?: PBRMaterial;
@@ -119,10 +161,55 @@ declare module "@yawn/handles" {
material: PBRMaterial;
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
setVertexData(kind: VertexKind, data: ArrayLike<number>): Promise<this>;
setMaterialForFaces(material: PBRMaterial, faces: number | number[]): Promise<this>;
setMaterialForFaces(
material: PBRMaterial,
faces: number | number[],
): Promise<this>;
}
export interface CameraOptions extends NodeOptions { fov?: number; near?: number; far?: number; aspect?: number }
export interface CubeOptions
extends Omit<MeshOptions, "geometryId" | "vertexData"> {
size?: number;
width?: number;
height?: number;
depth?: number;
}
export class Cube extends Mesh {
constructor(scene: Scene, options?: CubeOptions);
}
export interface IcosphereOptions
extends Omit<MeshOptions, "geometryId" | "vertexData"> {
radius?: number;
subdivisions?: number;
}
export class Icosphere extends Mesh {
constructor(scene: Scene, options?: IcosphereOptions);
}
export interface PlaneOptions
extends Omit<MeshOptions, "geometryId" | "vertexData"> {
size?: number;
width?: number;
depth?: number;
}
export class Plane extends Mesh {
constructor(scene: Scene, options?: PlaneOptions);
}
export interface SphereOptions
extends Omit<MeshOptions, "geometryId" | "vertexData"> {
radius?: number;
segments?: number;
rings?: number;
}
export class Sphere extends Mesh {
constructor(scene: Scene, options?: SphereOptions);
}
export interface CameraOptions extends NodeOptions {
fov?: number;
near?: number;
far?: number;
aspect?: number;
}
export class Camera extends Node {
fov: number;
near: number;
@@ -130,30 +217,106 @@ declare module "@yawn/handles" {
aspect: number;
}
export class ArcRotateCamera extends Camera {
constructor(scene: Scene, options?: CameraOptions & { target?: Node; alpha?: number; beta?: number; radius?: number; controls?: { element: HTMLElement; pointer?: boolean; controller?: boolean } });
constructor(
scene: Scene,
options?: CameraOptions & {
target?: Node;
alpha?: number;
beta?: number;
radius?: number;
controls?: {
element: HTMLElement;
pointer?: boolean;
controller?: boolean;
};
},
);
alpha: number;
beta: number;
radius: number;
detachControls(): this;
}
export class FreeCamera extends Camera {
constructor(scene: Scene, options?: CameraOptions & { controls?: { element: HTMLElement; keyboard?: boolean; pointer?: boolean; controller?: boolean; speed?: number } });
constructor(
scene: Scene,
options?: CameraOptions & {
controls?: {
element: HTMLElement;
keyboard?: boolean;
pointer?: boolean;
controller?: boolean;
speed?: number;
};
},
);
}
export class FollowCamera extends Camera {
constructor(scene: Scene, options?: CameraOptions & { target?: Node; distance?: number; height?: number; smoothing?: number });
constructor(
scene: Scene,
options?: CameraOptions & {
target?: Node;
distance?: number;
height?: number;
smoothing?: number;
},
);
}
export interface LightOptions extends NodeOptions { color?: ArrayLike<number>; intensity?: number }
export class PointLight extends Node { constructor(scene: Scene, options?: LightOptions & { range?: number }); intensity: number; range: number }
export class AmbientLight extends Node { constructor(scene: Scene, options?: LightOptions); intensity: number }
export class DirectionalLight extends Node { constructor(scene: Scene, options?: LightOptions); intensity: number }
export class SpotLight extends Node { constructor(scene: Scene, options?: LightOptions & { range?: number; innerAngle?: number; outerAngle?: number }) }
export class RectAreaLight extends Node { constructor(scene: Scene, options?: LightOptions & { width?: number; height?: number }) }
export interface LightOptions extends NodeOptions {
color?: ArrayLike<number>;
intensity?: number;
}
export class PointLight extends Node {
constructor(scene: Scene, options?: LightOptions & { range?: number });
intensity: number;
range: number;
}
export class AmbientLight extends Node {
constructor(scene: Scene, options?: LightOptions);
intensity: number;
}
export class DirectionalLight extends Node {
constructor(scene: Scene, options?: LightOptions);
intensity: number;
}
export class SpotLight extends Node {
constructor(
scene: Scene,
options?: LightOptions & {
range?: number;
innerAngle?: number;
outerAngle?: number;
},
);
}
export class RectAreaLight extends Node {
constructor(
scene: Scene,
options?: LightOptions & { width?: number; height?: number },
);
}
export class ComputePass { constructor(options: Record<string, unknown>); readonly id: string }
export class FXAA { constructor(scene: Scene, options?: Record<string, unknown>); readonly ready: Promise<this>; dispose(): Promise<void> }
export class ColorGrading extends FXAA { constructor(scene: Scene, options?: { toneMap?: "aces" | "reinhard" | "linear"; amount?: number }) }
export class DynamicExposure extends FXAA { constructor(scene: Scene, options?: { exposure?: number }) }
export class SSAO extends FXAA { constructor(scene: Scene, options?: { amount?: number }) }
export class ComputePass {
constructor(options: Record<string, unknown>);
readonly id: string;
}
export class FXAA {
constructor(scene: Scene, options?: Record<string, unknown>);
readonly ready: Promise<this>;
dispose(): Promise<void>;
}
export class ColorGrading extends FXAA {
constructor(
scene: Scene,
options?: { toneMap?: "aces" | "reinhard" | "linear"; amount?: number },
);
}
export class DynamicExposure extends FXAA {
constructor(scene: Scene, options?: { exposure?: number });
}
export class SSAO extends FXAA {
constructor(scene: Scene, options?: { amount?: number });
}
export function importGltf(scene: Scene, url: string | URL): Promise<Mesh[]>;
}
+238 -68
View File
@@ -4,26 +4,43 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
<meta name="description" content="Learn Yawn from your first WebGPU scene through shared memory and custom render graphs." />
<meta
name="description"
content="Learn Yawn from your first WebGPU scene through shared memory and custom render graphs."
/>
<title>Learn Yawn — Documentation</title>
<script src="/assets/theme.js"></script>
<link rel="stylesheet" href="/assets/site.css" />
<link rel="stylesheet" href="/assets/docs.css" />
<link rel="stylesheet" href="/assets/theme.css" />
<script type="module" src="/assets/docs.js"></script>
</head>
<body class="docs-body">
<header class="site-header docs-header">
<a class="wordmark" href="/" aria-label="Yawn home">
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span><span>Yawn</span>
<span class="wordmark-mark" aria-hidden="true"
><i></i><i></i><i></i></span
><span>Yawn</span>
</a>
<span class="docs-product">Docs</span>
<label class="docs-search">
<span aria-hidden="true">⌕</span>
<input type="search" placeholder="Filter this guide" aria-label="Filter documentation sections" />
<input
type="search"
placeholder="Filter this guide"
aria-label="Filter documentation sections"
/>
<kbd>/</kbd>
</label>
<nav class="site-nav" aria-label="Documentation navigation">
<a href="/playground">Playground</a><a href="https://git.heaust.org/heaust/yawn">Source</a>
<a href="/playground">Playground</a
><a href="https://git.heaust.org/heaust/yawn">Source</a>
</nav>
<select class="theme-select" data-theme-select aria-label="Color theme">
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</header>
<div class="docs-layout">
@@ -54,7 +71,12 @@
<a href="#custom-rows">Craft your own API</a>
<a href="#deployment">Deployment</a>
</div>
<a class="sidebar-playground" href="/playground"><span>▶</span><div><strong>Try as you learn</strong><small>Open the playground</small></div></a>
<a class="sidebar-playground" href="/playground"
><span>▶</span>
<div>
<strong>Try as you learn</strong><small>Open the playground</small>
</div></a
>
</aside>
<main class="docs-main">
@@ -63,13 +85,20 @@
<div class="doc-eyebrow">Start here · 5 minute read</div>
<h1>Build your first Yawn scene</h1>
<p class="doc-lede">
This guide starts from an empty HTML file and ends with a WebGPU triangle you can
move, shade, and inspect. You do not need engine experience. Every new term is
explained where it first appears.
This guide starts from an empty HTML file and ends with a WebGPU
triangle you can move, shade, and inspect. You do not need engine
experience. Every new term is explained where it first appears.
</p>
<div class="callout callout-blue">
<span class="callout-icon">i</span>
<div><strong>New to programming?</strong><p>Type the examples exactly as shown. Words in <code>code style</code> are names the computer expects; the prose around them explains why.</p></div>
<div>
<strong>New to programming?</strong>
<p>
Type the examples exactly as shown. Words in
<code>code style</code> are names the computer expects; the
prose around them explains why.
</p>
</div>
</div>
</section>
@@ -77,34 +106,55 @@
<div class="section-kicker"><span>01</span> Installation</div>
<h2>Load Handles from Yawn’s CDN</h2>
<p>
You do not build or host Yawn yourself. An <em>import map</em> gives the CDN module a
short name that the rest of this tutorial can use. Put this inside your page’s
You do not build or host Yawn yourself. An
<em>import map</em> gives the CDN module a short name that the
rest of this tutorial can use. Put this inside your page’s
<code>&lt;head&gt;</code>:
</p>
<div class="code-block" data-language="html">
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>index.html</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>&lt;script type="importmap"&gt;
{
"imports": {
"@yawn/core": "https://yawn.heaust.org/pkg/core.js",
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
}
}
&lt;/script&gt;</code></pre>
</div>
<p>
That URL serves a ready-to-use ES module. Its render worker, import worker, picking
worker, and WebAssembly module all continue loading from Yawn’s CDN automatically.
Core and Handles stay separate: Handles uses the mapped Core
module instead of containing its own copy. Their render worker,
import worker, picking worker, and WebAssembly module all continue
loading from Yawn’s CDN automatically.
</p>
<h3>Make a canvas</h3>
<p>A <code>canvas</code> is the rectangle where WebGPU will draw. Add one to the page body:</p>
<p>
A <code>canvas</code> is the rectangle where WebGPU will draw. Add
one to the page body:
</p>
<div class="code-block" data-language="html">
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>index.html</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>&lt;canvas id="view" width="1280" height="720"&gt;&lt;/canvas&gt;
&lt;script type="module" src="/app.js"&gt;&lt;/script&gt;</code></pre>
</div>
<div class="callout">
<span class="callout-icon">!</span>
<div><strong>Your page needs isolation headers</strong><p>Your own web host must send <code>COOP: same-origin</code> and <code>COEP: require-corp</code> so SharedArrayBuffer is available. The CDN already sends the matching CORS and resource-policy headers for Yawn’s files.</p></div>
<div>
<strong>Your page needs isolation headers</strong>
<p>
Your own web host must send <code>COOP: same-origin</code> and
<code>COEP: require-corp</code> so SharedArrayBuffer is
available. The CDN already sends the matching CORS and
resource-policy headers for Yawn’s files.
</p>
</div>
</div>
</section>
@@ -113,12 +163,16 @@
<h2>Scene → material → mesh</h2>
<p>
A <strong>scene</strong> owns the shared data and render graph. A
<strong>material</strong> describes the surface. A <strong>mesh</strong> supplies points
and tells Yawn which order to connect them.
<strong>material</strong> describes the surface. A
<strong>mesh</strong> supplies points and tells Yawn which order
to connect them.
</p>
<div class="code-block code-block-large" data-language="typescript">
<div class="code-title"><span>app.js</span><button type="button" data-copy>Copy</button></div>
<pre><code>import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
<div class="code-title">
<span>app.js</span><button type="button" data-copy>Copy</button>
</div>
<pre><code>import "@yawn/core";
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
const canvas = document.querySelector("#view");
const scene = new Scene(canvas, { hdr: true, fps: 60 });
@@ -141,23 +195,48 @@ const triangle = new Mesh(scene, {
await triangle.ready;</code></pre>
</div>
<div class="explain-grid">
<div><code>new</code><p>Creates one object. Here that is a scene, material, or mesh handle.</p></div>
<div><code>await …ready</code><p>Waits for setup to finish before the next object depends on it.</p></div>
<div><code>[x, y, z]</code><p>One point in 3D space: horizontal, vertical, and depth.</p></div>
<div>
<code>new</code>
<p>
Creates one object. Here that is a scene, material, or mesh
handle.
</p>
</div>
<div>
<code>await …ready</code>
<p>
Waits for setup to finish before the next object depends on
it.
</p>
</div>
<div>
<code>[x, y, z]</code>
<p>One point in 3D space: horizontal, vertical, and depth.</p>
</div>
</div>
<a class="inline-play" href="/playground"><span>▶</span><div><strong>Run this example</strong><small>It is already loaded in the playground</small></div><b>Open →</b></a>
<a class="inline-play" href="/playground"
><span>▶</span>
<div>
<strong>Run this example</strong
><small>It is already loaded in the playground</small>
</div>
<b>Open →</b></a
>
</section>
<section id="camera" data-title="Add a camera">
<div class="section-kicker"><span>03</span> Move around</div>
<h2>Add a camera you can orbit</h2>
<p>
The starter triangle is already in clip space, so it is visible without a camera.
For a 3D world, add an <code>ArcRotateCamera</code>. Drag to orbit and use the wheel to
The starter triangle is already in clip space, so it is visible
without a camera. For a 3D world, add an
<code>ArcRotateCamera</code>. Drag to orbit and use the wheel to
zoom.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>app.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>app.ts</span><button type="button" data-copy>Copy</button>
</div>
<pre><code>import { ArcRotateCamera } from "@yawn/handles";
const camera = new ArcRotateCamera(scene, {
@@ -175,12 +254,16 @@ await camera.ready;</code></pre>
<div class="section-kicker"><span>04</span> Build a world</div>
<h2>Clone geometry, not work</h2>
<p>
Every <code>Mesh</code> is an instance. Calling <code>clone()</code> shares its geometry
and creates only a new transform and mesh slot. If one clone later changes its vertex
Every <code>Mesh</code> is an instance. Calling
<code>clone()</code> shares its geometry and creates only a new
transform and mesh slot. If one clone later changes its vertex
data, Yawn makes that geometry unique automatically.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>instances.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>instances.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>for (let x = -4; x &lt;= 4; x++) {
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
await copy.ready;
@@ -191,11 +274,14 @@ await camera.ready;</code></pre>
<section id="materials" data-title="Materials and lights">
<h2>Materials and lights are ordinary handles</h2>
<p>
Change a material after it is ready and Yawn writes the new value directly into its
shared row. Lights use the same pattern.
Change a material after it is ready and Yawn writes the new value
directly into its shared row. Lights use the same pattern.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>lighting.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>lighting.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>import { AmbientLight, PointLight } from "@yawn/handles";
const key = new PointLight(scene, {
@@ -217,11 +303,15 @@ sky.roughness = 0.5; // one shared-memory write</code></pre>
<section id="models" data-title="Import a glTF model">
<h2>Bring in a glTF model</h2>
<p>
<code>importGltf</code> fetches and parses <code>.gltf</code> or <code>.glb</code> data in
a worker, then creates ordinary Yawn meshes and materials.
<code>importGltf</code> fetches and parses <code>.gltf</code> or
<code>.glb</code> data in a worker, then creates ordinary Yawn
meshes and materials.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>model.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>model.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>import { importGltf } from "@yawn/handles";
const meshes = await importGltf(scene, "/models/robot.glb");
@@ -231,9 +321,15 @@ meshes[0].position.y = 0.5;</code></pre>
<section id="post-processing" data-title="Post processing">
<h2>Add effects without leaving the scene API</h2>
<p>Effect handles add passes to the same render graph. Batch related additions to rebuild that graph once.</p>
<p>
Effect handles add passes to the same render graph. Batch related
additions to rebuild that graph once.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>effects.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>effects.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>import { ColorGrading, FXAA } from "@yawn/handles";
await scene.batchGraphUpdates(async () =&gt; {
@@ -248,27 +344,40 @@ await scene.batchGraphUpdates(async () =&gt; {
<div class="section-kicker"><span>05</span> Understand Yawn</div>
<h2>Setup is messages. Motion is memory.</h2>
<p>
Expensive structural changes—creating rows, allocating IDs, or replacing a render
graph—go to the render worker as messages. Values that already exist—positions,
colors, camera matrices, light strengths—change in shared memory.
Expensive structural changes—creating rows, allocating IDs, or
replacing a render graph—go to the render worker as messages.
Values that already exist—positions, colors, camera matrices,
light strengths—change in shared memory.
</p>
<div class="mental-model">
<div><strong>Infrequent control</strong><span>create · allocate · compile · switch</span></div>
<div>
<strong>Infrequent control</strong
><span>create · allocate · compile · switch</span>
</div>
<i>→</i>
<div class="mental-shared"><strong>Shared rows</strong><span>transform · shade · animate</span></div>
<div class="mental-shared">
<strong>Shared rows</strong
><span>transform · shade · animate</span>
</div>
<i>→</i>
<div><strong>Rust/WASM core</strong><span>schedule · upload · render</span></div>
<div>
<strong>Rust/WASM core</strong
><span>schedule · upload · render</span>
</div>
</div>
</section>
<section id="shared-memory" data-title="Shared memory">
<h2>Write your own hot data</h2>
<p>
Handles are views over named rows. Your application can add rows too. Each row is
aligned for predictable CPU and GPU use.
Handles are views over named rows. Your application can add rows
too. Each row is aligned for predictable CPU and GPU use.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>simulation.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>simulation.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>const velocity = await scene.ensureRows(
"app.velocity", 10_000, 16, "f32"
);
@@ -276,31 +385,48 @@ await scene.batchGraphUpdates(async () =&gt; {
velocity.row(42).set([1, 0, 0, 0]);</code></pre>
</div>
<p>
That final line changes four floats without cloning an object or posting a worker
message. The scene marks shared data dirty so the next frame sees it.
That final line changes four floats without cloning an object or
posting a worker message. The scene marks shared data dirty so the
next frame sees it.
</p>
</section>
<section id="profiler" data-title="GPU profiler">
<h2>Measure the passes the GPU actually ran</h2>
<p>
Open <strong>Profile</strong> in the playground to enable timestamp queries. Yawn
reports the physical pass names and GPU milliseconds without serializing the render
queue. Support depends on the browser and adapter.
Open <strong>Profile</strong> in the playground to enable
timestamp queries. Yawn reports the physical pass names and GPU
milliseconds without serializing the render queue. Support depends
on the browser and adapter.
</p>
<div class="callout callout-blue"><span class="callout-icon">⌁</span><div><strong>Profile a real scene</strong><p>The playground’s profiler uses <code>core.onProfile()</code> and <code>core.setProfiler(true)</code>—the same public APIs available to your app.</p></div></div>
<div class="callout callout-blue">
<span class="callout-icon">⌁</span>
<div>
<strong>Profile a real scene</strong>
<p>
The playground’s profiler uses
<code>core.onProfile()</code> and
<code>core.setProfiler(true)</code>—the same public APIs
available to your app.
</p>
</div>
</div>
</section>
<section id="core-deep-dive" data-title="Using core directly">
<div class="section-kicker"><span>06</span> Deep dive</div>
<h2>Outgrow handles without outgrowing Yawn</h2>
<p>
<code>@yawn/core</code> has no scene, mesh, material, camera, or built-in shader. It
owns a shared arena and a render-graph runtime. Handles are one replaceable frontend
that builds on those two primitives.
<code>@yawn/core</code> has no scene, mesh, material, camera, or
built-in shader. It owns a shared arena and a render-graph
runtime. Handles are one replaceable frontend that builds on those
two primitives.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>core.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>core.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>import { YawnCore } from "https://yawn.heaust.org/pkg/core.js";
const core = new YawnCore(canvas, {
@@ -316,20 +442,25 @@ const particles = await core.createRows({
});</code></pre>
</div>
<p>
From here, your frontend supplies WGSL, resources, pipelines, and a pass DAG. Core
compiles that description into an up-front loadout, aliases compatible transient
textures, and records render work.
From here, your frontend supplies WGSL, resources, pipelines, and
a pass DAG. Core compiles that description into an up-front
loadout, aliases compatible transient textures, and records render
work.
</p>
</section>
<section id="custom-rows" data-title="Craft your own API">
<h2>Craft an API for your problem</h2>
<p>
A custom handle can be as small as an ID plus getters and setters into shared rows.
Keep domain policy in your code and send only structural changes to core.
A custom handle can be as small as an ID plus getters and setters
into shared rows. Keep domain policy in your code and send only
structural changes to core.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>Particle.ts</span><button type="button" data-copy>Copy</button></div>
<div class="code-title">
<span>Particle.ts</span
><button type="button" data-copy>Copy</button>
</div>
<pre><code>class Particle {
constructor(
readonly id: number,
@@ -346,11 +477,50 @@ const particles = await core.createRows({
<section id="deployment" data-title="Deployment">
<h2>Deployment checklist</h2>
<ul class="deploy-list">
<li><span>1</span><div><strong>Serve over HTTPS</strong><p>WebGPU and cross-origin isolation require a secure browser context outside local development.</p></div></li>
<li><span>2</span><div><strong>Keep the isolation headers</strong><p>Send <code>Cross-Origin-Opener-Policy: same-origin</code> and <code>Cross-Origin-Embedder-Policy: require-corp</code>.</p></div></li>
<li><span>3</span><div><strong>Allow Yawn’s CDN</strong><p>The imported module keeps every worker and WASM request on <code>yawn.heaust.org</code>. If you use a Content Security Policy, allow that origin and <code>blob:</code> workers.</p></div></li>
<li>
<span>1</span>
<div>
<strong>Serve over HTTPS</strong>
<p>
WebGPU and cross-origin isolation require a secure browser
context outside local development.
</p>
</div>
</li>
<li>
<span>2</span>
<div>
<strong>Keep the isolation headers</strong>
<p>
Send
<code>Cross-Origin-Opener-Policy: same-origin</code> and
<code>Cross-Origin-Embedder-Policy: require-corp</code>.
</p>
</div>
</li>
<li>
<span>3</span>
<div>
<strong>Allow Yawn’s CDN</strong>
<p>
The imported module keeps every worker and WASM request on
<code>yawn.heaust.org</code>. If you use a Content Security
Policy, allow that origin and <code>blob:</code> workers.
</p>
</div>
</li>
</ul>
<div class="next-card"><div><small>Next step</small><h3>Make the starter scene yours.</h3><p>Open the playground, choose a snippet, and save a revision you can share.</p></div><a class="button" href="/playground">Open playground →</a></div>
<div class="next-card">
<div>
<small>Next step</small>
<h3>Make the starter scene yours.</h3>
<p>
Open the playground, choose a snippet, and save a revision you
can share.
</p>
</div>
<a class="button" href="/playground">Open playground →</a>
</div>
</section>
</article>
</main>
+104 -37
View File
@@ -9,13 +9,17 @@
content="Yawn is a WebGPU engine that starts with friendly scene handles and scales down to a programmable Rust/WASM render core."
/>
<title>Yawn — a WebGPU engine that grows with you</title>
<script src="/assets/theme.js"></script>
<link rel="stylesheet" href="/assets/site.css" />
<link rel="stylesheet" href="/assets/theme.css" />
<script type="module" src="/assets/site.js"></script>
</head>
<body>
<header class="site-header">
<a class="wordmark" href="/" aria-label="Yawn home">
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span>
<span class="wordmark-mark" aria-hidden="true"
><i></i><i></i><i></i
></span>
<span>Yawn</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
@@ -23,6 +27,11 @@
<a href="/playground">Playground</a>
<a href="https://git.heaust.org/heaust/yawn">Source</a>
</nav>
<select class="theme-select" data-theme-select aria-label="Color theme">
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<a class="button button-small" href="/docs#first-scene">Get started</a>
</header>
@@ -32,17 +41,24 @@
<div class="eyebrow"><span></span> WebGPU without the ceremony</div>
<h1>Start with a scene.<br />Scale into an engine.</h1>
<p class="hero-lede">
Yawn gives you approachable TypeScript handles on day one, direct shared-memory
performance when you need it, and a programmable render core when your ideas outgrow
conventions.
Yawn gives you approachable TypeScript handles on day one, direct
shared-memory performance when you need it, and a programmable
render core when your ideas outgrow conventions.
</p>
<div class="hero-actions">
<a class="button" href="/playground">Open the playground <span>→</span></a>
<a class="button button-quiet" href="/docs#installation">Read the 5-minute setup</a>
<a class="button" href="/playground"
>Open the playground <span>→</span></a
>
<a class="button button-quiet" href="/docs#installation"
>Read the 5-minute setup</a
>
</div>
<div class="hero-note">
<span class="browser-dots" aria-hidden="true"><i></i><i></i><i></i></span>
One CDN import · No framework required · Rust/WASM core
<span class="browser-dots" aria-hidden="true"
><i></i><i></i><i></i
></span>
Separate Core + Handles modules · No framework required · Rust/WASM
core
</div>
</div>
@@ -78,7 +94,9 @@
<section class="trust-strip" aria-label="Yawn principles">
<div><strong>One</strong><span>shared arena</span></div>
<div><strong>Zero</strong><span>messages for hot state</span></div>
<div><strong>Any</strong><span>render graph you can describe</span></div>
<div>
<strong>Any</strong><span>render graph you can describe</span>
</div>
<div><strong>Small</strong><span>Rust/WASM foundation</span></div>
</section>
@@ -87,8 +105,9 @@
<div class="eyebrow"><span></span> Progressive by design</div>
<h2>Use exactly as much engine as you need.</h2>
<p>
You do not have to choose between an easy API and a serious foundation. Yawn keeps
those layers separate, so learning more never means starting over.
You do not have to choose between an easy API and a serious
foundation. Yawn keeps those layers separate, so learning more never
means starting over.
</p>
</div>
<div class="gear-grid">
@@ -97,8 +116,9 @@
<div class="gear-icon"><span class="cube-icon"></span></div>
<h3>Build with handles</h3>
<p>
Start with familiar scenes, meshes, materials, lights, cameras, glTF imports, and
post effects. Constructors are explicit and every async boundary is visible.
Start with familiar scenes, meshes, materials, lights, cameras,
glTF imports, and post effects. Constructors are explicit and
every async boundary is visible.
</p>
<a href="/docs#first-scene">Make your first scene <span>→</span></a>
</article>
@@ -107,18 +127,22 @@
<div class="gear-icon"><span class="rows-icon"></span></div>
<h3>Move through shared data</h3>
<p>
Positions, cameras, lights, materials, and your own application rows live in one
SharedArrayBuffer. Hot updates become direct typed-array writes.
Positions, cameras, lights, materials, and your own application
rows live in one SharedArrayBuffer. Hot updates become direct
typed-array writes.
</p>
<a href="/docs#shared-memory">Understand the fast path <span>→</span></a>
<a href="/docs#shared-memory"
>Understand the fast path <span>→</span></a
>
</article>
<article class="gear-card">
<div class="gear-number">03</div>
<div class="gear-icon"><span class="graph-icon"></span></div>
<h3>Author the whole graph</h3>
<p>
Use core directly when you need custom resources, WGSL pipelines, compute, pass
dependencies, transient aliasing, and up-front GPU loadouts.
Use core directly when you need custom resources, WGSL pipelines,
compute, pass dependencies, transient aliasing, and up-front GPU
loadouts.
</p>
<a href="/docs#core-deep-dive">Go beneath handles <span>→</span></a>
</article>
@@ -128,18 +152,30 @@
<section class="architecture-section">
<div class="section-shell architecture-grid">
<div class="architecture-copy">
<div class="eyebrow eyebrow-light"><span></span> A quiet hot path</div>
<div class="eyebrow eyebrow-light">
<span></span> A quiet hot path
</div>
<h2>Your frame should move data, not negotiate it.</h2>
<p>
Yawn pays setup costs when your scene or graph changes. During play, ordinary updates
stay in shared rows that JavaScript and the render worker can both see.
Yawn pays setup costs when your scene or graph changes. During
play, ordinary updates stay in shared rows that JavaScript and the
render worker can both see.
</p>
<ul class="check-list">
<li><span>✓</span> Structure-of-arrays layout with aligned rows</li>
<li><span>✓</span> Render graphs compiled before they become active</li>
<li><span>✓</span> Optional timestamp profiling for physical GPU passes</li>
<li>
<span>✓</span> Structure-of-arrays layout with aligned rows
</li>
<li>
<span>✓</span> Render graphs compiled before they become active
</li>
<li>
<span>✓</span> Optional timestamp profiling for physical GPU
passes
</li>
</ul>
<a class="text-link-light" href="/docs#mental-model">See how the pieces connect →</a>
<a class="text-link-light" href="/docs#mental-model"
>See how the pieces connect →</a
>
</div>
<div class="architecture-visual" aria-label="Yawn data flow">
<div class="flow-label">Your application</div>
@@ -148,11 +184,18 @@
</div>
<div class="flow-line"><i></i><i></i><i></i></div>
<div class="arena-card">
<div><span></span><strong>SharedArrayBuffer</strong><small>aligned SOA arena</small></div>
<div class="arena-rows"><i></i><i></i><i></i><i></i><i></i><i></i></div>
<div>
<span></span><strong>SharedArrayBuffer</strong
><small>aligned SOA arena</small>
</div>
<div class="arena-rows">
<i></i><i></i><i></i><i></i><i></i><i></i>
</div>
</div>
<div class="flow-line flow-line-bottom"><i></i></div>
<div class="core-card"><span>Rust / WASM</span><strong>Render graph core</strong></div>
<div class="core-card">
<span>Rust / WASM</span><strong>Render graph core</strong>
</div>
<div class="gpu-line"></div>
<div class="gpu-card">WebGPU</div>
</div>
@@ -167,17 +210,30 @@
<ol class="steps">
<li>
<span class="step-index">1</span>
<div><h3>Create a scene</h3><p>Give Yawn a canvas and await one clear readiness promise.</p></div>
<div>
<h3>Create a scene</h3>
<p>Give Yawn a canvas and await one clear readiness promise.</p>
</div>
<code>new Scene(canvas)</code>
</li>
<li>
<span class="step-index">2</span>
<div><h3>Add what you can see</h3><p>Materials and meshes own typed handles, not hidden global state.</p></div>
<div>
<h3>Add what you can see</h3>
<p>
Materials and meshes own typed handles, not hidden global state.
</p>
</div>
<code>new Mesh(scene, options)</code>
</li>
<li>
<span class="step-index">3</span>
<div><h3>Change values directly</h3><p>After setup, ordinary property updates write into shared rows.</p></div>
<div>
<h3>Change values directly</h3>
<p>
After setup, ordinary property updates write into shared rows.
</p>
</div>
<code>mesh.position.x += 0.1</code>
</li>
</ol>
@@ -185,21 +241,32 @@
<section class="cta-section section-shell">
<div>
<div class="eyebrow eyebrow-light"><span></span> The best way to understand it</div>
<div class="eyebrow eyebrow-light">
<span></span> The best way to understand it
</div>
<h2>Change the code. Watch the passes.</h2>
<p>
The playground includes TypeScript intelligence, reusable snippets, live output,
shareable SQLite-backed revisions, and the engine’s real GPU profiler.
The playground includes TypeScript intelligence, reusable snippets,
live output, shareable SQLite-backed revisions, and the engine’s
real GPU profiler.
</p>
</div>
<a class="button button-white" href="/playground">Launch playground <span>↗</span></a>
<a class="button button-white" href="/playground"
>Launch playground <span>↗</span></a
>
</section>
</main>
<footer class="site-footer section-shell">
<a class="wordmark" href="/"><span class="wordmark-mark"><i></i><i></i><i></i></span><span>Yawn</span></a>
<a class="wordmark" href="/"
><span class="wordmark-mark"><i></i><i></i><i></i></span
><span>Yawn</span></a
>
<p>Start obvious. Optimize deliberately.</p>
<nav aria-label="Footer navigation"><a href="/docs">Docs</a><a href="/playground">Playground</a><a href="https://git.heaust.org/heaust/yawn">Source</a></nav>
<nav aria-label="Footer navigation">
<a href="/docs">Docs</a><a href="/playground">Playground</a
><a href="https://git.heaust.org/heaust/yawn">Source</a>
</nav>
</footer>
</body>
</html>
+178 -28
View File
@@ -4,9 +4,22 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f7fbff" />
<meta name="description" content="Write, run, profile, and share Yawn WebGPU scenes." />
<meta
name="description"
content="Write, run, profile, and share Yawn WebGPU scenes."
/>
<title>Yawn Playground</title>
<script src="/assets/theme.js"></script>
<link rel="stylesheet" href="/assets/playground.css" />
<link rel="stylesheet" href="/assets/theme.css" />
<script type="importmap">
{
"imports": {
"@yawn/core": "/pkg/core.js",
"@yawn/handles": "/pkg/handles.js"
}
}
</script>
<script src="/assets/monaco/vs/loader.js"></script>
<script type="module" src="/assets/playground.js"></script>
</head>
@@ -14,11 +27,18 @@
<main class="playground-app">
<header class="playground-header">
<a class="playground-brand" href="/" aria-label="Yawn home">
<span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span><strong>Yawn</strong>
<span class="brand-mark" aria-hidden="true"
><i></i><i></i><i></i></span
><strong>Yawn</strong>
</a>
<div class="header-divider"></div>
<div class="project-name">
<input id="project-title" maxlength="80" value="Sky triangle" aria-label="Playground title" />
<input
id="project-title"
maxlength="80"
value="Three-light icosphere"
aria-label="Playground title"
/>
<span id="dirty-indicator" title="Unsaved changes"></span>
<span id="revision-label">Draft</span>
</div>
@@ -26,6 +46,11 @@
<a href="/docs">Docs</a>
<a href="https://git.heaust.org/heaust/yawn">Source</a>
</nav>
<select class="theme-select" data-theme-select aria-label="Color theme">
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<button id="share-button" class="tool-button" type="button" hidden>
<span aria-hidden="true">↗</span> Share
</button>
@@ -39,9 +64,15 @@
<div class="playground-tools">
<div class="tool-group">
<button id="snippets-button" type="button"><span class="braces">{ }</span> Snippets</button>
<button id="reset-button" type="button"><span aria-hidden="true">↶</span> Reset</button>
<button id="format-button" type="button"><span aria-hidden="true">≡</span> Format</button>
<button id="snippets-button" type="button">
<span class="braces">{ }</span> Snippets
</button>
<button id="reset-button" type="button">
<span aria-hidden="true">↶</span> Reset
</button>
<button id="format-button" type="button">
<span aria-hidden="true">≡</span> Format
</button>
</div>
<div class="tool-context">
<span class="language-dot"></span>
@@ -50,17 +81,29 @@
<span id="type-status">Starting language service…</span>
</div>
<div class="tool-group tool-group-right">
<button id="inspector-button" type="button" aria-pressed="true"><span aria-hidden="true">▤</span> Inspector</button>
<button id="fullscreen-button" type="button" aria-label="Toggle fullscreen"><span aria-hidden="true">⛶</span></button>
<button id="inspector-button" type="button" aria-pressed="true">
<span aria-hidden="true">▤</span> Inspector
</button>
<button
id="fullscreen-button"
type="button"
aria-label="Toggle fullscreen"
>
<span aria-hidden="true">⛶</span>
</button>
</div>
</div>
<div class="workbench" id="workbench">
<section class="editor-pane" aria-label="TypeScript editor">
<div class="pane-tabs">
<button class="pane-tab active" type="button"><span class="ts-icon">TS</span> scene.ts <i id="tab-dirty"></i></button>
<button class="pane-tab active" type="button">
<span class="ts-icon">TS</span> scene.ts <i id="tab-dirty"></i>
</button>
</div>
<div id="editor" class="editor">
<div class="editor-loading">Loading TypeScript editor…</div>
</div>
<div id="editor" class="editor"><div class="editor-loading">Loading TypeScript editor…</div></div>
<footer class="editor-status">
<span id="cursor-status">Ln 1, Col 1</span>
<span>Spaces: 2</span>
@@ -70,7 +113,13 @@
</footer>
</section>
<div class="resize-handle" id="resize-handle" role="separator" aria-orientation="vertical" aria-label="Resize editor and preview"></div>
<div
class="resize-handle"
id="resize-handle"
role="separator"
aria-orientation="vertical"
aria-label="Resize editor and preview"
></div>
<section class="stage-pane" aria-label="Yawn output">
<div class="stage-toolbar">
@@ -95,22 +144,73 @@
</div>
<div class="inspector" id="inspector">
<div class="inspector-tabs" role="tablist">
<button type="button" class="active" role="tab" aria-selected="true" data-panel="console">Console <span id="console-count">0</span></button>
<button type="button" role="tab" aria-selected="false" data-panel="profile">GPU profiler</button>
<button id="clear-console" class="clear-console" type="button">Clear</button>
<button
type="button"
class="active"
role="tab"
aria-selected="true"
data-panel="console"
>
Console <span id="console-count">0</span>
</button>
<button
type="button"
role="tab"
aria-selected="false"
data-panel="profile"
>
GPU profiler
</button>
<button id="clear-console" class="clear-console" type="button">
Clear
</button>
</div>
<div class="console-panel inspector-panel active" id="console-panel" role="tabpanel">
<div class="console-empty" id="console-empty"><span>›_</span> Output from <code>log()</code> appears here.</div>
<div
class="console-panel inspector-panel active"
id="console-panel"
role="tabpanel"
>
<div class="console-empty" id="console-empty">
<span>›_</span> Output from <code>log()</code> appears here.
</div>
<ol id="console-lines"></ol>
</div>
<div class="profile-panel inspector-panel" id="profile-panel" role="tabpanel">
<div
class="profile-panel inspector-panel"
id="profile-panel"
role="tabpanel"
>
<div class="profile-empty" id="profile-empty">
<span class="pulse-icon">⌁</span>
<div><strong>Waiting for GPU timestamps</strong><small>Run the scene to inspect its physical passes.</small></div>
<div>
<strong>Waiting for GPU timestamps</strong
><small>Run the scene to inspect its physical passes.</small>
</div>
</div>
<div class="profile-results" id="profile-results" hidden>
<div class="profile-summary"><div><small>Total GPU time</small><strong id="profile-total">—</strong></div><div><small>Readback latency</small><strong id="profile-latency">—</strong></div><div><small>Adapter</small><strong id="profile-adapter">—</strong></div></div>
<table><thead><tr><th>Physical pass</th><th>GPU time</th></tr></thead><tbody id="profile-passes"></tbody></table>
<div class="profile-summary">
<div>
<small>Total GPU time</small
><strong id="profile-total">—</strong>
</div>
<div>
<small>Readback latency</small
><strong id="profile-latency">—</strong>
</div>
<div>
<small>Adapter</small
><strong id="profile-adapter">—</strong>
</div>
</div>
<table>
<thead>
<tr>
<th>Physical pass</th>
<th>GPU time</th>
</tr>
</thead>
<tbody id="profile-passes"></tbody>
</table>
</div>
</div>
</div>
@@ -120,16 +220,66 @@
<dialog id="snippets-dialog" class="snippets-dialog">
<div class="dialog-heading">
<div><span>Code library</span><h2>Snippets</h2><p>Insert a focused building block at your cursor.</p></div>
<button type="button" data-close-dialog aria-label="Close snippets">×</button>
<div>
<span>Code library</span>
<h2>Snippets</h2>
<p>Insert a focused building block at your cursor.</p>
</div>
<button type="button" data-close-dialog aria-label="Close snippets">
×
</button>
</div>
<div class="snippet-search">
<span>⌕</span
><input
type="search"
placeholder="Find a snippet"
aria-label="Find a snippet"
/>
</div>
<div class="snippet-search"><span>⌕</span><input type="search" placeholder="Find a snippet" aria-label="Find a snippet" /></div>
<div class="snippet-grid">
<button type="button" data-snippet="camera"><span class="snippet-icon">◎</span><div><strong>Orbit camera</strong><small>Pointer orbit and wheel zoom</small><code>ArcRotateCamera</code></div><b>+</b></button>
<button type="button" data-snippet="light"><span class="snippet-icon">☀</span><div><strong>Point light</strong><small>Warm, local illumination</small><code>PointLight</code></div><b>+</b></button>
<button type="button" data-snippet="instances"><span class="snippet-icon">◇</span><div><strong>Mesh instances</strong><small>Clone shared geometry</small><code>mesh.clone()</code></div><b>+</b></button>
<button type="button" data-snippet="rows"><span class="snippet-icon">≋</span><div><strong>Shared rows</strong><small>Add application-owned hot data</small><code>scene.ensureRows()</code></div><b>+</b></button>
<button type="button" data-snippet="post"><span class="snippet-icon">✦</span><div><strong>Post processing</strong><small>Color grading and FXAA</small><code>batchGraphUpdates()</code></div><b>+</b></button>
<button type="button" data-snippet="camera">
<span class="snippet-icon">◎</span>
<div>
<strong>Orbit camera</strong
><small>Pointer orbit and wheel zoom</small
><code>ArcRotateCamera</code>
</div>
<b>+</b>
</button>
<button type="button" data-snippet="light">
<span class="snippet-icon">☀</span>
<div>
<strong>Point light</strong><small>Warm, local illumination</small
><code>PointLight</code>
</div>
<b>+</b>
</button>
<button type="button" data-snippet="instances">
<span class="snippet-icon">◇</span>
<div>
<strong>Mesh instances</strong><small>Clone shared geometry</small
><code>mesh.clone()</code>
</div>
<b>+</b>
</button>
<button type="button" data-snippet="rows">
<span class="snippet-icon">≋</span>
<div>
<strong>Shared rows</strong
><small>Add application-owned hot data</small
><code>scene.ensureRows()</code>
</div>
<b>+</b>
</button>
<button type="button" data-snippet="post">
<span class="snippet-icon">✦</span>
<div>
<strong>Post processing</strong><small>Color grading and FXAA</small
><code>batchGraphUpdates()</code>
</div>
<b>+</b>
</button>
</div>
</dialog>