page.ts
examples/spark/js/page.ts · raw · open the example · all examples · site home
Exports: examplePagewindowRenderer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
// The page around a Spark port: error capture, a status line, a legend, the
// footer (backend, links) and window.example, which tells the smoke check
// (scripts/check-examples.mjs) when the page has drawn its content
// ({ ready: true }) or failed ({ error }). windowRenderer makes the
// renderer every port starts with.
import type * as THREE from "three";
import type { WebGPURenderer } from "three/webgpu";
import { createRenderer, getGpuCaps } from "@sparkjsdev/radix";
declare global {
interface Window {
example: { ready: boolean; error: string | undefined };
}
}
window.example = { ready: false, error: undefined };
const statusLine = document.createElement("div");
statusLine.id = "example-status";
document.body.append(statusLine);
function status(text: string) {
statusLine.textContent = text;
statusLine.classList.toggle("empty", !text);
}
function fail(error: unknown) {
const err = error as Error | undefined;
window.example.error = err?.stack ?? err?.message ?? String(error);
status(`Error: ${err?.message ?? error}`);
statusLine.classList.add("error");
console.error(error);
}
// Errors anywhere, the animation loop's included
addEventListener("error", (event) => fail(event.error ?? event.message));
addEventListener("unhandledrejection", (event) => fail(event.reason));
// `name` is the Spark example's folder (footer links); `note` a footer
// line saying what the port leaves out
export function examplePage(name: string, { note }: { note?: string } = {}) {
return {
status,
fail,
// A line of instructions at the top of the page
legend(html: string) {
const element = document.createElement("div");
element.className = "example-legend";
element.innerHTML = html;
document.body.append(element);
return element;
},
// An onProgress for loading that shows `label` and the stage
progress(label: string) {
return (fraction: number, stage: string) => status(`${label}: ${stage} ${Math.round(fraction * 100)}%`);
},
// The page shows what it loaded: the footer, and a few frames later the
// smoke check's signal (later calls only clear the status)
ready(renderer: WebGPURenderer) {
status("");
if (document.getElementById("example-footer")) {
return;
}
footer(name, getGpuCaps(renderer).backend === "webgpu", note);
let frames = 5;
const tick = () => (--frames > 0 ? requestAnimationFrame(tick) : (window.example.ready = true));
requestAnimationFrame(tick);
},
};
}
// The backend, a link to the other one, the Spark original and the index
function footer(name: string, webgpu: boolean, note?: string) {
const params = new URLSearchParams(location.search);
params.set("gpu", webgpu ? "webgpu-gl" : "webgpu");
const element = document.createElement("div");
element.id = "example-footer";
element.innerHTML = [
`<b>${webgpu ? "WebGPU" : "WebGL2"}</b>`,
`<a href="?${params}">${webgpu ? "WebGL2" : "WebGPU"}</a>`,
`<a href="https://sparkjs.dev/examples/#${name}" target="_blank">Spark original</a>`,
"<a href=\"../index.html\">all ports</a>",
note ? `<span>${note}</span>` : "",
].join(" ");
document.body.append(element);
}
type RendererOptions = NonNullable<Parameters<typeof createRenderer>[0]>;
// createRenderer (WebGPU, or the WebGL2 backend with ?gpu=webgpu-gl), sized
// to the window and added to the page; it follows window resizes, and so
// does the camera's aspect. pixelRatio: only where the Spark example sets
// one (else three's 1).
export async function windowRenderer(camera: THREE.PerspectiveCamera, { pixelRatio, ...options }: RendererOptions & { pixelRatio?: number } = {}) {
const renderer = (await createRenderer(options)) as WebGPURenderer;
if (pixelRatio !== undefined) {
renderer.setPixelRatio(pixelRatio);
}
renderer.setSize(innerWidth, innerHeight);
document.body.append(renderer.domElement);
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
return renderer;
}