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
+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>