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:
+238
-68
@@ -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><head></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><script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@yawn/core": "https://yawn.heaust.org/pkg/core.js",
|
||||
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
||||
}
|
||||
}
|
||||
</script></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><canvas id="view" width="1280" height="720"></canvas>
|
||||
<script type="module" src="/app.js"></script></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 <= 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 () => {
|
||||
@@ -248,27 +344,40 @@ await scene.batchGraphUpdates(async () => {
|
||||
<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 () => {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user