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
+369
View File
@@ -0,0 +1,369 @@
<!doctype html>
<html lang="en">
<head>
<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." />
<title>Learn Yawn — Documentation</title>
<link rel="stylesheet" href="/assets/site.css" />
<link rel="stylesheet" href="/assets/docs.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>
</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" />
<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>
</nav>
</header>
<div class="docs-layout">
<aside class="docs-sidebar" aria-label="Documentation sections">
<div class="sidebar-group">
<strong>Start here</strong>
<a href="#welcome" class="active">Welcome to Yawn</a>
<a href="#installation">Installation</a>
<a href="#first-scene">Your first scene</a>
<a href="#camera">Add a camera</a>
</div>
<div class="sidebar-group">
<strong>Build a world</strong>
<a href="#meshes">Meshes and instances</a>
<a href="#materials">Materials and lights</a>
<a href="#models">Import a glTF model</a>
<a href="#post-processing">Post processing</a>
</div>
<div class="sidebar-group">
<strong>Understand Yawn</strong>
<a href="#mental-model">The mental model</a>
<a href="#shared-memory">Shared memory</a>
<a href="#profiler">GPU profiler</a>
</div>
<div class="sidebar-group">
<strong>Deep dives</strong>
<a href="#core-deep-dive">Using core directly</a>
<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>
</aside>
<main class="docs-main">
<article>
<section id="welcome" data-title="Welcome to Yawn">
<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.
</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>
</section>
<section id="installation" data-title="Installation">
<div class="section-kicker"><span>01</span> Installation</div>
<h2>Load Handles from Yawns 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 pages
<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>
<pre><code>&lt;script type="importmap"&gt;
{
"imports": {
"@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 Yawns 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>
<div class="code-block" data-language="html">
<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 Yawns files.</p></div>
</div>
</section>
<section id="first-scene" data-title="Your first scene">
<div class="section-kicker"><span>02</span> Your first scene</div>
<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.
</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";
const canvas = document.querySelector("#view");
const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;
const sky = new PBRMaterial(scene, {
baseColor: [0.12, 0.58, 1, 1],
metallic: 0.15,
roughness: 0.35,
});
await sky.ready;
const triangle = new Mesh(scene, {
material: sky,
vertexData: {
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
indices: [0, 1, 2],
},
});
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>
<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
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>
<pre><code>import { ArcRotateCamera } from "@yawn/handles";
const camera = new ArcRotateCamera(scene, {
target: triangle,
alpha: 0,
beta: Math.PI / 2,
radius: 3,
controls: { element: canvas, pointer: true },
});
await camera.ready;</code></pre>
</div>
</section>
<section id="meshes" data-title="Meshes and instances">
<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
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>
<pre><code>for (let x = -4; x &lt;= 4; x++) {
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
await copy.ready;
}</code></pre>
</div>
</section>
<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.
</p>
<div class="code-block" data-language="typescript">
<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, {
position: [0, 1, 1],
color: [1, 0.72, 0.5],
intensity: 12,
range: 8,
});
const fill = new AmbientLight(scene, {
color: [0.08, 0.2, 0.5],
intensity: 0.3,
});
await Promise.all([key.ready, fill.ready]);
sky.roughness = 0.5; // one shared-memory write</code></pre>
</div>
</section>
<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.
</p>
<div class="code-block" data-language="typescript">
<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");
meshes[0].position.y = 0.5;</code></pre>
</div>
</section>
<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>
<div class="code-block" data-language="typescript">
<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; {
const grade = new ColorGrading(scene, { toneMap: "aces" });
const fxaa = new FXAA(scene);
await Promise.all([grade.ready, fxaa.ready]);
});</code></pre>
</div>
</section>
<section id="mental-model" data-title="The mental model">
<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.
</p>
<div class="mental-model">
<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>
<i></i>
<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.
</p>
<div class="code-block" data-language="typescript">
<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"
);
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.
</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.
</p>
<div class="callout callout-blue"><span class="callout-icon"></span><div><strong>Profile a real scene</strong><p>The playgrounds 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.
</p>
<div class="code-block" data-language="typescript">
<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, {
arenaBytes: 64 * 1024 * 1024,
});
await core.ready;
const particles = await core.createRows({
name: "particles",
rows: 100_000,
stride: 16,
format: "f32",
});</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.
</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.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>Particle.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>class Particle {
constructor(
readonly id: number,
readonly positions: SharedRows,
) {}
set x(value: number) {
this.positions.row(this.id)[0] = value;
}
}</code></pre>
</div>
</section>
<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 Yawns 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>
</section>
</article>
</main>
<aside class="docs-toc" aria-label="On this page">
<strong>On this page</strong>
<a href="#installation">Installation</a>
<a href="#first-scene">Your first scene</a>
<a href="#camera">Add a camera</a>
<a href="#mental-model">The mental model</a>
<a href="#core-deep-dive">Using core directly</a>
<a href="#deployment">Deployment</a>
</aside>
</div>
</body>
</html>