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:
@@ -0,0 +1,95 @@
|
||||
.docs-body { background: #fff; }
|
||||
.docs-header { position: sticky; top: 0; width: 100%; height: 66px; padding: 0 28px; border-bottom: 1px solid #dbe7f0; background: rgb(255 255 255 / 94%); backdrop-filter: blur(14px); }
|
||||
.docs-header .wordmark { font-size: 19px; }
|
||||
.docs-product { margin-left: -22px; padding-left: 18px; color: #8190a1; border-left: 1px solid #d7e3ec; font-size: 13px; font-weight: 600; }
|
||||
.docs-header .site-nav { flex: 0 0 auto; }
|
||||
.docs-search { display: flex; width: min(370px, 35vw); height: 36px; align-items: center; gap: 9px; margin: 0 auto; padding: 0 10px; color: #8496a9; border: 1px solid #dbe6ef; border-radius: 8px; background: #f8fbfd; }
|
||||
.docs-search input { min-width: 0; flex: 1; color: #243d57; border: 0; outline: 0; background: transparent; font-size: 12px; }
|
||||
.docs-search kbd { padding: 2px 6px; color: #93a3b4; border: 1px solid #d4e0e9; border-radius: 4px; background: white; font: 10px/1.4 ui-monospace, monospace; }
|
||||
.docs-layout { display: grid; grid-template-columns: 245px minmax(0, 790px) 180px; justify-content: center; gap: 58px; }
|
||||
.docs-sidebar { position: sticky; top: 66px; height: calc(100vh - 66px); overflow: auto; padding: 35px 20px 35px 29px; border-right: 1px solid #e2ebf2; scrollbar-width: thin; }
|
||||
.sidebar-group { display: grid; gap: 2px; margin-bottom: 27px; }
|
||||
.sidebar-group > strong, .docs-toc > strong { margin-bottom: 8px; color: #8a9aaa; font-size: 9px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
|
||||
.sidebar-group a { padding: 7px 10px; color: #64768a; border-radius: 6px; font-size: 12px; text-decoration: none; }
|
||||
.sidebar-group a:hover { color: #1685cf; background: #f3f9fd; }
|
||||
.sidebar-group a.active { color: #087fc9; background: #eaf7ff; font-weight: 650; }
|
||||
.sidebar-playground { display: flex; align-items: center; gap: 11px; margin-top: 36px; padding: 13px; color: #155a87; border: 1px solid #cde5f5; border-radius: 9px; background: #f2faff; text-decoration: none; }
|
||||
.sidebar-playground > span { display: grid; width: 25px; height: 25px; place-items: center; border-radius: 50%; color: white; background: #1b8fe5; font-size: 8px; }
|
||||
.sidebar-playground div { display: grid; gap: 3px; }
|
||||
.sidebar-playground strong { font-size: 11px; }
|
||||
.sidebar-playground small { color: #7e99ad; font-size: 9px; }
|
||||
.docs-main { min-width: 0; padding: 72px 0 150px; }
|
||||
.docs-main article { min-width: 0; }
|
||||
.docs-main section { padding-top: 80px; scroll-margin-top: 55px; }
|
||||
.docs-main section:first-child { padding-top: 0; }
|
||||
.docs-main section + section { margin-top: 20px; border-top: 1px solid #e5edf3; }
|
||||
.doc-eyebrow { margin-bottom: 16px; color: #1788d2; font-size: 10px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.docs-main h1 { max-width: 700px; margin: 0 0 20px; color: #102a44; font-size: 48px; letter-spacing: -.055em; line-height: 1.08; }
|
||||
.docs-main h2 { margin: 14px 0 19px; color: #112c47; font-size: 31px; letter-spacing: -.04em; line-height: 1.2; }
|
||||
.docs-main h3 { margin: 32px 0 12px; color: #1b3751; font-size: 18px; letter-spacing: -.02em; }
|
||||
.docs-main p, .docs-main li { color: #5f7083; font-size: 14px; line-height: 1.78; }
|
||||
.docs-main p { margin: 0 0 19px; }
|
||||
.docs-main strong { color: #314d67; }
|
||||
.doc-lede { max-width: 730px; color: #50657a !important; font-size: 17px !important; line-height: 1.75 !important; }
|
||||
.docs-main :not(pre) > code { padding: 2px 5px; color: #147ab9; border: 1px solid #d7e9f4; border-radius: 4px; background: #f3faff; font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.section-kicker { display: flex; align-items: center; gap: 9px; margin-bottom: 13px; color: #1587d0; font-size: 10px; font-weight: 800; letter-spacing: .11em; text-transform: uppercase; }
|
||||
.section-kicker span { display: grid; width: 23px; height: 23px; place-items: center; border: 1px solid #bde1f7; border-radius: 50%; background: #eef9ff; font: 700 8px/1 ui-monospace, monospace; }
|
||||
.callout { display: flex; gap: 14px; margin: 25px 0; padding: 18px 20px; border: 1px solid #e0e8ef; border-radius: 10px; background: #f9fbfd; }
|
||||
.callout-blue { border-color: #cce8f8; background: #f1faff; }
|
||||
.callout-icon { display: grid; width: 25px; height: 25px; flex: 0 0 auto; place-items: center; color: #1688d1; border: 1px solid #b9dff5; border-radius: 50%; font-size: 11px; font-weight: 800; }
|
||||
.callout strong { display: block; margin-bottom: 3px; font-size: 12px; }
|
||||
.callout p { margin: 0; font-size: 12px; line-height: 1.65; }
|
||||
.code-block { overflow: hidden; margin: 23px 0 26px; border: 1px solid #d4e2ec; border-radius: 10px; background: #0c2037; box-shadow: 0 8px 24px rgb(30 65 94 / 7%); }
|
||||
.code-title { display: flex; height: 38px; align-items: center; justify-content: space-between; padding: 0 12px 0 16px; color: #8299ae; border-bottom: 1px solid #233c54; background: #102840; font: 10px/1 ui-monospace, monospace; }
|
||||
.code-title button { padding: 5px 8px; color: #91a9bc; border: 1px solid #2b4861; border-radius: 5px; background: #163149; cursor: pointer; font-size: 9px; }
|
||||
.code-title button:hover { color: #bce7ff; border-color: #34729c; }
|
||||
.code-block pre { margin: 0; padding: 19px 21px 22px; overflow: auto; }
|
||||
.code-block code { color: #c8d8e6; font: 11.5px/1.75 ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; }
|
||||
.code-block-large code { font-size: 12px; }
|
||||
.explain-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 11px; margin: 24px 0; }
|
||||
.explain-grid > div { padding: 16px; border: 1px solid #e0e9f0; border-radius: 9px; background: #fbfdfe; }
|
||||
.explain-grid p { margin: 10px 0 0; font-size: 11px; line-height: 1.55; }
|
||||
.inline-play { display: flex; align-items: center; gap: 14px; margin: 27px 0; padding: 18px; color: #185e8c; border: 1px solid #c8e5f6; border-radius: 11px; background: #f0f9ff; text-decoration: none; }
|
||||
.inline-play > span { display: grid; width: 33px; height: 33px; place-items: center; border-radius: 50%; color: white; background: #198ddd; font-size: 10px; }
|
||||
.inline-play > div { display: grid; flex: 1; gap: 3px; }
|
||||
.inline-play strong { color: #185e8c; font-size: 12px; }
|
||||
.inline-play small { color: #7598af; font-size: 10px; }
|
||||
.inline-play b { font-size: 11px; }
|
||||
.mental-model { display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; align-items: center; gap: 11px; margin: 28px 0; }
|
||||
.mental-model > div { display: grid; min-height: 85px; align-content: center; gap: 7px; padding: 13px; border: 1px solid #d9e6ef; border-radius: 8px; background: #f9fcfe; text-align: center; }
|
||||
.mental-model strong { color: #31536e; font-size: 11px; }
|
||||
.mental-model span { color: #8a9aaa; font: 8px/1.5 ui-monospace, monospace; }
|
||||
.mental-model i { color: #8bbbd9; font-style: normal; }
|
||||
.mental-model .mental-shared { border-color: #a9daf5; background: #edf8ff; }
|
||||
.deploy-list { display: grid; gap: 0; margin: 20px 0 35px; padding: 0; list-style: none; }
|
||||
.deploy-list li { display: flex; gap: 16px; padding: 18px 0; border-bottom: 1px solid #e5edf3; }
|
||||
.deploy-list li > span { display: grid; width: 29px; height: 29px; flex: 0 0 auto; place-items: center; color: #1689d2; border: 1px solid #c3e2f5; border-radius: 50%; background: #f0f9ff; font: 700 10px/1 ui-monospace, monospace; }
|
||||
.deploy-list strong { display: block; margin-bottom: 3px; font-size: 13px; }
|
||||
.deploy-list p { margin: 0; font-size: 12px; }
|
||||
.next-card { display: flex; align-items: center; justify-content: space-between; gap: 35px; margin-top: 48px; padding: 30px; color: white; border-radius: 13px; background: linear-gradient(120deg, #0c2945, #0d5586); }
|
||||
.next-card small { color: #7dc8f2; font-size: 9px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
|
||||
.next-card h3 { margin: 7px 0 4px; color: white; }
|
||||
.next-card p { margin: 0; color: #a8c7da; font-size: 11px; }
|
||||
.next-card .button { flex: 0 0 auto; }
|
||||
.docs-toc { position: sticky; top: 66px; height: fit-content; display: grid; gap: 9px; padding-top: 47px; }
|
||||
.docs-toc a { padding-left: 11px; color: #8795a5; border-left: 1px solid #dce6ed; font-size: 10px; line-height: 1.5; text-decoration: none; }
|
||||
.docs-toc a:hover, .docs-toc a.active { color: #1789d2; border-color: #1789d2; }
|
||||
.search-hidden { display: none !important; }
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.docs-layout { grid-template-columns: 235px minmax(0, 740px); gap: 40px; padding-right: 30px; }
|
||||
.docs-toc { display: none; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.docs-header { padding-inline: 15px; }
|
||||
.docs-product, .docs-header .site-nav { display: none; }
|
||||
.docs-search { width: auto; flex: 1; margin-left: auto; }
|
||||
.docs-layout { display: block; padding: 0 18px; }
|
||||
.docs-sidebar { display: none; }
|
||||
.docs-main { padding-top: 48px; }
|
||||
.docs-main h1 { font-size: 39px; }
|
||||
.docs-main h2 { font-size: 27px; }
|
||||
.explain-grid, .mental-model { grid-template-columns: 1fr; }
|
||||
.mental-model > i { transform: rotate(90deg); text-align: center; }
|
||||
.next-card { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
const search = document.querySelector(".docs-search input");
|
||||
const sections = [...document.querySelectorAll(".docs-main section[data-title]")];
|
||||
const navigationLinks = [...document.querySelectorAll(".docs-sidebar a[href^='#']")];
|
||||
const tocLinks = [...document.querySelectorAll(".docs-toc a")];
|
||||
|
||||
for (const button of document.querySelectorAll("[data-copy]")) {
|
||||
button.addEventListener("click", async () => {
|
||||
const code = button.closest(".code-block")?.querySelector("code")?.textContent ?? "";
|
||||
await navigator.clipboard.writeText(code);
|
||||
const original = button.textContent;
|
||||
button.textContent = "Copied";
|
||||
setTimeout(() => (button.textContent = original), 1200);
|
||||
});
|
||||
}
|
||||
|
||||
search?.addEventListener("input", () => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
for (const section of sections) {
|
||||
const matches = !query || section.textContent.toLowerCase().includes(query);
|
||||
section.classList.toggle("search-hidden", !matches);
|
||||
}
|
||||
});
|
||||
|
||||
addEventListener("keydown", (event) => {
|
||||
if (event.key === "/" && document.activeElement !== search) {
|
||||
event.preventDefault();
|
||||
search?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const activate = (id) => {
|
||||
for (const link of [...navigationLinks, ...tocLinks]) {
|
||||
link.classList.toggle("active", link.hash === `#${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries.filter((entry) => entry.isIntersecting);
|
||||
if (visible.length) activate(visible.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0].target.id);
|
||||
},
|
||||
{ rootMargin: "-70px 0px -72%", threshold: 0 },
|
||||
);
|
||||
for (const section of sections) observer.observe(section);
|
||||
@@ -0,0 +1,163 @@
|
||||
:root {
|
||||
color: #17324c;
|
||||
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-synthesis: none;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||
button, input { font: inherit; }
|
||||
button { color: inherit; }
|
||||
.playground-app { display: grid; width: 100%; height: 100%; height: 100dvh; grid-template-rows: 58px 41px minmax(0, 1fr); background: #f7fbfe; }
|
||||
|
||||
.playground-header { display: flex; min-width: 0; align-items: center; gap: 14px; padding: 0 16px 0 19px; border-bottom: 1px solid #d5e3ed; background: #fff; }
|
||||
.playground-brand { display: inline-flex; align-items: center; gap: 9px; color: #102d49; text-decoration: none; }
|
||||
.playground-brand strong { font-size: 18px; letter-spacing: -.04em; }
|
||||
.brand-mark { position: relative; display: inline-block; width: 22px; height: 22px; }
|
||||
.brand-mark i { position: absolute; width: 7px; border: 2px solid #218ddd; border-radius: 7px; transform: rotate(-28deg); }
|
||||
.brand-mark i:nth-child(1) { left: 1px; top: 9px; height: 10px; }
|
||||
.brand-mark i:nth-child(2) { left: 7px; top: 5px; height: 14px; }
|
||||
.brand-mark i:nth-child(3) { left: 14px; top: 1px; height: 18px; }
|
||||
.header-divider { width: 1px; height: 22px; margin-inline: 2px; background: #d8e3eb; }
|
||||
.project-name { display: flex; min-width: 0; align-items: center; gap: 7px; }
|
||||
.project-name input { width: min(250px, 24vw); padding: 5px 7px; color: #29445e; border: 1px solid transparent; border-radius: 5px; outline: 0; background: transparent; font-size: 13px; font-weight: 650; text-overflow: ellipsis; }
|
||||
.project-name input:hover { background: #f5f9fc; }
|
||||
.project-name input:focus { border-color: #a9d8f5; background: #f7fcff; box-shadow: 0 0 0 2px rgb(50 166 232 / 10%); }
|
||||
#dirty-indicator, #tab-dirty { display: none; width: 6px; height: 6px; border-radius: 50%; background: #2a9ee6; }
|
||||
#dirty-indicator.visible, #tab-dirty.visible { display: inline-block; }
|
||||
#revision-label { padding: 4px 7px; color: #74879a; border: 1px solid #dae5ed; border-radius: 4px; background: #f8fafc; font: 600 9px/1 ui-monospace, monospace; }
|
||||
.playground-nav { display: flex; flex: 1; justify-content: flex-end; gap: 21px; }
|
||||
.playground-nav a { color: #687a8c; font-size: 11px; font-weight: 600; text-decoration: none; }
|
||||
.playground-nav a:hover { color: #1889d2; }
|
||||
.tool-button, .run-button { display: inline-flex; height: 32px; align-items: center; justify-content: center; gap: 7px; padding: 0 12px; border-radius: 6px; cursor: pointer; font-size: 11px; font-weight: 650; }
|
||||
.tool-button { color: #486177; border: 1px solid #d4e1ea; background: #fff; }
|
||||
.tool-button:hover { color: #177fc2; border-color: #add5ee; background: #f6fbfe; }
|
||||
.run-button { min-width: 111px; color: white; border: 1px solid #1687ce; background: #1a8fda; box-shadow: 0 4px 12px rgb(26 143 218 / 18%); }
|
||||
.run-button:hover { background: #117fc5; }
|
||||
.run-button:disabled, .tool-button:disabled { cursor: wait; opacity: .58; }
|
||||
.run-button kbd { padding-left: 7px; color: rgb(255 255 255 / 70%); border: 0; border-left: 1px solid rgb(255 255 255 / 24%); font: 8px/1 ui-monospace, monospace; }
|
||||
|
||||
.playground-tools { display: flex; min-width: 0; align-items: center; gap: 14px; padding: 0 10px; border-bottom: 1px solid #cfdce6; background: #f6f9fb; }
|
||||
.tool-group { display: flex; align-items: center; gap: 2px; }
|
||||
.tool-group button { display: inline-flex; height: 29px; align-items: center; gap: 6px; padding: 0 9px; color: #586d80; border: 1px solid transparent; border-radius: 5px; background: transparent; cursor: pointer; font-size: 10px; }
|
||||
.tool-group button:hover, .tool-group button[aria-pressed="true"] { color: #187fc2; border-color: #d3e3ed; background: #fff; }
|
||||
.braces { color: #2498df; font: 700 10px/1 ui-monospace, monospace; }
|
||||
.tool-context { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: center; gap: 7px; color: #748698; font-size: 9px; }
|
||||
.language-dot { width: 6px; height: 6px; border-radius: 2px; background: #2d9de3; }
|
||||
.context-divider { width: 1px; height: 12px; margin-inline: 4px; background: #cfdae3; }
|
||||
#type-status { overflow: hidden; max-width: 250px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tool-group-right { margin-left: auto; }
|
||||
|
||||
.workbench { --editor-width: 50%; display: grid; min-width: 0; min-height: 0; grid-template-columns: minmax(330px, var(--editor-width)) 5px minmax(330px, 1fr); background: #0b1725; }
|
||||
.editor-pane, .stage-pane { min-width: 0; min-height: 0; }
|
||||
.editor-pane { display: grid; grid-template-rows: 37px minmax(0, 1fr) 24px; }
|
||||
.pane-tabs, .stage-toolbar { display: flex; min-width: 0; align-items: stretch; border-bottom: 1px solid #1d3245; background: #112233; }
|
||||
.pane-tab { display: flex; min-width: 145px; align-items: center; gap: 7px; padding: 0 14px; color: #9db1c2; border: 0; border-right: 1px solid #22384b; background: #0d1d2d; font-size: 10px; }
|
||||
.pane-tab.active { color: #d4e2ed; border-top: 2px solid #2ba5ea; background: #0b1927; }
|
||||
.ts-icon { display: grid; width: 18px; height: 16px; place-items: center; color: #fff; border-radius: 2px; background: #278dcd; font: 700 7px/1 ui-monospace, monospace; }
|
||||
.editor { position: relative; min-height: 0; overflow: hidden; background: #0b1927; }
|
||||
.editor-loading { display: grid; height: 100%; place-items: center; color: #668097; font: 10px/1 ui-monospace, monospace; }
|
||||
.monaco-editor .margin { background-color: #0b1927 !important; }
|
||||
.editor-status { display: flex; align-items: center; gap: 14px; padding: 0 10px; color: #6f8599; border-top: 1px solid #1b3042; background: #0c1d2c; font: 8px/1 ui-monospace, monospace; }
|
||||
.status-spacer { flex: 1; }
|
||||
#editor-diagnostics { display: flex; align-items: center; gap: 5px; }
|
||||
#editor-diagnostics i { width: 5px; height: 5px; border-radius: 50%; background: #4aa975; }
|
||||
#editor-diagnostics.has-errors { color: #e8a4a4; }
|
||||
#editor-diagnostics.has-errors i { background: #e26f6f; }
|
||||
.resize-handle { position: relative; z-index: 3; cursor: col-resize; background: #172a3b; }
|
||||
.resize-handle::after { position: absolute; top: 50%; left: 1px; width: 3px; height: 40px; transform: translateY(-50%); border-radius: 3px; background: #355067; content: ""; opacity: .65; }
|
||||
.resize-handle:hover, .resize-handle.dragging { background: #218ed2; }
|
||||
|
||||
.stage-pane { display: grid; grid-template-rows: 37px minmax(230px, 1fr) 190px; background: #091521; }
|
||||
.stage-toolbar { justify-content: space-between; }
|
||||
.stage-tabs button { height: 100%; padding: 0 14px; color: #8da3b6; border: 0; border-top: 2px solid transparent; background: transparent; font-size: 10px; }
|
||||
.stage-tabs button.active { color: #d4e3ee; border-top-color: #35a9ed; background: #0d1d2c; }
|
||||
.stage-meta { display: flex; min-width: 0; align-items: center; gap: 10px; padding-right: 12px; color: #6e869a; font: 8px/1 ui-monospace, monospace; }
|
||||
#run-status { display: flex; align-items: center; gap: 6px; }
|
||||
#run-status i { width: 6px; height: 6px; border-radius: 50%; background: #4cad79; box-shadow: 0 0 0 3px rgb(76 173 121 / 10%); }
|
||||
#run-status.running i { background: #36a8e8; animation: status-pulse 1s infinite; }
|
||||
#run-status.failed { color: #e49a9a; }
|
||||
#run-status.failed i { background: #e36d6d; }
|
||||
.stage-divider { width: 1px; height: 12px; background: #294052; }
|
||||
.preview { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: radial-gradient(circle at 50% 45%, #102b43, #06111d 72%); }
|
||||
.preview::before { position: absolute; inset: 0; background-image: linear-gradient(rgb(82 143 182 / 5%) 1px, transparent 1px), linear-gradient(90deg, rgb(82 143 182 / 5%) 1px, transparent 1px); background-size: 32px 32px; content: ""; pointer-events: none; }
|
||||
.preview canvas { position: relative; display: block; width: 100%; height: 100%; object-fit: contain; touch-action: none; }
|
||||
.preview-message { position: absolute; top: 50%; left: 50%; display: grid; justify-items: center; gap: 7px; transform: translate(-50%, -50%); color: #93aabd; text-align: center; pointer-events: none; }
|
||||
.preview-message[hidden] { display: none; }
|
||||
.preview-message-icon { display: grid; width: 43px; height: 43px; margin-bottom: 7px; place-items: center; color: #54bdf6; border: 1px solid #25577a; border-radius: 12px; background: #0b2941; box-shadow: 0 0 30px rgb(46 171 238 / 12%); font-size: 16px; font-weight: 800; }
|
||||
.preview-message strong { color: #b9cddd; font-size: 11px; }
|
||||
.preview-message small { color: #627a8f; font-size: 9px; }
|
||||
|
||||
.inspector { min-height: 0; border-top: 1px solid #22384b; background: #0a1825; }
|
||||
.inspector.collapsed { display: none; }
|
||||
.stage-pane:has(.inspector.collapsed) { grid-template-rows: 37px minmax(230px, 1fr) 0; }
|
||||
.inspector-tabs { display: flex; height: 31px; align-items: stretch; border-bottom: 1px solid #1c3041; background: #0e1e2c; }
|
||||
.inspector-tabs button { padding: 0 13px; color: #71889b; border: 0; border-bottom: 1px solid transparent; background: transparent; cursor: pointer; font-size: 9px; }
|
||||
.inspector-tabs button.active { color: #c0d2df; border-bottom-color: #32a8eb; }
|
||||
.inspector-tabs button span { margin-left: 4px; padding: 1px 5px; border-radius: 5px; background: #1a3245; font-size: 7px; }
|
||||
.inspector-tabs .clear-console { margin-left: auto; color: #61788b; }
|
||||
.inspector-panel { display: none; height: calc(100% - 31px); overflow: auto; }
|
||||
.inspector-panel.active { display: block; }
|
||||
.console-empty, .profile-empty { display: flex; height: 100%; align-items: center; justify-content: center; gap: 10px; color: #718a9f; font: 9px/1 ui-monospace, monospace; }
|
||||
.console-empty > span { color: #318fc7; font-size: 12px; }
|
||||
.console-empty code { color: #90aabd; }
|
||||
#console-lines { margin: 0; padding: 5px 0; color: #9cb1c1; list-style: none; font: 9px/1.6 ui-monospace, monospace; }
|
||||
#console-lines li { display: flex; min-height: 22px; align-items: flex-start; gap: 9px; padding: 3px 10px; border-bottom: 1px solid rgb(36 57 73 / 45%); white-space: pre-wrap; word-break: break-word; }
|
||||
#console-lines li::before { color: #399ed9; content: "›"; }
|
||||
#console-lines li.error { color: #e19a9a; }
|
||||
#console-lines li.error::before { color: #dd6b6b; content: "×"; }
|
||||
.profile-empty { font-family: inherit; }
|
||||
.profile-empty .pulse-icon { display: grid; width: 30px; height: 30px; place-items: center; color: #48ace4; border: 1px solid #24516f; border-radius: 50%; background: #0e2940; }
|
||||
.profile-empty > div { display: grid; gap: 4px; }
|
||||
.profile-empty strong { color: #91a9ba; font-size: 10px; }
|
||||
.profile-empty small { color: #526b7e; font-size: 8px; }
|
||||
.profile-results { padding: 10px 12px; color: #8fa6b8; }
|
||||
.profile-summary { display: grid; grid-template-columns: .7fr .8fr 1.5fr; gap: 8px; margin-bottom: 9px; }
|
||||
.profile-summary > div { display: grid; gap: 3px; padding: 7px 9px; border: 1px solid #1e384b; border-radius: 5px; background: #0e2130; }
|
||||
.profile-summary small { color: #587185; font-size: 7px; text-transform: uppercase; }
|
||||
.profile-summary strong { overflow: hidden; color: #9fc7de; font: 8px/1.3 ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.profile-results table { width: 100%; border-collapse: collapse; font: 8px/1.4 ui-monospace, monospace; }
|
||||
.profile-results th, .profile-results td { padding: 5px 7px; border-bottom: 1px solid #1b3041; text-align: left; }
|
||||
.profile-results th { color: #506b80; font-weight: 600; }
|
||||
.profile-results td:last-child, .profile-results th:last-child { color: #56b4e8; text-align: right; }
|
||||
|
||||
.snippets-dialog { width: min(580px, calc(100% - 30px)); padding: 0; color: #18354f; border: 1px solid #cadce8; border-radius: 14px; background: #fff; box-shadow: 0 30px 90px rgb(5 25 42 / 28%); }
|
||||
.snippets-dialog::backdrop { background: rgb(6 23 38 / 48%); backdrop-filter: blur(3px); }
|
||||
.dialog-heading { display: flex; align-items: flex-start; justify-content: space-between; padding: 25px 26px 17px; }
|
||||
.dialog-heading span { color: #1b8ed8; font-size: 8px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
|
||||
.dialog-heading h2 { margin: 5px 0 3px; font-size: 23px; letter-spacing: -.04em; }
|
||||
.dialog-heading p { margin: 0; color: #798b9c; font-size: 10px; }
|
||||
.dialog-heading > button { color: #7c8fa0; border: 0; background: transparent; cursor: pointer; font-size: 22px; }
|
||||
.snippet-search { display: flex; height: 35px; align-items: center; gap: 8px; margin: 0 26px 14px; padding: 0 10px; color: #8a9aaa; border: 1px solid #d8e5ed; border-radius: 7px; background: #f9fbfd; }
|
||||
.snippet-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; font-size: 10px; }
|
||||
.snippet-grid { display: grid; gap: 1px; padding: 0 16px 18px; }
|
||||
.snippet-grid > button { display: grid; min-height: 66px; grid-template-columns: 40px 1fr auto; align-items: center; gap: 12px; padding: 8px 10px; text-align: left; border: 1px solid transparent; border-radius: 8px; background: transparent; cursor: pointer; }
|
||||
.snippet-grid > button:hover { border-color: #cee5f4; background: #f3faff; }
|
||||
.snippet-icon { display: grid; width: 36px; height: 36px; place-items: center; color: #168bd3; border: 1px solid #c6e4f6; border-radius: 9px; background: #edf9ff; font-size: 14px; }
|
||||
.snippet-grid button > div { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 3px 8px; }
|
||||
.snippet-grid strong { color: #294861; font-size: 11px; }
|
||||
.snippet-grid small { grid-column: 1; color: #7b8e9f; font-size: 9px; }
|
||||
.snippet-grid code { grid-column: 2; grid-row: 1 / 3; justify-self: end; padding: 4px 6px; color: #3b82ac; border: 1px solid #d8e8f1; border-radius: 4px; background: #f9fcfe; font: 8px/1 ui-monospace, monospace; }
|
||||
.snippet-grid b { color: #329cdc; font-size: 16px; }
|
||||
.toast { position: fixed; z-index: 100; right: 18px; bottom: 18px; max-width: 360px; padding: 11px 15px; transform: translateY(20px); color: #d8e9f4; border: 1px solid #2d5875; border-radius: 7px; background: #102d42; box-shadow: 0 12px 35px rgb(2 17 29 / 24%); font-size: 10px; opacity: 0; pointer-events: none; transition: opacity 180ms ease, transform 180ms ease; }
|
||||
.toast.visible { transform: none; opacity: 1; }
|
||||
@keyframes status-pulse { 50% { opacity: .35; } }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.playground-header { gap: 8px; padding-inline: 10px; }
|
||||
.header-divider, .playground-nav, #share-button, .project-name #revision-label { display: none; }
|
||||
.project-name { min-width: 0; flex: 1; }
|
||||
.project-name input { width: 100%; }
|
||||
.tool-button { padding-inline: 8px; }
|
||||
.run-button { min-width: auto; }
|
||||
.run-button kbd { display: none; }
|
||||
.tool-context { display: none; }
|
||||
.tool-group-right { margin-left: auto; }
|
||||
.workbench { display: grid; overflow: auto; grid-template-columns: 1fr; grid-template-rows: minmax(400px, 55vh) minmax(450px, 1fr); }
|
||||
.resize-handle { display: none; }
|
||||
.stage-pane { min-height: 500px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
const defaultSource = `import {
|
||||
ArcRotateCamera,
|
||||
ColorGrading,
|
||||
FXAA,
|
||||
Mesh,
|
||||
PBRMaterial,
|
||||
PointLight,
|
||||
Scene,
|
||||
} from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
||||
await scene.ready;
|
||||
|
||||
const material = new PBRMaterial(scene, {
|
||||
baseColor: [0.12, 0.58, 1, 1],
|
||||
metallic: 0.15,
|
||||
roughness: 0.35,
|
||||
});
|
||||
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],
|
||||
},
|
||||
});
|
||||
await mesh.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);
|
||||
|
||||
export default {
|
||||
scene,
|
||||
mesh,
|
||||
dispose() {
|
||||
canvas.removeEventListener("pointermove", move);
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const snippets = {
|
||||
camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
|
||||
const camera = new ArcRotateCamera(scene, {
|
||||
target: mesh,
|
||||
alpha: 0,
|
||||
beta: Math.PI / 2,
|
||||
radius: 3,
|
||||
controls: { element: canvas, pointer: true },
|
||||
});
|
||||
await camera.ready;`,
|
||||
light: `// A warm point light backed by shared rows.
|
||||
const light = new PointLight(scene, {
|
||||
position: [0, 0.7, 1],
|
||||
color: [1, 0.68, 0.4],
|
||||
intensity: 12,
|
||||
range: 8,
|
||||
});
|
||||
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] });
|
||||
await instance.ready;
|
||||
}`,
|
||||
rows: `// Application-owned hot data in the same shared arena.
|
||||
const velocity = await scene.ensureRows("app.velocity", 1024, 16, "f32");
|
||||
velocity.row(0).set([1, 0, 0, 0]);
|
||||
log("velocity[0]", velocity.read(0));`,
|
||||
post: `// Batch graph changes so the loadout is rebuilt once.
|
||||
await scene.batchGraphUpdates(async () => {
|
||||
const grade = new ColorGrading(scene, { toneMap: "aces", amount: 1 });
|
||||
const fxaa = new FXAA(scene);
|
||||
await Promise.all([grade.ready, fxaa.ready]);
|
||||
});`,
|
||||
};
|
||||
|
||||
const elements = Object.fromEntries(
|
||||
[
|
||||
"canvas",
|
||||
"clear-console",
|
||||
"console-count",
|
||||
"console-empty",
|
||||
"console-lines",
|
||||
"cursor-status",
|
||||
"dirty-indicator",
|
||||
"editor",
|
||||
"editor-diagnostics",
|
||||
"format-button",
|
||||
"fps-status",
|
||||
"fullscreen-button",
|
||||
"inspector",
|
||||
"inspector-button",
|
||||
"preview",
|
||||
"preview-message",
|
||||
"profile-adapter",
|
||||
"profile-empty",
|
||||
"profile-latency",
|
||||
"profile-passes",
|
||||
"profile-results",
|
||||
"profile-total",
|
||||
"project-title",
|
||||
"reset-button",
|
||||
"resize-handle",
|
||||
"resolution-status",
|
||||
"revision-label",
|
||||
"run-button",
|
||||
"run-status",
|
||||
"save-button",
|
||||
"share-button",
|
||||
"snippets-button",
|
||||
"snippets-dialog",
|
||||
"tab-dirty",
|
||||
"toast",
|
||||
"type-status",
|
||||
"workbench",
|
||||
].map((id) => [id, document.getElementById(id)]),
|
||||
);
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const packages = Promise.all([import("/pkg/handles.js"), import("/pkg/core.js")]).then(
|
||||
([handles, core]) => ({ ...handles, ...core }),
|
||||
);
|
||||
let monaco;
|
||||
let editor;
|
||||
let model;
|
||||
let current;
|
||||
let currentID = "";
|
||||
let currentRevision = 0;
|
||||
let initialSource = defaultSource;
|
||||
let generation = 0;
|
||||
let dirty = false;
|
||||
let activePanel = "console";
|
||||
let stopProfile;
|
||||
let profileCore;
|
||||
let toastTimer;
|
||||
let frameRequest;
|
||||
let sampledFrame = 0;
|
||||
let sampledAt = 0;
|
||||
|
||||
function savedLocation() {
|
||||
const match = location.pathname.match(/^\/playground\/([a-z0-9]{6,24})\/(\d+)\/?$/);
|
||||
return match ? { id: match[1], revision: Number(match[2]) } : undefined;
|
||||
}
|
||||
|
||||
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 result = await response.json();
|
||||
currentID = result.id;
|
||||
currentRevision = result.revision;
|
||||
elements["project-title"].value = result.title;
|
||||
elements["revision-label"].textContent = `Revision ${result.revision}`;
|
||||
elements["share-button"].hidden = false;
|
||||
initialSource = result.code;
|
||||
document.title = `${result.title} — Yawn Playground`;
|
||||
return result.code;
|
||||
} catch (error) {
|
||||
history.replaceState(null, "", "/playground");
|
||||
setRunStatus(error.message, "failed");
|
||||
showToast(error.message);
|
||||
}
|
||||
}
|
||||
return localStorage.getItem("yawn:playground:draft") || defaultSource;
|
||||
}
|
||||
|
||||
function loadMonaco() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.require?.config) {
|
||||
reject(new Error("The TypeScript editor could not be loaded."));
|
||||
return;
|
||||
}
|
||||
const monacoBase = `${window.location.origin}/assets/monaco/`;
|
||||
const workerSource = `
|
||||
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoBase)} };
|
||||
importScripts(${JSON.stringify(`${monacoBase}vs/base/worker/workerMain.js`)});
|
||||
`;
|
||||
const workerURL = URL.createObjectURL(
|
||||
new Blob([workerSource], { type: "text/javascript" }),
|
||||
);
|
||||
window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
|
||||
window.require.config({ paths: { vs: `${monacoBase}vs` } });
|
||||
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 defaults = monaco.languages.typescript.typescriptDefaults;
|
||||
defaults.setEagerModelSync(true);
|
||||
defaults.setCompilerOptions({
|
||||
allowNonTsExtensions: true,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
noEmit: false,
|
||||
strict: true,
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
});
|
||||
defaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false });
|
||||
defaults.addExtraLib(types, "file:///types/yawn.d.ts");
|
||||
monaco.editor.defineTheme("yawn", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: "comment", foreground: "658198", fontStyle: "italic" },
|
||||
{ token: "keyword", foreground: "C792EA" },
|
||||
{ token: "string", foreground: "8FD5B6" },
|
||||
{ token: "number", foreground: "F6AD7B" },
|
||||
{ token: "type.identifier", foreground: "69C7F4" },
|
||||
],
|
||||
colors: {
|
||||
"editor.background": "#0B1927",
|
||||
"editor.foreground": "#C8D8E5",
|
||||
"editor.lineHighlightBackground": "#102235",
|
||||
"editor.selectionBackground": "#1D5B8066",
|
||||
"editorCursor.foreground": "#55BDF4",
|
||||
"editorGutter.background": "#0B1927",
|
||||
"editorLineNumber.foreground": "#405B70",
|
||||
"editorLineNumber.activeForeground": "#89A4B8",
|
||||
"editorIndentGuide.background1": "#1B3143",
|
||||
"editorWidget.background": "#102538",
|
||||
"editorSuggestWidget.background": "#102538",
|
||||
},
|
||||
});
|
||||
elements.editor.textContent = "";
|
||||
model = monaco.editor.createModel(source, "typescript", monaco.Uri.parse("file:///playground/scene.ts"));
|
||||
editor = monaco.editor.create(elements.editor, {
|
||||
model,
|
||||
theme: "yawn",
|
||||
automaticLayout: true,
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
fontLigatures: true,
|
||||
fontSize: 12,
|
||||
lineHeight: 20,
|
||||
minimap: { enabled: false },
|
||||
padding: { top: 12, bottom: 12 },
|
||||
renderLineHighlight: "all",
|
||||
scrollBeyondLastLine: false,
|
||||
smoothScrolling: true,
|
||||
tabSize: 2,
|
||||
wordWrap: "off",
|
||||
});
|
||||
editor.addAction({
|
||||
id: "yawn.run",
|
||||
label: "Run playground",
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||
run,
|
||||
});
|
||||
editor.onDidChangeCursorPosition(({ position }) => {
|
||||
elements["cursor-status"].textContent = `Ln ${position.lineNumber}, Col ${position.column}`;
|
||||
});
|
||||
model.onDidChangeContent(() => {
|
||||
markDirty(true);
|
||||
if (!currentID) localStorage.setItem("yawn:playground:draft", model.getValue());
|
||||
});
|
||||
monaco.editor.onDidChangeMarkers(([resource]) => {
|
||||
if (resource.toString() === model.uri.toString()) updateDiagnostics();
|
||||
});
|
||||
elements["type-status"].textContent = "TypeScript language service ready";
|
||||
updateDiagnostics();
|
||||
}
|
||||
|
||||
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 total = errors + warnings;
|
||||
elements["editor-diagnostics"].classList.toggle("has-errors", errors > 0);
|
||||
elements["editor-diagnostics"].lastChild.textContent = total
|
||||
? ` ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`
|
||||
: " No problems";
|
||||
elements["type-status"].textContent = errors
|
||||
? `${errors} TypeScript error${errors === 1 ? "" : "s"}`
|
||||
: "TypeScript language service ready";
|
||||
}
|
||||
|
||||
function markDirty(value) {
|
||||
dirty = value;
|
||||
elements["dirty-indicator"].classList.toggle("visible", value);
|
||||
elements["tab-dirty"].classList.toggle("visible", value);
|
||||
}
|
||||
|
||||
async function transpile() {
|
||||
const worker = await monaco.languages.typescript.getTypeScriptWorker();
|
||||
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.");
|
||||
return output.text
|
||||
.replace(/^\s*import\s+(?:[\s\S]*?\s+from\s+)?["'][^"']+["'];?\s*$/gm, "")
|
||||
.replace(/^\s*export\s*\{\s*\};?\s*$/gm, "")
|
||||
.replace(/\bexport\s+default\s+/, "return ");
|
||||
}
|
||||
|
||||
async function disposeCurrent() {
|
||||
stopProfile?.();
|
||||
stopProfile = undefined;
|
||||
if (profileCore) {
|
||||
try {
|
||||
await profileCore.setProfiler(false);
|
||||
} catch {
|
||||
// A disposed core cannot disable a profiler that is already gone.
|
||||
}
|
||||
profileCore = undefined;
|
||||
}
|
||||
const value = current;
|
||||
current = undefined;
|
||||
delete window.__yawnPlayground;
|
||||
if (!value) return;
|
||||
if (typeof value === "function") await value();
|
||||
else if (typeof value.dispose === "function") await value.dispose();
|
||||
else if (value.scene?.dispose) value.scene.dispose();
|
||||
}
|
||||
|
||||
function replaceCanvas() {
|
||||
const previous = elements.canvas;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = "canvas";
|
||||
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));
|
||||
previous.replaceWith(canvas);
|
||||
elements.canvas = canvas;
|
||||
elements["resolution-status"].textContent = `${canvas.width} × ${canvas.height}`;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (!editor || elements["run-button"].disabled) return;
|
||||
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.");
|
||||
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.");
|
||||
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)(
|
||||
...names.map((name) => api[name]),
|
||||
canvas,
|
||||
(...values) => appendConsole(values.map(formatValue).join(" ")),
|
||||
);
|
||||
if (runID !== generation) {
|
||||
if (result?.dispose) await result.dispose();
|
||||
return;
|
||||
}
|
||||
current = result;
|
||||
window.__yawnPlayground = result;
|
||||
elements["preview-message"].hidden = true;
|
||||
setRunStatus(`Running · ${Math.round(performance.now() - started)} ms`, "ready");
|
||||
if (activePanel === "profile") await attachProfiler();
|
||||
} catch (error) {
|
||||
if (runID === generation) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
appendConsole(message, true);
|
||||
setRunStatus("Run failed", "failed");
|
||||
showPreviewMessage("Scene could not start", message);
|
||||
openInspector("console");
|
||||
}
|
||||
} finally {
|
||||
if (runID === generation) {
|
||||
elements["run-button"].disabled = false;
|
||||
elements["run-button"].innerHTML = '<span aria-hidden="true">▶</span> Run <kbd>Ctrl ↵</kbd>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function currentCore() {
|
||||
return current?.scene?.core ?? current?.core;
|
||||
}
|
||||
|
||||
async function attachProfiler() {
|
||||
stopProfile?.();
|
||||
stopProfile = undefined;
|
||||
profileCore = currentCore();
|
||||
if (!profileCore?.onProfile || !profileCore?.setProfiler) {
|
||||
setProfileMessage("Run a scene to begin profiling.");
|
||||
return;
|
||||
}
|
||||
stopProfile = profileCore.onProfile(renderProfile);
|
||||
const supported = await profileCore.setProfiler(true);
|
||||
if (!supported) setProfileMessage("Timestamp queries are not supported by this GPU adapter.");
|
||||
}
|
||||
|
||||
function setProfileMessage(message) {
|
||||
elements["profile-empty"].querySelector("strong").textContent = message;
|
||||
elements["profile-empty"].hidden = false;
|
||||
elements["profile-results"].hidden = true;
|
||||
}
|
||||
|
||||
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-adapter"].textContent = profile.adapter || "WebGPU adapter";
|
||||
elements["profile-adapter"].title = profile.adapter || "WebGPU adapter";
|
||||
elements["profile-passes"].replaceChildren(
|
||||
...profile.passes.map((pass) => {
|
||||
const row = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
const time = document.createElement("td");
|
||||
name.textContent = pass.name;
|
||||
time.textContent = `${pass.milliseconds.toFixed(2)} ms`;
|
||||
row.append(name, time);
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (typeof value === "string") return value;
|
||||
if (value instanceof Error) return value.message;
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function appendConsole(message, error = false) {
|
||||
elements["console-empty"].hidden = true;
|
||||
const line = document.createElement("li");
|
||||
line.textContent = message;
|
||||
line.classList.toggle("error", error);
|
||||
elements["console-lines"].append(line);
|
||||
elements["console-count"].textContent = elements["console-lines"].children.length;
|
||||
line.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
elements["console-lines"].replaceChildren();
|
||||
elements["console-empty"].hidden = false;
|
||||
elements["console-count"].textContent = "0";
|
||||
}
|
||||
|
||||
function setRunStatus(message, state) {
|
||||
elements["run-status"].className = state === "running" ? "running" : state === "failed" ? "failed" : "";
|
||||
elements["run-status"].lastChild.textContent = ` ${message}`;
|
||||
}
|
||||
|
||||
function showPreviewMessage(title, detail) {
|
||||
elements["preview-message"].hidden = false;
|
||||
elements["preview-message"].querySelector("strong").textContent = title;
|
||||
elements["preview-message"].querySelector("small").textContent = detail;
|
||||
}
|
||||
|
||||
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]")) {
|
||||
const active = button.dataset.panel === panel;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-selected", String(active));
|
||||
}
|
||||
for (const target of document.querySelectorAll(".inspector-panel")) {
|
||||
target.classList.toggle("active", target.id === `${panel}-panel`);
|
||||
}
|
||||
if (panel === "profile") void attachProfiler();
|
||||
else if (profileCore) {
|
||||
stopProfile?.();
|
||||
stopProfile = undefined;
|
||||
void profileCore.setProfiler(false).catch(() => undefined);
|
||||
profileCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!editor || elements["save-button"].disabled) return;
|
||||
elements["save-button"].disabled = true;
|
||||
try {
|
||||
const response = await fetch("/api/playgrounds", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: currentID,
|
||||
title: elements["project-title"].value.trim() || "Untitled playground",
|
||||
code: model.getValue(),
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || "The playground could not be saved.");
|
||||
currentID = result.id;
|
||||
currentRevision = result.revision;
|
||||
initialSource = result.code;
|
||||
history.pushState(null, "", `/playground/${result.id}/${result.revision}`);
|
||||
elements["revision-label"].textContent = `Revision ${result.revision}`;
|
||||
elements["share-button"].hidden = false;
|
||||
localStorage.removeItem("yawn:playground:draft");
|
||||
markDirty(false);
|
||||
showToast(`Saved revision ${result.revision}`);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
elements["save-button"].disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
clearTimeout(toastTimer);
|
||||
elements.toast.textContent = message;
|
||||
elements.toast.classList.add("visible");
|
||||
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 }]);
|
||||
elements["snippets-dialog"].close();
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function sampleFPS(now = performance.now()) {
|
||||
try {
|
||||
const core = currentCore();
|
||||
if (!core) throw new Error();
|
||||
const frame = Number(core.array("signals").row(0)[1]);
|
||||
if (!sampledAt || frame < sampledFrame) {
|
||||
sampledAt = 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`;
|
||||
sampledAt = now;
|
||||
sampledFrame = frame;
|
||||
}
|
||||
} catch {
|
||||
sampledAt = now;
|
||||
sampledFrame = 0;
|
||||
elements["fps-status"].textContent = "0 FPS";
|
||||
}
|
||||
frameRequest = requestAnimationFrame(sampleFPS);
|
||||
}
|
||||
|
||||
elements["run-button"].addEventListener("click", run);
|
||||
elements["save-button"].addEventListener("click", save);
|
||||
elements["share-button"].addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(location.href);
|
||||
showToast("Playground URL copied");
|
||||
});
|
||||
elements["project-title"].addEventListener("input", () => markDirty(true));
|
||||
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["snippets-dialog"].addEventListener("click", (event) => {
|
||||
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]")) {
|
||||
button.addEventListener("click", () => openInspector(button.dataset.panel));
|
||||
}
|
||||
elements["clear-console"].addEventListener("click", clearConsole);
|
||||
elements["inspector-button"].addEventListener("click", () => {
|
||||
const collapsed = elements.inspector.classList.toggle("collapsed");
|
||||
elements["inspector-button"].setAttribute("aria-pressed", String(!collapsed));
|
||||
});
|
||||
elements["fullscreen-button"].addEventListener("click", () => {
|
||||
if (document.fullscreenElement) void document.exitFullscreen();
|
||||
else void document.documentElement.requestFullscreen();
|
||||
});
|
||||
|
||||
elements["resize-handle"].addEventListener("pointerdown", (event) => {
|
||||
if (innerWidth <= 800) return;
|
||||
elements["resize-handle"].setPointerCapture(event.pointerId);
|
||||
elements["resize-handle"].classList.add("dragging");
|
||||
});
|
||||
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));
|
||||
elements.workbench.style.setProperty("--editor-width", `${percentage}%`);
|
||||
});
|
||||
elements["resize-handle"].addEventListener("pointerup", (event) => {
|
||||
elements["resize-handle"].releasePointerCapture(event.pointerId);
|
||||
elements["resize-handle"].classList.remove("dragging");
|
||||
});
|
||||
addEventListener("popstate", () => location.reload());
|
||||
addEventListener("beforeunload", () => {
|
||||
cancelAnimationFrame(frameRequest);
|
||||
void disposeCurrent();
|
||||
});
|
||||
|
||||
try {
|
||||
const source = await loadInitialPlayground();
|
||||
await createEditor(source);
|
||||
markDirty(false);
|
||||
sampleFPS();
|
||||
await run();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
elements.editor.textContent = message;
|
||||
setRunStatus("Editor failed", "failed");
|
||||
showPreviewMessage("Editor could not start", message);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
:root {
|
||||
--ink: #10233e;
|
||||
--muted: #5c6d82;
|
||||
--line: #dfe9f2;
|
||||
--line-strong: #cbdbe9;
|
||||
--sky-25: #f8fcff;
|
||||
--sky-50: #f0f9ff;
|
||||
--sky-100: #e0f2fe;
|
||||
--sky-200: #bae6fd;
|
||||
--sky-400: #38bdf8;
|
||||
--sky-500: #0ea5e9;
|
||||
--sky-600: #0284c7;
|
||||
--blue: #1b8fe5;
|
||||
--blue-deep: #0d6fbd;
|
||||
--navy: #0a1c35;
|
||||
--radius: 16px;
|
||||
--shadow-sm: 0 1px 2px rgb(18 49 77 / 4%), 0 8px 24px rgb(18 49 77 / 5%);
|
||||
color: var(--ink);
|
||||
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { margin: 0; background: white; color: var(--ink); }
|
||||
a { color: inherit; }
|
||||
button, input { font: inherit; }
|
||||
.section-shell { width: min(1180px, calc(100% - 48px)); margin-inline: auto; }
|
||||
|
||||
.site-header {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
width: min(1240px, calc(100% - 48px));
|
||||
height: 76px;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
gap: 36px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.wordmark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
color: #102945;
|
||||
font-size: 21px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -0.04em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.wordmark-mark { position: relative; display: inline-block; width: 25px; height: 25px; transform: rotate(-2deg); }
|
||||
.wordmark-mark i { position: absolute; display: block; width: 8px; border: 2.5px solid #218ddd; border-radius: 8px; transform: rotate(-28deg); }
|
||||
.wordmark-mark i:nth-child(1) { left: 2px; top: 10px; height: 12px; }
|
||||
.wordmark-mark i:nth-child(2) { left: 9px; top: 5px; height: 17px; }
|
||||
.wordmark-mark i:nth-child(3) { left: 16px; top: 1px; height: 21px; }
|
||||
.site-nav { display: flex; flex: 1; align-items: center; justify-content: center; gap: 34px; }
|
||||
.site-nav a, .site-footer nav a { color: #53667c; font-size: 14px; font-weight: 550; text-decoration: none; }
|
||||
.site-nav a:hover, .site-footer nav a:hover { color: var(--blue); }
|
||||
.button {
|
||||
display: inline-flex;
|
||||
min-height: 46px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 0 21px;
|
||||
color: white;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 9px;
|
||||
background: var(--blue);
|
||||
box-shadow: 0 8px 20px rgb(27 143 229 / 16%);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
.button:hover { transform: translateY(-1px); background: var(--blue-deep); box-shadow: 0 11px 25px rgb(27 143 229 / 22%); }
|
||||
.button-small { min-height: 38px; padding: 0 17px; font-size: 13px; }
|
||||
.button-quiet { color: #294661; border-color: var(--line-strong); background: white; box-shadow: none; }
|
||||
.button-quiet:hover { color: var(--blue); background: var(--sky-50); box-shadow: none; }
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 660px;
|
||||
grid-template-columns: minmax(0, .9fr) minmax(540px, 1.1fr);
|
||||
align-items: center;
|
||||
gap: 66px;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 82px;
|
||||
}
|
||||
.hero::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 20px;
|
||||
left: 32%;
|
||||
width: min(600px, 68%);
|
||||
height: 480px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgb(224 242 254 / 85%), transparent 67%);
|
||||
content: "";
|
||||
filter: blur(10px);
|
||||
}
|
||||
.eyebrow { display: flex; align-items: center; gap: 9px; color: var(--sky-600); font-size: 11px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.eyebrow > span { width: 22px; height: 2px; border-radius: 2px; background: var(--sky-500); }
|
||||
.hero h1 { max-width: 670px; margin: 24px 0 24px; color: #0e2743; font-size: clamp(48px, 5.4vw, 71px); font-weight: 730; letter-spacing: -.06em; line-height: 1.02; }
|
||||
.hero-lede { max-width: 580px; margin: 0; color: var(--muted); font-size: 18px; line-height: 1.72; }
|
||||
.hero-actions { display: flex; gap: 12px; margin-top: 31px; }
|
||||
.hero-note { display: flex; align-items: center; gap: 11px; margin-top: 27px; color: #7b8b9e; font-size: 12px; }
|
||||
.browser-dots { display: flex; gap: 3px; }
|
||||
.browser-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--sky-400); }
|
||||
.browser-dots i:nth-child(2) { opacity: .55; }
|
||||
.browser-dots i:nth-child(3) { opacity: .28; }
|
||||
|
||||
.hero-demo { position: relative; overflow: hidden; min-width: 0; border: 1px solid #cee0ed; border-radius: 16px; background: #fff; box-shadow: 0 30px 70px rgb(22 66 102 / 15%), 0 4px 12px rgb(22 66 102 / 7%); transform: perspective(1400px) rotateY(-2.5deg) rotateX(1deg); }
|
||||
.hero-demo::after { position: absolute; right: -100px; bottom: -100px; width: 250px; height: 250px; border-radius: 50%; background: rgb(56 189 248 / 10%); content: ""; filter: blur(35px); }
|
||||
.demo-toolbar { display: flex; height: 44px; align-items: center; justify-content: space-between; padding: 0 16px; color: #7890a6; border-bottom: 1px solid #dbe8f1; background: #f8fbfd; font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.demo-toolbar span { display: flex; align-items: center; gap: 7px; }
|
||||
.demo-toolbar > span > i { width: 7px; height: 7px; border-radius: 2px; background: #49aef1; }
|
||||
.demo-toolbar .demo-status { color: #479165; }
|
||||
.demo-toolbar .demo-status i { border-radius: 50%; background: #38b76b; box-shadow: 0 0 0 3px rgb(56 183 107 / 12%); }
|
||||
.hero-demo pre { min-height: 355px; margin: 0; padding: 25px 27px 28px; overflow: auto; color: #405670; background: #fbfdff; font: 12px/1.7 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; tab-size: 2; }
|
||||
.syntax-purple { color: #9a61d2; }
|
||||
.syntax-blue { color: #207fca; }
|
||||
.syntax-green { color: #268a71; }
|
||||
.syntax-orange { color: #c56b2d; }
|
||||
.demo-preview { position: relative; height: 166px; overflow: hidden; border-top: 1px solid #c9d9e5; background: linear-gradient(145deg, #0a1e38, #0d294b); }
|
||||
.demo-grid { position: absolute; inset: 0; opacity: .25; background-image: linear-gradient(rgb(105 190 249 / 30%) 1px, transparent 1px), linear-gradient(90deg, rgb(105 190 249 / 30%) 1px, transparent 1px); background-size: 30px 30px; transform: perspective(400px) rotateX(64deg) scale(1.4) translateY(26px); }
|
||||
.demo-glow { position: absolute; top: 48%; left: 50%; width: 190px; height: 80px; transform: translate(-50%, -50%); border-radius: 50%; background: #3eb9ff; opacity: .3; filter: blur(35px); }
|
||||
.demo-triangle { position: absolute; top: 25px; left: 50%; width: 125px; height: 112px; transform: translateX(-50%); background: linear-gradient(150deg, #a8e5ff, #258fdf 75%); clip-path: polygon(50% 0, 100% 100%, 0 100%); filter: drop-shadow(0 16px 16px rgb(46 168 240 / 25%)); }
|
||||
.demo-preview > span { position: absolute; right: 12px; bottom: 10px; color: #7494b0; font: 9px/1 ui-monospace, monospace; letter-spacing: .08em; text-transform: uppercase; }
|
||||
|
||||
.trust-strip { display: grid; grid-template-columns: repeat(4, 1fr); border-block: 1px solid var(--line); background: var(--sky-25); }
|
||||
.trust-strip div { display: flex; min-height: 92px; align-items: center; justify-content: center; gap: 10px; border-right: 1px solid var(--line); }
|
||||
.trust-strip div:last-child { border-right: 0; }
|
||||
.trust-strip strong { color: #143757; font-size: 16px; letter-spacing: -.03em; }
|
||||
.trust-strip span { color: #718196; font-size: 12px; }
|
||||
|
||||
.progression { padding-top: 115px; padding-bottom: 125px; }
|
||||
.section-heading { max-width: 690px; margin-bottom: 50px; }
|
||||
.section-heading h2, .architecture-copy h2, .cta-section h2 { margin: 16px 0; font-size: clamp(36px, 4vw, 48px); font-weight: 700; letter-spacing: -.05em; line-height: 1.12; }
|
||||
.section-heading > p, .architecture-copy > p, .cta-section p { margin: 0; color: var(--muted); font-size: 16px; line-height: 1.75; }
|
||||
.gear-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 17px; }
|
||||
.gear-card { position: relative; min-height: 390px; padding: 29px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius); background: white; box-shadow: var(--shadow-sm); }
|
||||
.gear-card::before { position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(145deg, var(--sky-50), transparent 42%); content: ""; opacity: 0; transition: opacity .2s ease; }
|
||||
.gear-card:hover::before { opacity: 1; }
|
||||
.gear-card > * { position: relative; }
|
||||
.gear-card-featured { border-color: #b9ddf6; background: linear-gradient(145deg, #f8fcff, white); }
|
||||
.gear-number { position: absolute; top: 25px; right: 27px; color: #bdd1e0; font: 600 11px/1 ui-monospace, monospace; }
|
||||
.gear-icon { display: grid; width: 52px; height: 52px; margin: 6px 0 33px; place-items: center; border: 1px solid #c6e5f8; border-radius: 13px; background: #eff9ff; }
|
||||
.gear-card h3 { margin: 0 0 14px; color: #13324f; font-size: 20px; letter-spacing: -.025em; }
|
||||
.gear-card p { margin: 0 0 25px; color: #6a7b8e; font-size: 14px; line-height: 1.72; }
|
||||
.gear-card a { position: absolute; bottom: 28px; color: var(--blue-deep); font-size: 13px; font-weight: 700; text-decoration: none; }
|
||||
.gear-card a span { margin-left: 5px; }
|
||||
.cube-icon { width: 21px; height: 21px; border: 2px solid #258fd9; transform: rotate(30deg) skew(-4deg); }
|
||||
.rows-icon { width: 24px; height: 19px; background: repeating-linear-gradient(to bottom, #258fd9 0 2px, transparent 2px 6px); }
|
||||
.graph-icon { position: relative; width: 26px; height: 22px; background: linear-gradient(28deg, transparent 47%, #258fd9 48% 53%, transparent 54%); }
|
||||
.graph-icon::before, .graph-icon::after { position: absolute; width: 7px; height: 7px; border: 2px solid #258fd9; border-radius: 50%; background: #eff9ff; content: ""; }
|
||||
.graph-icon::before { left: 0; bottom: 0; }
|
||||
.graph-icon::after { top: 0; right: 0; }
|
||||
|
||||
.architecture-section { overflow: hidden; padding: 110px 0; color: #d7e9f8; background: radial-gradient(circle at 80% 30%, #153d67, transparent 36%), linear-gradient(145deg, #07192e, #0b2746); }
|
||||
.architecture-grid { display: grid; grid-template-columns: .9fr 1.1fr; align-items: center; gap: 100px; }
|
||||
.eyebrow-light { color: #65c9ff; }
|
||||
.architecture-copy h2, .cta-section h2 { color: #f2f9ff; }
|
||||
.architecture-copy > p { color: #9ab2c8; }
|
||||
.check-list { display: grid; gap: 14px; margin: 30px 0; padding: 0; list-style: none; }
|
||||
.check-list li { color: #bed1e2; font-size: 14px; }
|
||||
.check-list li span { display: inline-grid; width: 20px; height: 20px; margin-right: 10px; place-items: center; border-radius: 50%; color: #6dd0ff; background: rgb(56 189 248 / 12%); font-size: 10px; }
|
||||
.text-link-light { color: #6ed0ff; font-size: 13px; font-weight: 700; text-decoration: none; }
|
||||
.architecture-visual { position: relative; display: flex; min-height: 480px; flex-direction: column; align-items: center; justify-content: center; }
|
||||
.architecture-visual::before { position: absolute; width: 470px; height: 470px; border: 1px solid rgb(106 193 245 / 8%); border-radius: 50%; content: ""; box-shadow: 0 0 0 70px rgb(106 193 245 / 3%), 0 0 0 140px rgb(106 193 245 / 2%); }
|
||||
.flow-label { position: relative; margin-bottom: 11px; color: #6887a3; font: 700 9px/1 ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
|
||||
.flow-nodes { position: relative; display: flex; gap: 10px; }
|
||||
.flow-nodes span { padding: 9px 13px; color: #aac3d8; border: 1px solid #294b69; border-radius: 6px; background: #102d4b; font: 10px/1 ui-monospace, monospace; }
|
||||
.flow-line { position: relative; display: flex; width: 250px; height: 41px; justify-content: space-around; }
|
||||
.flow-line i { width: 1px; height: 100%; background: linear-gradient(#356688, #48b9f4); }
|
||||
.arena-card { position: relative; width: min(410px, 100%); padding: 20px; border: 1px solid #2f8bc0; border-radius: 12px; background: linear-gradient(120deg, #0c3254, #0d2a47); box-shadow: 0 0 40px rgb(27 143 229 / 12%); }
|
||||
.arena-card > div:first-child { display: grid; grid-template-columns: 20px 1fr; }
|
||||
.arena-card > div > span { width: 11px; height: 11px; margin-top: 3px; border: 2px solid #62c9ff; border-radius: 3px; }
|
||||
.arena-card strong { color: #d9f0ff; font: 600 13px/1.2 ui-monospace, monospace; }
|
||||
.arena-card small { grid-column: 2; margin-top: 3px; color: #6389a6; font: 9px/1 ui-monospace, monospace; }
|
||||
.arena-rows { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-top: 18px; }
|
||||
.arena-rows i { height: 30px; border: 1px solid #285f84; border-radius: 3px; background: repeating-linear-gradient(90deg, rgb(69 178 235 / 18%) 0 5px, transparent 5px 8px); }
|
||||
.flow-line-bottom { width: 1px; height: 34px; }
|
||||
.core-card { position: relative; display: flex; width: 260px; min-height: 66px; flex-direction: column; align-items: center; justify-content: center; border: 1px solid #365b7a; border-radius: 9px; background: #102b46; }
|
||||
.core-card span { color: #6c8ca8; font: 8px/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.core-card strong { margin-top: 7px; color: #c7deef; font-size: 12px; }
|
||||
.gpu-line { position: relative; width: 1px; height: 28px; background: #315a77; }
|
||||
.gpu-card { position: relative; padding: 8px 21px; color: #83d5ff; border: 1px solid #2c6f98; border-radius: 20px; background: #0c304f; font: 700 9px/1 ui-monospace, monospace; letter-spacing: .1em; }
|
||||
|
||||
.steps-section { padding-top: 115px; padding-bottom: 120px; }
|
||||
.section-heading-centered { margin-inline: auto; text-align: center; }
|
||||
.section-heading-centered .eyebrow { justify-content: center; }
|
||||
.steps { max-width: 880px; margin: 55px auto 0; padding: 0; border-top: 1px solid var(--line); list-style: none; }
|
||||
.steps li { display: grid; min-height: 126px; grid-template-columns: 46px 1fr auto; align-items: center; gap: 24px; border-bottom: 1px solid var(--line); }
|
||||
.step-index { display: grid; width: 34px; height: 34px; place-items: center; color: var(--blue); border: 1px solid #b9dcf4; border-radius: 50%; background: var(--sky-50); font: 700 11px/1 ui-monospace, monospace; }
|
||||
.steps h3 { margin: 0 0 5px; font-size: 16px; }
|
||||
.steps p { margin: 0; color: #708196; font-size: 13px; }
|
||||
.steps code { padding: 10px 13px; color: #2977ad; border: 1px solid #d9e7f1; border-radius: 6px; background: #f8fbfd; font: 11px/1 ui-monospace, monospace; }
|
||||
.cta-section { display: flex; min-height: 305px; align-items: center; justify-content: space-between; gap: 50px; margin-bottom: 90px; padding: 56px 65px; border-radius: 20px; background: radial-gradient(circle at 85% 20%, #229eea, transparent 35%), linear-gradient(125deg, #0a2542, #0c4b78); box-shadow: 0 24px 55px rgb(12 64 101 / 18%); }
|
||||
.cta-section > div { max-width: 700px; }
|
||||
.cta-section h2 { margin-block: 13px; font-size: 39px; }
|
||||
.cta-section p { color: #a7c4d9; }
|
||||
.button-white { flex: 0 0 auto; color: #155a89; border-color: white; background: white; box-shadow: none; }
|
||||
.button-white:hover { color: #0f70ae; background: #f0f9ff; box-shadow: none; }
|
||||
.site-footer { display: grid; min-height: 100px; grid-template-columns: 1fr 1fr 1fr; align-items: center; border-top: 1px solid var(--line); }
|
||||
.site-footer p { color: #8694a5; font-size: 12px; text-align: center; }
|
||||
.site-footer nav { display: flex; justify-content: flex-end; gap: 24px; }
|
||||
|
||||
[data-reveal] { opacity: 0; transform: translateY(12px); transition: opacity 500ms ease, transform 500ms ease; }
|
||||
[data-reveal].revealed { opacity: 1; transform: none; }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.hero { grid-template-columns: 1fr; padding-top: 80px; }
|
||||
.hero-copy { max-width: 700px; }
|
||||
.hero-demo { width: min(680px, 100%); transform: none; }
|
||||
.gear-grid { grid-template-columns: 1fr; }
|
||||
.gear-card { min-height: 320px; }
|
||||
.architecture-grid { grid-template-columns: 1fr; gap: 60px; }
|
||||
.architecture-copy { max-width: 700px; }
|
||||
.trust-strip { grid-template-columns: 1fr 1fr; }
|
||||
.trust-strip div:nth-child(2) { border-right: 0; }
|
||||
.trust-strip div:nth-child(-n + 2) { border-bottom: 1px solid var(--line); }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.section-shell, .site-header { width: min(100% - 28px, 1180px); }
|
||||
.site-header { height: 64px; gap: 14px; }
|
||||
.site-nav { justify-content: flex-end; gap: 16px; }
|
||||
.site-nav a:last-child { display: none; }
|
||||
.site-header > .button { display: none; }
|
||||
.hero { min-height: auto; grid-template-columns: minmax(0, 1fr); gap: 45px; padding-block: 60px; }
|
||||
.hero h1 { font-size: 47px; }
|
||||
.hero-lede { font-size: 16px; }
|
||||
.hero-actions { align-items: stretch; flex-direction: column; }
|
||||
.hero-demo pre { font-size: 10px; }
|
||||
.trust-strip { grid-template-columns: 1fr; }
|
||||
.trust-strip div { min-height: 72px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.progression, .steps-section { padding-block: 80px; }
|
||||
.architecture-section { padding-block: 80px; }
|
||||
.architecture-visual { transform: scale(.82); margin-inline: -35px; }
|
||||
.steps li { grid-template-columns: 42px 1fr; padding-block: 22px; }
|
||||
.steps code { display: none; }
|
||||
.cta-section { align-items: flex-start; flex-direction: column; margin-bottom: 45px; padding: 36px 28px; }
|
||||
.site-footer { grid-template-columns: 1fr; gap: 18px; padding-block: 28px; text-align: center; }
|
||||
.site-footer p { margin: 0; }
|
||||
.site-footer nav { justify-content: center; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const revealTargets = document.querySelectorAll(
|
||||
".section-heading, .gear-card, .architecture-copy, .architecture-visual, .steps li, .cta-section",
|
||||
);
|
||||
|
||||
if ("IntersectionObserver" in window && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
for (const target of revealTargets) target.dataset.reveal = "";
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
entry.target.classList.add("revealed");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -40px", threshold: 0.08 },
|
||||
);
|
||||
for (const target of revealTargets) observer.observe(target);
|
||||
}
|
||||
Vendored
+161
@@ -0,0 +1,161 @@
|
||||
declare module "@yawn/core" {
|
||||
export type RowFormat = "f32" | "u32" | "i32";
|
||||
export interface RowDescriptor {
|
||||
name: string;
|
||||
rows: number;
|
||||
stride: number;
|
||||
format: RowFormat;
|
||||
offset: number;
|
||||
bytes: number;
|
||||
}
|
||||
export class SharedRows {
|
||||
readonly buffer: SharedArrayBuffer;
|
||||
readonly descriptor: RowDescriptor;
|
||||
readonly name: string;
|
||||
readonly rows: number;
|
||||
readonly stride: number;
|
||||
readonly format: RowFormat;
|
||||
readonly view: Float32Array | Uint32Array | Int32Array;
|
||||
row(index: number): Float32Array | Uint32Array | Int32Array;
|
||||
read(index: number): number[];
|
||||
write(index: number, values: ArrayLike<number>): this;
|
||||
share(): { buffer: SharedArrayBuffer; descriptor: RowDescriptor };
|
||||
}
|
||||
export interface ProfileFrame {
|
||||
frame: number;
|
||||
milliseconds: number;
|
||||
readbackMilliseconds: number;
|
||||
adapter: string;
|
||||
canvas: { width: number; height: number };
|
||||
passes: Array<{ name: string; milliseconds: number }>;
|
||||
}
|
||||
export class YawnCore {
|
||||
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[]>;
|
||||
deleteRows(name: string): Promise<void>;
|
||||
allocateObject(name: string): Promise<number>;
|
||||
deleteObject(name: string, id: number): Promise<void>;
|
||||
compileGraph(serialized: string): Promise<string>;
|
||||
switchLoadout(id: string): Promise<void>;
|
||||
play(): Promise<void>;
|
||||
pause(): Promise<void>;
|
||||
setFps(fps: number): Promise<void>;
|
||||
setProfiler(enabled: boolean): Promise<boolean>;
|
||||
onProfile(listener: (frame: ProfileFrame) => void): () => void;
|
||||
array(name: string): SharedRows;
|
||||
dispose(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@yawn/handles" {
|
||||
import { SharedRows, YawnCore } from "@yawn/core";
|
||||
|
||||
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>;
|
||||
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 class Node {
|
||||
constructor(scene: Scene, options?: NodeOptions);
|
||||
readonly scene: Scene;
|
||||
readonly ready: Promise<this>;
|
||||
readonly id: number;
|
||||
readonly position: Vector3View;
|
||||
readonly rotor: RotorView;
|
||||
readonly scale: Vector3View;
|
||||
setPosition(value: ArrayLike<number>): this;
|
||||
setRotor(value: ArrayLike<number>): this;
|
||||
setScale(value: ArrayLike<number>): this;
|
||||
translate(x: number, y: number, z: number): this;
|
||||
rotateX(radians: number): this;
|
||||
rotateY(radians: number): this;
|
||||
rotateZ(radians: number): this;
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PBRMaterialOptions {
|
||||
baseColor?: ArrayLike<number>;
|
||||
metallic?: number;
|
||||
roughness?: number;
|
||||
emissive?: ArrayLike<number>;
|
||||
}
|
||||
export class PBRMaterial {
|
||||
constructor(scene: Scene, options?: PBRMaterialOptions);
|
||||
readonly ready: Promise<this>;
|
||||
readonly id: number;
|
||||
metallic: number;
|
||||
roughness: number;
|
||||
readonly baseColor: { 0: number; 1: number; 2: number; 3: number };
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
export type VertexKind = "positions" | "normals" | "tangents" | "uvs" | "colors" | "indices";
|
||||
export interface MeshOptions extends NodeOptions {
|
||||
geometryId?: number;
|
||||
material?: PBRMaterial;
|
||||
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
|
||||
visible?: boolean;
|
||||
}
|
||||
export class Mesh extends Node {
|
||||
constructor(scene: Scene, options?: MeshOptions);
|
||||
geometryId: number;
|
||||
vertexCount: number;
|
||||
indexCount: number;
|
||||
isVisible: boolean;
|
||||
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>;
|
||||
}
|
||||
|
||||
export interface CameraOptions extends NodeOptions { fov?: number; near?: number; far?: number; aspect?: number }
|
||||
export class Camera extends Node {
|
||||
fov: number;
|
||||
near: number;
|
||||
far: number;
|
||||
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 } });
|
||||
alpha: number;
|
||||
beta: number;
|
||||
radius: number;
|
||||
}
|
||||
export class FreeCamera extends Camera {
|
||||
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 });
|
||||
}
|
||||
|
||||
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 function importGltf(scene: Scene, url: string | URL): Promise<Mesh[]>;
|
||||
}
|
||||
|
||||
declare const canvas: HTMLCanvasElement;
|
||||
declare function log(...values: unknown[]): void;
|
||||
Reference in New Issue
Block a user