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
+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[]>;
}