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:
@@ -19,8 +19,9 @@ npm run server
|
|||||||
```
|
```
|
||||||
|
|
||||||
The server rebuilds `@yawn/core` and `@yawn/handles` at startup and serves the public CDN modules at
|
The server rebuilds `@yawn/core` and `@yawn/handles` at startup and serves the public CDN modules at
|
||||||
`https://yawn.heaust.org/pkg/core.js` and `https://yawn.heaust.org/pkg/handles.js`. Their workers and
|
`https://yawn.heaust.org/pkg/core.js` and `https://yawn.heaust.org/pkg/handles.js`. Handles keeps Core
|
||||||
WASM stay on the CDN automatically, so applications only import the module URLs. The complete editor is at `/playground`; saved revisions use
|
as a separate module dependency instead of bundling it. Map and import both URLs as shown in
|
||||||
|
`server/examples/basic.html`; their workers and WASM stay on the CDN automatically. The complete editor is at `/playground`; saved revisions use
|
||||||
`/playground/{id}/{revision}` URLs. Set `PORT` or `YAWN_DATABASE` to override the default port and
|
`/playground/{id}/{revision}` URLs. Set `PORT` or `YAWN_DATABASE` to override the default port and
|
||||||
`server/data/yawn.db` database path.
|
`server/data/yawn.db` database path.
|
||||||
|
|
||||||
|
|||||||
@@ -62,10 +62,14 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|||||||
if (id.x != 0u) { return; }
|
if (id.x != 0u) { return; }
|
||||||
var count = 0u;
|
var count = 0u;
|
||||||
var light = vec3<f32>(0.0);
|
var light = vec3<f32>(0.0);
|
||||||
|
var pointLight = vec3<f32>(0.0);
|
||||||
|
var spotLight = vec3<f32>(0.0);
|
||||||
for (var i = 0u; i < arrayLength(&pointLights) / 2u; i++) {
|
for (var i = 0u; i < arrayLength(&pointLights) / 2u; i++) {
|
||||||
let enabled = pointLights[i * 2u + 1u].y;
|
let enabled = pointLights[i * 2u + 1u].y;
|
||||||
count += select(0u, 1u, enabled != 0.0);
|
count += select(0u, 1u, enabled != 0.0);
|
||||||
light += pointLights[i * 2u].rgb * pointLights[i * 2u].a * enabled * 0.02;
|
let contribution = pointLights[i * 2u].rgb * pointLights[i * 2u].a * enabled * 0.02;
|
||||||
|
light += contribution;
|
||||||
|
pointLight += contribution;
|
||||||
}
|
}
|
||||||
for (var i = 0u; i < arrayLength(&rectLights) / 3u; i++) {
|
for (var i = 0u; i < arrayLength(&rectLights) / 3u; i++) {
|
||||||
let enabled = rectLights[i * 3u + 1u].z;
|
let enabled = rectLights[i * 3u + 1u].z;
|
||||||
@@ -75,7 +79,9 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|||||||
for (var i = 0u; i < arrayLength(&spotLights) / 2u; i++) {
|
for (var i = 0u; i < arrayLength(&spotLights) / 2u; i++) {
|
||||||
let enabled = spotLights[i * 2u + 1u].w;
|
let enabled = spotLights[i * 2u + 1u].w;
|
||||||
count += select(0u, 1u, enabled != 0.0);
|
count += select(0u, 1u, enabled != 0.0);
|
||||||
light += spotLights[i * 2u].rgb * spotLights[i * 2u].a * enabled * 0.02;
|
let contribution = spotLights[i * 2u].rgb * spotLights[i * 2u].a * enabled * 0.02;
|
||||||
|
light += contribution;
|
||||||
|
spotLight += contribution;
|
||||||
}
|
}
|
||||||
for (var i = 0u; i < arrayLength(&directionalLights) / 2u; i++) {
|
for (var i = 0u; i < arrayLength(&directionalLights) / 2u; i++) {
|
||||||
let enabled = directionalLights[i * 2u + 1u].x;
|
let enabled = directionalLights[i * 2u + 1u].x;
|
||||||
@@ -90,6 +96,12 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|||||||
clusters[1] = bitcast<u32>(light.r);
|
clusters[1] = bitcast<u32>(light.r);
|
||||||
clusters[2] = bitcast<u32>(light.g);
|
clusters[2] = bitcast<u32>(light.g);
|
||||||
clusters[3] = bitcast<u32>(light.b);
|
clusters[3] = bitcast<u32>(light.b);
|
||||||
|
clusters[4] = bitcast<u32>(pointLight.r);
|
||||||
|
clusters[5] = bitcast<u32>(pointLight.g);
|
||||||
|
clusters[6] = bitcast<u32>(pointLight.b);
|
||||||
|
clusters[7] = bitcast<u32>(spotLight.r);
|
||||||
|
clusters[8] = bitcast<u32>(spotLight.g);
|
||||||
|
clusters[9] = bitcast<u32>(spotLight.b);
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const basicForwardShader = /* wgsl */ `
|
const basicForwardShader = /* wgsl */ `
|
||||||
@@ -173,6 +185,8 @@ struct VertexOutput {
|
|||||||
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
||||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||||
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
||||||
|
@group(0) @binding(8) var<storage, read> pointLights: array<vec4<f32>>;
|
||||||
|
@group(0) @binding(9) var<storage, read> spotLights: array<vec4<f32>>;
|
||||||
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
|
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
|
||||||
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
|
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
|
||||||
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
|
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
|
||||||
@@ -235,10 +249,38 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
let f0 = mix(vec3(0.04), base.rgb, metallic);
|
let f0 = mix(vec3(0.04), base.rgb, metallic);
|
||||||
let specular = f0 * pow(nDotH, max(2.0, 2.0 / (roughness * roughness) - 2.0));
|
let specular = f0 * pow(nDotH, max(2.0, 2.0 / (roughness * roughness) - 2.0));
|
||||||
let clusterLight = vec3<f32>(bitcast<f32>(clusters[1]), bitcast<f32>(clusters[2]), bitcast<f32>(clusters[3]));
|
let clusterLight = vec3<f32>(bitcast<f32>(clusters[1]), bitcast<f32>(clusters[2]), bitcast<f32>(clusters[3]));
|
||||||
let ambient = vec3(0.08) + clusterLight;
|
let pointAmbient = vec3<f32>(bitcast<f32>(clusters[4]), bitcast<f32>(clusters[5]), bitcast<f32>(clusters[6]));
|
||||||
let diffuse = base.rgb * (1.0 - metallic) * nDotL;
|
let spotAmbient = vec3<f32>(bitcast<f32>(clusters[7]), bitcast<f32>(clusters[8]), bitcast<f32>(clusters[9]));
|
||||||
|
let ambient = vec3(0.08) + max(clusterLight - pointAmbient - spotAmbient, vec3(0.0));
|
||||||
|
var pointDiffuse = vec3<f32>(0.0);
|
||||||
|
for (var i = 0u; i < arrayLength(&pointLights) / 2u; i++) {
|
||||||
|
let colorIntensity = pointLights[i * 2u];
|
||||||
|
let state = pointLights[i * 2u + 1u];
|
||||||
|
if (state.y == 0.0 || state.x <= 0.0) { continue; }
|
||||||
|
let offset = positions[i].xyz - input.world;
|
||||||
|
let distance = length(offset);
|
||||||
|
let attenuation = pow(max(1.0 - distance / state.x, 0.0), 2.0);
|
||||||
|
let incidence = max(dot(normal, offset / max(distance, 0.0001)), 0.0);
|
||||||
|
pointDiffuse += colorIntensity.rgb * colorIntensity.a * attenuation * incidence * 0.08;
|
||||||
|
}
|
||||||
|
var spotDiffuse = vec3<f32>(0.0);
|
||||||
|
for (var i = 0u; i < arrayLength(&spotLights) / 2u; i++) {
|
||||||
|
let colorIntensity = spotLights[i * 2u];
|
||||||
|
let state = spotLights[i * 2u + 1u];
|
||||||
|
if (state.w == 0.0 || state.x <= 0.0) { continue; }
|
||||||
|
let fromLight = input.world - positions[i].xyz;
|
||||||
|
let distance = length(fromLight);
|
||||||
|
let lightDirection = normalize(rotate(rotors[i], vec3<f32>(0.0, 0.0, -1.0)));
|
||||||
|
let cone = smoothstep(cos(state.z), cos(state.y), dot(lightDirection, fromLight / max(distance, 0.0001)));
|
||||||
|
let attenuation = pow(max(1.0 - distance / state.x, 0.0), 2.0);
|
||||||
|
let incidence = max(dot(normal, -fromLight / max(distance, 0.0001)), 0.0);
|
||||||
|
spotDiffuse += colorIntensity.rgb * colorIntensity.a * cone * attenuation * incidence * 0.08;
|
||||||
|
}
|
||||||
|
let defaultLight = select(1.0, 0.18, clusters[0] != 0u);
|
||||||
|
let diffuse = base.rgb * (1.0 - metallic) * nDotL * defaultLight;
|
||||||
let emissive = vec3(properties.z, properties.w, extra.x);
|
let emissive = vec3(properties.z, properties.w, extra.x);
|
||||||
return vec4((base.rgb * ambient + diffuse + specular * nDotL + emissive) * accent.a, base.a);
|
let localDiffuse = base.rgb * (1.0 - metallic) * (pointDiffuse + spotDiffuse);
|
||||||
|
return vec4((base.rgb * ambient + diffuse + localDiffuse + specular * nDotL * defaultLight + emissive) * accent.a, base.a);
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,6 +1091,12 @@ export class Scene {
|
|||||||
binding,
|
binding,
|
||||||
resource,
|
resource,
|
||||||
})),
|
})),
|
||||||
|
...(detailed
|
||||||
|
? [
|
||||||
|
{ group: 0, binding: 8, resource: "point-lights" },
|
||||||
|
{ group: 0, binding: 9, resource: "spot-lights" },
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(detailed && mask
|
...(detailed && mask
|
||||||
? [
|
? [
|
||||||
{ group: 1, binding: 0, resource: "material-linear" },
|
{ group: 1, binding: 0, resource: "material-linear" },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from "./Scene";
|
|||||||
export * from "./ComputePass";
|
export * from "./ComputePass";
|
||||||
export * from "./Node";
|
export * from "./Node";
|
||||||
export * from "./Mesh";
|
export * from "./Mesh";
|
||||||
|
export * from "./shapes";
|
||||||
export * from "./camera/Camera";
|
export * from "./camera/Camera";
|
||||||
export * from "./camera/ArcRotateCamera";
|
export * from "./camera/ArcRotateCamera";
|
||||||
export * from "./camera/FreeCamera";
|
export * from "./camera/FreeCamera";
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Mesh, type MeshOptions } from "../Mesh";
|
||||||
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
export type CubeOptions = Omit<MeshOptions, "geometryId" | "vertexData"> & {
|
||||||
|
size?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
depth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function dimension(value: number, name: string) {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) throw new RangeError(name);
|
||||||
|
return value / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A six-sided mesh with independent vertices and normals for every face. */
|
||||||
|
export class Cube extends Mesh {
|
||||||
|
constructor(scene: Scene, options: CubeOptions = {}) {
|
||||||
|
const {
|
||||||
|
size = 1,
|
||||||
|
width = size,
|
||||||
|
height = size,
|
||||||
|
depth = size,
|
||||||
|
...meshOptions
|
||||||
|
} = options;
|
||||||
|
const x = dimension(width, "width");
|
||||||
|
const y = dimension(height, "height");
|
||||||
|
const z = dimension(depth, "depth");
|
||||||
|
const positions = [
|
||||||
|
[-x, -y, z, x, -y, z, x, y, z, -x, y, z],
|
||||||
|
[x, -y, -z, -x, -y, -z, -x, y, -z, x, y, -z],
|
||||||
|
[x, -y, z, x, -y, -z, x, y, -z, x, y, z],
|
||||||
|
[-x, -y, -z, -x, -y, z, -x, y, z, -x, y, -z],
|
||||||
|
[-x, y, z, x, y, z, x, y, -z, -x, y, -z],
|
||||||
|
[-x, -y, -z, x, -y, -z, x, -y, z, -x, -y, z],
|
||||||
|
].flat();
|
||||||
|
const normals = [
|
||||||
|
[0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
|
||||||
|
[0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1],
|
||||||
|
[1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
|
||||||
|
[-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0],
|
||||||
|
[0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
|
||||||
|
[0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0],
|
||||||
|
].flat();
|
||||||
|
const uvs = Array.from({ length: 6 }, () => [
|
||||||
|
0, 0, 1, 0, 1, 1, 0, 1,
|
||||||
|
]).flat();
|
||||||
|
const indices = Array.from({ length: 6 }, (_, face) => {
|
||||||
|
const vertex = face * 4;
|
||||||
|
return [vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3];
|
||||||
|
}).flat();
|
||||||
|
super(scene, {
|
||||||
|
...meshOptions,
|
||||||
|
vertexData: { positions, normals, uvs, indices },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Mesh, type MeshOptions } from "../Mesh";
|
||||||
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
export type IcosphereOptions = Omit<
|
||||||
|
MeshOptions,
|
||||||
|
"geometryId" | "vertexData"
|
||||||
|
> & {
|
||||||
|
radius?: number;
|
||||||
|
subdivisions?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalized(vertex: number[]) {
|
||||||
|
const length = Math.hypot(...vertex);
|
||||||
|
return vertex.map((lane) => lane / length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A sphere produced by recursively subdividing an icosahedron. */
|
||||||
|
export class Icosphere extends Mesh {
|
||||||
|
constructor(scene: Scene, options: IcosphereOptions = {}) {
|
||||||
|
const { radius = 0.5, subdivisions = 2, ...meshOptions } = options;
|
||||||
|
if (!Number.isFinite(radius) || radius <= 0) throw new RangeError("radius");
|
||||||
|
if (!Number.isInteger(subdivisions) || subdivisions < 0 || subdivisions > 6)
|
||||||
|
throw new RangeError("subdivisions");
|
||||||
|
|
||||||
|
const golden = (1 + Math.sqrt(5)) / 2;
|
||||||
|
const vertices = [
|
||||||
|
[-1, golden, 0],
|
||||||
|
[1, golden, 0],
|
||||||
|
[-1, -golden, 0],
|
||||||
|
[1, -golden, 0],
|
||||||
|
[0, -1, golden],
|
||||||
|
[0, 1, golden],
|
||||||
|
[0, -1, -golden],
|
||||||
|
[0, 1, -golden],
|
||||||
|
[golden, 0, -1],
|
||||||
|
[golden, 0, 1],
|
||||||
|
[-golden, 0, -1],
|
||||||
|
[-golden, 0, 1],
|
||||||
|
].map(normalized);
|
||||||
|
let faces = [
|
||||||
|
[0, 11, 5],
|
||||||
|
[0, 5, 1],
|
||||||
|
[0, 1, 7],
|
||||||
|
[0, 7, 10],
|
||||||
|
[0, 10, 11],
|
||||||
|
[1, 5, 9],
|
||||||
|
[5, 11, 4],
|
||||||
|
[11, 10, 2],
|
||||||
|
[10, 7, 6],
|
||||||
|
[7, 1, 8],
|
||||||
|
[3, 9, 4],
|
||||||
|
[3, 4, 2],
|
||||||
|
[3, 2, 6],
|
||||||
|
[3, 6, 8],
|
||||||
|
[3, 8, 9],
|
||||||
|
[4, 9, 5],
|
||||||
|
[2, 4, 11],
|
||||||
|
[6, 2, 10],
|
||||||
|
[8, 6, 7],
|
||||||
|
[9, 8, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let level = 0; level < subdivisions; level++) {
|
||||||
|
const midpoints = new Map<string, number>();
|
||||||
|
const midpoint = (left: number, right: number) => {
|
||||||
|
const key = left < right ? `${left}:${right}` : `${right}:${left}`;
|
||||||
|
const existing = midpoints.get(key);
|
||||||
|
if (existing !== undefined) return existing;
|
||||||
|
const vertex = normalized(
|
||||||
|
vertices[left].map((lane, index) => lane + vertices[right][index]),
|
||||||
|
);
|
||||||
|
const index = vertices.push(vertex) - 1;
|
||||||
|
midpoints.set(key, index);
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
faces = faces.flatMap(([a, b, c]) => {
|
||||||
|
const ab = midpoint(a, b);
|
||||||
|
const bc = midpoint(b, c);
|
||||||
|
const ca = midpoint(c, a);
|
||||||
|
return [
|
||||||
|
[a, ab, ca],
|
||||||
|
[b, bc, ab],
|
||||||
|
[c, ca, bc],
|
||||||
|
[ab, bc, ca],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const normals = vertices.flat();
|
||||||
|
const positions = normals.map((lane) => lane * radius);
|
||||||
|
const uvs = vertices.flatMap(([x, y, z]) => [
|
||||||
|
0.5 + Math.atan2(z, x) / (Math.PI * 2),
|
||||||
|
0.5 - Math.asin(y) / Math.PI,
|
||||||
|
]);
|
||||||
|
super(scene, {
|
||||||
|
...meshOptions,
|
||||||
|
vertexData: { positions, normals, uvs, indices: faces.flat() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Mesh, type MeshOptions } from "../Mesh";
|
||||||
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
export type PlaneOptions = Omit<MeshOptions, "geometryId" | "vertexData"> & {
|
||||||
|
size?: number;
|
||||||
|
width?: number;
|
||||||
|
depth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function dimension(value: number, name: string) {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) throw new RangeError(name);
|
||||||
|
return value / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A horizontal, upward-facing plane centered on its transform. */
|
||||||
|
export class Plane extends Mesh {
|
||||||
|
constructor(scene: Scene, options: PlaneOptions = {}) {
|
||||||
|
const { size = 1, width = size, depth = size, ...meshOptions } = options;
|
||||||
|
const x = dimension(width, "width");
|
||||||
|
const z = dimension(depth, "depth");
|
||||||
|
super(scene, {
|
||||||
|
...meshOptions,
|
||||||
|
vertexData: {
|
||||||
|
positions: [-x, 0, -z, x, 0, -z, x, 0, z, -x, 0, z],
|
||||||
|
normals: [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
|
||||||
|
uvs: [0, 0, 1, 0, 1, 1, 0, 1],
|
||||||
|
indices: [0, 2, 1, 0, 3, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Mesh, type MeshOptions } from "../Mesh";
|
||||||
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
export type SphereOptions = Omit<MeshOptions, "geometryId" | "vertexData"> & {
|
||||||
|
radius?: number;
|
||||||
|
segments?: number;
|
||||||
|
rings?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function count(value: number, minimum: number, name: string) {
|
||||||
|
if (!Number.isInteger(value) || value < minimum) throw new RangeError(name);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A UV sphere with configurable horizontal segments and vertical rings. */
|
||||||
|
export class Sphere extends Mesh {
|
||||||
|
constructor(scene: Scene, options: SphereOptions = {}) {
|
||||||
|
const { radius = 0.5, segments = 32, rings = 16, ...meshOptions } = options;
|
||||||
|
if (!Number.isFinite(radius) || radius <= 0) throw new RangeError("radius");
|
||||||
|
const columns = count(segments, 3, "segments");
|
||||||
|
const rows = count(rings, 2, "rings");
|
||||||
|
const positions: number[] = [];
|
||||||
|
const normals: number[] = [];
|
||||||
|
const uvs: number[] = [];
|
||||||
|
const indices: number[] = [];
|
||||||
|
|
||||||
|
for (let ring = 0; ring <= rows; ring++) {
|
||||||
|
const vertical = ring / rows;
|
||||||
|
const theta = vertical * Math.PI;
|
||||||
|
const y = Math.cos(theta);
|
||||||
|
const scale = Math.sin(theta);
|
||||||
|
for (let segment = 0; segment <= columns; segment++) {
|
||||||
|
const horizontal = segment / columns;
|
||||||
|
const phi = horizontal * Math.PI * 2;
|
||||||
|
const x = scale * Math.cos(phi);
|
||||||
|
const z = scale * Math.sin(phi);
|
||||||
|
normals.push(x, y, z);
|
||||||
|
positions.push(x * radius, y * radius, z * radius);
|
||||||
|
uvs.push(horizontal, 1 - vertical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let ring = 0; ring < rows; ring++) {
|
||||||
|
for (let segment = 0; segment < columns; segment++) {
|
||||||
|
const current = ring * (columns + 1) + segment;
|
||||||
|
const next = current + columns + 1;
|
||||||
|
indices.push(current, current + 1, next, current + 1, next + 1, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super(scene, {
|
||||||
|
...meshOptions,
|
||||||
|
vertexData: { positions, normals, uvs, indices },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./Cube";
|
||||||
|
export * from "./Icosphere";
|
||||||
|
export * from "./Plane";
|
||||||
|
export * from "./Sphere";
|
||||||
+19
-7
@@ -4,7 +4,8 @@ import process from "node:process";
|
|||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
|
|
||||||
const [root, output] = process.argv.slice(2);
|
const [root, output] = process.argv.slice(2);
|
||||||
if (!root || !output) throw new Error("usage: node build.mjs <repository> <output>");
|
if (!root || !output)
|
||||||
|
throw new Error("usage: node build.mjs <repository> <output>");
|
||||||
|
|
||||||
const browser = {
|
const browser = {
|
||||||
bundle: true,
|
bundle: true,
|
||||||
@@ -17,8 +18,14 @@ const browser = {
|
|||||||
|
|
||||||
const workerFiles = new Map([
|
const workerFiles = new Map([
|
||||||
[path.join(root, "core", "index.js"), "./worker.js"],
|
[path.join(root, "core", "index.js"), "./worker.js"],
|
||||||
[path.join(root, "addons", "handles", "src", "importers", "gltf.ts"), "./importer-worker.js"],
|
[
|
||||||
[path.join(root, "addons", "handles", "src", "bvh", "picking.ts"), "./bvh-worker.js"],
|
path.join(root, "addons", "handles", "src", "importers", "gltf.ts"),
|
||||||
|
"./importer-worker.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
path.join(root, "addons", "handles", "src", "bvh", "picking.ts"),
|
||||||
|
"./bvh-worker.js",
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const workerURLs = {
|
const workerURLs = {
|
||||||
@@ -32,7 +39,8 @@ const workerURLs = {
|
|||||||
/new Worker\(new URL\("\.\/worker\.(?:js|ts)", import\.meta\.url\),/,
|
/new Worker\(new URL\("\.\/worker\.(?:js|ts)", import\.meta\.url\),/,
|
||||||
`createYawnWorker(new URL(${JSON.stringify(worker)}, import.meta.url),`,
|
`createYawnWorker(new URL(${JSON.stringify(worker)}, import.meta.url),`,
|
||||||
);
|
);
|
||||||
if (transformed === contents) throw new Error(`worker constructor not found in ${source}`);
|
if (transformed === contents)
|
||||||
|
throw new Error(`worker constructor not found in ${source}`);
|
||||||
contents = `
|
contents = `
|
||||||
function createYawnWorker(url, options) {
|
function createYawnWorker(url, options) {
|
||||||
if (url.origin === globalThis.location.origin) return new Worker(url, options);
|
if (url.origin === globalThis.location.origin) return new Worker(url, options);
|
||||||
@@ -57,8 +65,8 @@ await Promise.all([
|
|||||||
}),
|
}),
|
||||||
build({
|
build({
|
||||||
...browser,
|
...browser,
|
||||||
alias: { "@yawn/core": path.join(root, "core", "index.js") },
|
|
||||||
entryPoints: [path.join(root, "addons", "handles", "src", "index.ts")],
|
entryPoints: [path.join(root, "addons", "handles", "src", "index.ts")],
|
||||||
|
external: ["@yawn/core"],
|
||||||
outfile: path.join(output, "handles.js"),
|
outfile: path.join(output, "handles.js"),
|
||||||
plugins: [workerURLs],
|
plugins: [workerURLs],
|
||||||
}),
|
}),
|
||||||
@@ -73,12 +81,16 @@ await Promise.all([
|
|||||||
}),
|
}),
|
||||||
build({
|
build({
|
||||||
...browser,
|
...browser,
|
||||||
entryPoints: [path.join(root, "addons", "handles", "src", "importers", "worker.ts")],
|
entryPoints: [
|
||||||
|
path.join(root, "addons", "handles", "src", "importers", "worker.ts"),
|
||||||
|
],
|
||||||
outfile: path.join(output, "importer-worker.js"),
|
outfile: path.join(output, "importer-worker.js"),
|
||||||
}),
|
}),
|
||||||
build({
|
build({
|
||||||
...browser,
|
...browser,
|
||||||
entryPoints: [path.join(root, "addons", "handles", "src", "bvh", "worker.ts")],
|
entryPoints: [
|
||||||
|
path.join(root, "addons", "handles", "src", "bvh", "worker.ts"),
|
||||||
|
],
|
||||||
outfile: path.join(output, "bvh-worker.js"),
|
outfile: path.join(output, "bvh-worker.js"),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!-- The host serving this page must send COOP: same-origin and COEP: require-corp headers. -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Yawn CDN example</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #15304a;
|
||||||
|
background: #f4faff;
|
||||||
|
font:
|
||||||
|
16px/1.5 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
width: min(720px, calc(100% - 32px));
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@yawn/core": "https://yawn.heaust.org/pkg/core.js",
|
||||||
|
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<canvas id="view" width="1280" height="720"></canvas>
|
||||||
|
<p id="status">Starting Yawn…</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { YawnCore } from "@yawn/core";
|
||||||
|
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||||
|
|
||||||
|
const canvas = document.querySelector("#view");
|
||||||
|
const status = document.querySelector("#status");
|
||||||
|
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
||||||
|
await scene.ready;
|
||||||
|
|
||||||
|
if (!(scene.core instanceof YawnCore)) {
|
||||||
|
throw new Error(
|
||||||
|
"Handles did not use the separately imported Core module.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const material = new PBRMaterial(scene, {
|
||||||
|
baseColor: [0.12, 0.58, 1, 1],
|
||||||
|
metallic: 0.15,
|
||||||
|
roughness: 0.35,
|
||||||
|
});
|
||||||
|
await material.ready;
|
||||||
|
|
||||||
|
const triangle = 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 triangle.ready;
|
||||||
|
|
||||||
|
status.textContent = "Scene ready — Core and Handles loaded separately.";
|
||||||
|
document.documentElement.dataset.ready = "true";
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+252
-72
@@ -1,60 +1,98 @@
|
|||||||
const defaultSource = `import {
|
const defaultSource = `import {
|
||||||
|
AmbientLight,
|
||||||
ArcRotateCamera,
|
ArcRotateCamera,
|
||||||
ColorGrading,
|
ColorGrading,
|
||||||
FXAA,
|
FXAA,
|
||||||
Mesh,
|
Icosphere,
|
||||||
PBRMaterial,
|
PBRMaterial,
|
||||||
|
Plane,
|
||||||
PointLight,
|
PointLight,
|
||||||
Scene,
|
Scene,
|
||||||
|
SpotLight,
|
||||||
} from "@yawn/handles";
|
} from "@yawn/handles";
|
||||||
|
|
||||||
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
||||||
await scene.ready;
|
await scene.ready;
|
||||||
|
await scene.reserve({ nodes: 6, materials: 2 });
|
||||||
|
|
||||||
const material = new PBRMaterial(scene, {
|
const objectMaterial = new PBRMaterial(scene, {
|
||||||
baseColor: [0.12, 0.58, 1, 1],
|
baseColor: [0.82, 0.86, 0.9, 1],
|
||||||
metallic: 0.15,
|
metallic: 0.12,
|
||||||
roughness: 0.35,
|
roughness: 0.32,
|
||||||
});
|
});
|
||||||
await material.ready;
|
const groundMaterial = new PBRMaterial(scene, {
|
||||||
|
baseColor: [0.24, 0.28, 0.34, 1],
|
||||||
const mesh = new Mesh(scene, {
|
metallic: 0,
|
||||||
material,
|
roughness: 0.82,
|
||||||
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;
|
await Promise.all([objectMaterial.ready, groundMaterial.ready]);
|
||||||
|
|
||||||
log("Scene ready — drag your pointer across the preview.");
|
const icosphere = new Icosphere(scene, {
|
||||||
const move = (event: PointerEvent) => {
|
material: objectMaterial,
|
||||||
const bounds = canvas.getBoundingClientRect();
|
subdivisions: 2,
|
||||||
mesh.position.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.2;
|
position: [0, 0.2, 0],
|
||||||
mesh.position.y = (0.5 - (event.clientY - bounds.top) / bounds.height) * 0.8;
|
});
|
||||||
};
|
const ground = new Plane(scene, {
|
||||||
canvas.addEventListener("pointermove", move);
|
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 {
|
export default {
|
||||||
scene,
|
scene,
|
||||||
mesh,
|
icosphere,
|
||||||
|
ground,
|
||||||
|
lights,
|
||||||
dispose() {
|
dispose() {
|
||||||
canvas.removeEventListener("pointermove", move);
|
camera.detachControls();
|
||||||
scene.dispose();
|
scene.dispose();
|
||||||
},
|
},
|
||||||
};`;
|
};`;
|
||||||
|
|
||||||
const snippets = {
|
const snippets = {
|
||||||
camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
|
camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
|
||||||
const camera = new ArcRotateCamera(scene, {
|
const orbitCamera = new ArcRotateCamera(scene, {
|
||||||
target: mesh,
|
target: icosphere,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
beta: Math.PI / 2,
|
beta: Math.PI / 2,
|
||||||
radius: 3,
|
radius: 3,
|
||||||
controls: { element: canvas, pointer: true },
|
controls: { element: canvas, pointer: true },
|
||||||
});
|
});
|
||||||
await camera.ready;`,
|
await orbitCamera.ready;`,
|
||||||
light: `// A warm point light backed by shared rows.
|
light: `// A warm point light backed by shared rows.
|
||||||
const light = new PointLight(scene, {
|
const light = new PointLight(scene, {
|
||||||
position: [0, 0.7, 1],
|
position: [0, 0.7, 1],
|
||||||
@@ -65,7 +103,7 @@ const light = new PointLight(scene, {
|
|||||||
await light.ready;`,
|
await light.ready;`,
|
||||||
instances: `// Clones share geometry until one clone changes its vertex data.
|
instances: `// Clones share geometry until one clone changes its vertex data.
|
||||||
for (let x = -2; x <= 2; x++) {
|
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;
|
await instance.ready;
|
||||||
}`,
|
}`,
|
||||||
rows: `// Application-owned hot data in the same shared arena.
|
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 AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||||
const packages = Promise.all([import("/pkg/handles.js"), import("/pkg/core.js")]).then(
|
const nativeSetTimeout = window.setTimeout.bind(window);
|
||||||
([handles, core]) => ({ ...handles, ...core }),
|
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 monaco;
|
||||||
let editor;
|
let editor;
|
||||||
let model;
|
let model;
|
||||||
@@ -143,8 +231,14 @@ let frameRequest;
|
|||||||
let sampledFrame = 0;
|
let sampledFrame = 0;
|
||||||
let sampledAt = 0;
|
let sampledAt = 0;
|
||||||
|
|
||||||
|
addEventListener("yawn-theme-change", (event) => {
|
||||||
|
monaco?.editor.setTheme(event.detail.resolved === "dark" ? "yawn" : "vs");
|
||||||
|
});
|
||||||
|
|
||||||
function savedLocation() {
|
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;
|
return match ? { id: match[1], revision: Number(match[2]) } : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +246,11 @@ async function loadInitialPlayground() {
|
|||||||
const saved = savedLocation();
|
const saved = savedLocation();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/playgrounds/${saved.id}/${saved.revision}`);
|
const response = await fetch(
|
||||||
if (!response.ok) throw new Error("This saved playground could not be found.");
|
`/api/playgrounds/${saved.id}/${saved.revision}`,
|
||||||
|
);
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error("This saved playground could not be found.");
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
currentID = result.id;
|
currentID = result.id;
|
||||||
currentRevision = result.revision;
|
currentRevision = result.revision;
|
||||||
@@ -188,13 +285,19 @@ importScripts(${JSON.stringify(`${monacoBase}vs/base/worker/workerMain.js`)});
|
|||||||
);
|
);
|
||||||
window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
|
window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
|
||||||
window.require.config({ paths: { vs: `${monacoBase}vs` } });
|
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) {
|
async function createEditor(source) {
|
||||||
monaco = await loadMonaco();
|
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;
|
const defaults = monaco.languages.typescript.typescriptDefaults;
|
||||||
defaults.setEagerModelSync(true);
|
defaults.setEagerModelSync(true);
|
||||||
defaults.setCompilerOptions({
|
defaults.setCompilerOptions({
|
||||||
@@ -205,7 +308,10 @@ async function createEditor(source) {
|
|||||||
strict: true,
|
strict: true,
|
||||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
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");
|
defaults.addExtraLib(types, "file:///types/yawn.d.ts");
|
||||||
monaco.editor.defineTheme("yawn", {
|
monaco.editor.defineTheme("yawn", {
|
||||||
base: "vs-dark",
|
base: "vs-dark",
|
||||||
@@ -232,12 +338,17 @@ async function createEditor(source) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
elements.editor.textContent = "";
|
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, {
|
editor = monaco.editor.create(elements.editor, {
|
||||||
model,
|
model,
|
||||||
theme: "yawn",
|
theme: document.documentElement.dataset.theme === "dark" ? "yawn" : "vs",
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
fontFamily:
|
||||||
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||||
fontLigatures: true,
|
fontLigatures: true,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
@@ -256,11 +367,13 @@ async function createEditor(source) {
|
|||||||
run,
|
run,
|
||||||
});
|
});
|
||||||
editor.onDidChangeCursorPosition(({ position }) => {
|
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(() => {
|
model.onDidChangeContent(() => {
|
||||||
markDirty(true);
|
markDirty(true);
|
||||||
if (!currentID) localStorage.setItem("yawn:playground:draft", model.getValue());
|
if (!currentID)
|
||||||
|
localStorage.setItem("yawn:playground:draft", model.getValue());
|
||||||
});
|
});
|
||||||
monaco.editor.onDidChangeMarkers(([resource]) => {
|
monaco.editor.onDidChangeMarkers(([resource]) => {
|
||||||
if (resource.toString() === model.uri.toString()) updateDiagnostics();
|
if (resource.toString() === model.uri.toString()) updateDiagnostics();
|
||||||
@@ -272,8 +385,12 @@ async function createEditor(source) {
|
|||||||
function updateDiagnostics() {
|
function updateDiagnostics() {
|
||||||
if (!monaco || !model) return;
|
if (!monaco || !model) return;
|
||||||
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
|
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
|
||||||
const errors = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Error).length;
|
const errors = markers.filter(
|
||||||
const warnings = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Warning).length;
|
(marker) => marker.severity === monaco.MarkerSeverity.Error,
|
||||||
|
).length;
|
||||||
|
const warnings = markers.filter(
|
||||||
|
(marker) => marker.severity === monaco.MarkerSeverity.Warning,
|
||||||
|
).length;
|
||||||
const total = errors + warnings;
|
const total = errors + warnings;
|
||||||
elements["editor-diagnostics"].classList.toggle("has-errors", errors > 0);
|
elements["editor-diagnostics"].classList.toggle("has-errors", errors > 0);
|
||||||
elements["editor-diagnostics"].lastChild.textContent = total
|
elements["editor-diagnostics"].lastChild.textContent = total
|
||||||
@@ -295,7 +412,8 @@ async function transpile() {
|
|||||||
const client = await worker(model.uri);
|
const client = await worker(model.uri);
|
||||||
const emitted = await client.getEmitOutput(model.uri.toString());
|
const emitted = await client.getEmitOutput(model.uri.toString());
|
||||||
const output = emitted.outputFiles.find((file) => file.name.endsWith(".js"));
|
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
|
return output.text
|
||||||
.replace(/^\s*import\s+(?:[\s\S]*?\s+from\s+)?["'][^"']+["'];?\s*$/gm, "")
|
.replace(/^\s*import\s+(?:[\s\S]*?\s+from\s+)?["'][^"']+["'];?\s*$/gm, "")
|
||||||
.replace(/^\s*export\s*\{\s*\};?\s*$/gm, "")
|
.replace(/^\s*export\s*\{\s*\};?\s*$/gm, "")
|
||||||
@@ -329,33 +447,56 @@ function replaceCanvas() {
|
|||||||
canvas.setAttribute("aria-label", "Yawn WebGPU preview");
|
canvas.setAttribute("aria-label", "Yawn WebGPU preview");
|
||||||
const scale = Math.min(devicePixelRatio || 1, 2);
|
const scale = Math.min(devicePixelRatio || 1, 2);
|
||||||
canvas.width = Math.max(1, Math.round(elements.preview.clientWidth * scale));
|
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);
|
previous.replaceWith(canvas);
|
||||||
elements.canvas = canvas;
|
elements.canvas = canvas;
|
||||||
elements["resolution-status"].textContent = `${canvas.width} × ${canvas.height}`;
|
elements["resolution-status"].textContent =
|
||||||
|
`${canvas.width} × ${canvas.height}`;
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
if (!editor || elements["run-button"].disabled) return;
|
if (!editor || elements["run-button"].disabled) return;
|
||||||
|
clearPlaygroundTimers();
|
||||||
const runID = ++generation;
|
const runID = ++generation;
|
||||||
elements["run-button"].disabled = true;
|
elements["run-button"].disabled = true;
|
||||||
elements["run-button"].lastChild.textContent = " Running";
|
elements["run-button"].lastChild.textContent = " Running";
|
||||||
clearConsole();
|
clearConsole();
|
||||||
setRunStatus("Compiling", "running");
|
setRunStatus("Compiling", "running");
|
||||||
showPreviewMessage("Compiling TypeScript", "Checking types and preparing the render graph.");
|
showPreviewMessage(
|
||||||
|
"Compiling TypeScript",
|
||||||
|
"Checking types and preparing the render graph.",
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
if (!crossOriginIsolated) throw new Error("Cross-origin isolation is disabled on this page.");
|
if (!crossOriginIsolated)
|
||||||
if (!navigator.gpu) throw new Error("WebGPU is not available in this browser.");
|
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]);
|
const [code, api] = await Promise.all([transpile(), packages]);
|
||||||
await disposeCurrent();
|
await disposeCurrent();
|
||||||
const canvas = replaceCanvas();
|
const canvas = replaceCanvas();
|
||||||
const names = Object.keys(api);
|
const names = Object.keys(api);
|
||||||
const started = performance.now();
|
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]),
|
...names.map((name) => api[name]),
|
||||||
canvas,
|
canvas,
|
||||||
(...values) => appendConsole(values.map(formatValue).join(" ")),
|
(...values) => appendConsole(values.map(formatValue).join(" ")),
|
||||||
|
trackedSetTimeout,
|
||||||
|
trackedSetInterval,
|
||||||
|
trackedClearTimer,
|
||||||
|
trackedClearTimer,
|
||||||
);
|
);
|
||||||
if (runID !== generation) {
|
if (runID !== generation) {
|
||||||
if (result?.dispose) await result.dispose();
|
if (result?.dispose) await result.dispose();
|
||||||
@@ -364,7 +505,10 @@ async function run() {
|
|||||||
current = result;
|
current = result;
|
||||||
window.__yawnPlayground = result;
|
window.__yawnPlayground = result;
|
||||||
elements["preview-message"].hidden = true;
|
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();
|
if (activePanel === "profile") await attachProfiler();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (runID === generation) {
|
if (runID === generation) {
|
||||||
@@ -377,7 +521,8 @@ async function run() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (runID === generation) {
|
if (runID === generation) {
|
||||||
elements["run-button"].disabled = false;
|
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);
|
stopProfile = profileCore.onProfile(renderProfile);
|
||||||
const supported = await profileCore.setProfiler(true);
|
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) {
|
function setProfileMessage(message) {
|
||||||
@@ -408,8 +556,10 @@ function setProfileMessage(message) {
|
|||||||
function renderProfile(profile) {
|
function renderProfile(profile) {
|
||||||
elements["profile-empty"].hidden = true;
|
elements["profile-empty"].hidden = true;
|
||||||
elements["profile-results"].hidden = false;
|
elements["profile-results"].hidden = false;
|
||||||
elements["profile-total"].textContent = `${profile.milliseconds.toFixed(2)} ms`;
|
elements["profile-total"].textContent =
|
||||||
elements["profile-latency"].textContent = `${profile.readbackMilliseconds.toFixed(2)} ms`;
|
`${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"].textContent = profile.adapter || "WebGPU adapter";
|
||||||
elements["profile-adapter"].title = profile.adapter || "WebGPU adapter";
|
elements["profile-adapter"].title = profile.adapter || "WebGPU adapter";
|
||||||
elements["profile-passes"].replaceChildren(
|
elements["profile-passes"].replaceChildren(
|
||||||
@@ -441,7 +591,8 @@ function appendConsole(message, error = false) {
|
|||||||
line.textContent = message;
|
line.textContent = message;
|
||||||
line.classList.toggle("error", error);
|
line.classList.toggle("error", error);
|
||||||
elements["console-lines"].append(line);
|
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" });
|
line.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +603,8 @@ function clearConsole() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setRunStatus(message, state) {
|
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}`;
|
elements["run-status"].lastChild.textContent = ` ${message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +618,9 @@ function openInspector(panel) {
|
|||||||
activePanel = panel;
|
activePanel = panel;
|
||||||
elements.inspector.classList.remove("collapsed");
|
elements.inspector.classList.remove("collapsed");
|
||||||
elements["inspector-button"].setAttribute("aria-pressed", "true");
|
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;
|
const active = button.dataset.panel === panel;
|
||||||
button.classList.toggle("active", active);
|
button.classList.toggle("active", active);
|
||||||
button.setAttribute("aria-selected", String(active));
|
button.setAttribute("aria-selected", String(active));
|
||||||
@@ -497,7 +651,8 @@ async function save() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
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;
|
currentID = result.id;
|
||||||
currentRevision = result.revision;
|
currentRevision = result.revision;
|
||||||
initialSource = result.code;
|
initialSource = result.code;
|
||||||
@@ -518,13 +673,22 @@ function showToast(message) {
|
|||||||
clearTimeout(toastTimer);
|
clearTimeout(toastTimer);
|
||||||
elements.toast.textContent = message;
|
elements.toast.textContent = message;
|
||||||
elements.toast.classList.add("visible");
|
elements.toast.classList.add("visible");
|
||||||
toastTimer = setTimeout(() => elements.toast.classList.remove("visible"), 2400);
|
toastTimer = setTimeout(
|
||||||
|
() => elements.toast.classList.remove("visible"),
|
||||||
|
2400,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertSnippet(name) {
|
function insertSnippet(name) {
|
||||||
const selection = editor.getSelection();
|
const selection = editor.getSelection();
|
||||||
const prefix = model.getValueInRange(selection).endsWith("\n") ? "" : "\n\n";
|
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();
|
elements["snippets-dialog"].close();
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
@@ -539,7 +703,8 @@ function sampleFPS(now = performance.now()) {
|
|||||||
sampledFrame = frame;
|
sampledFrame = frame;
|
||||||
} else if (now - sampledAt >= 500) {
|
} else if (now - sampledAt >= 500) {
|
||||||
const fps = ((frame - sampledFrame) * 1000) / (now - sampledAt);
|
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;
|
sampledAt = now;
|
||||||
sampledFrame = frame;
|
sampledFrame = frame;
|
||||||
}
|
}
|
||||||
@@ -562,22 +727,34 @@ elements["reset-button"].addEventListener("click", () => {
|
|||||||
model?.setValue(initialSource);
|
model?.setValue(initialSource);
|
||||||
void run();
|
void run();
|
||||||
});
|
});
|
||||||
elements["format-button"].addEventListener("click", () => editor?.getAction("editor.action.formatDocument").run());
|
elements["format-button"].addEventListener("click", () =>
|
||||||
elements["snippets-button"].addEventListener("click", () => elements["snippets-dialog"].showModal());
|
editor?.getAction("editor.action.formatDocument").run(),
|
||||||
elements["snippets-dialog"].querySelector("[data-close-dialog]").addEventListener("click", () => elements["snippets-dialog"].close());
|
);
|
||||||
|
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) => {
|
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]")) {
|
for (const button of document.querySelectorAll("[data-snippet]")) {
|
||||||
button.addEventListener("click", () => insertSnippet(button.dataset.snippet));
|
button.addEventListener("click", () => insertSnippet(button.dataset.snippet));
|
||||||
}
|
}
|
||||||
elements["snippets-dialog"].querySelector("input").addEventListener("input", (event) => {
|
elements["snippets-dialog"]
|
||||||
|
.querySelector("input")
|
||||||
|
.addEventListener("input", (event) => {
|
||||||
const query = event.target.value.trim().toLowerCase();
|
const query = event.target.value.trim().toLowerCase();
|
||||||
for (const button of document.querySelectorAll("[data-snippet]")) {
|
for (const button of document.querySelectorAll("[data-snippet]")) {
|
||||||
button.hidden = Boolean(query) && !button.textContent.toLowerCase().includes(query);
|
button.hidden =
|
||||||
|
Boolean(query) && !button.textContent.toLowerCase().includes(query);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (const button of document.querySelectorAll(".inspector-tabs [data-panel]")) {
|
for (const button of document.querySelectorAll(
|
||||||
|
".inspector-tabs [data-panel]",
|
||||||
|
)) {
|
||||||
button.addEventListener("click", () => openInspector(button.dataset.panel));
|
button.addEventListener("click", () => openInspector(button.dataset.panel));
|
||||||
}
|
}
|
||||||
elements["clear-console"].addEventListener("click", clearConsole);
|
elements["clear-console"].addEventListener("click", clearConsole);
|
||||||
@@ -598,7 +775,10 @@ elements["resize-handle"].addEventListener("pointerdown", (event) => {
|
|||||||
elements["resize-handle"].addEventListener("pointermove", (event) => {
|
elements["resize-handle"].addEventListener("pointermove", (event) => {
|
||||||
if (!elements["resize-handle"].hasPointerCapture(event.pointerId)) return;
|
if (!elements["resize-handle"].hasPointerCapture(event.pointerId)) return;
|
||||||
const bounds = elements.workbench.getBoundingClientRect();
|
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.workbench.style.setProperty("--editor-width", `${percentage}%`);
|
||||||
});
|
});
|
||||||
elements["resize-handle"].addEventListener("pointerup", (event) => {
|
elements["resize-handle"].addEventListener("pointerup", (event) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
Vendored
+188
-25
@@ -30,10 +30,25 @@ declare module "@yawn/core" {
|
|||||||
passes: Array<{ name: string; milliseconds: number }>;
|
passes: Array<{ name: string; milliseconds: number }>;
|
||||||
}
|
}
|
||||||
export class YawnCore {
|
export class YawnCore {
|
||||||
constructor(canvas: HTMLCanvasElement, options?: { arenaBytes?: number; debug?: boolean });
|
constructor(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
options?: { arenaBytes?: number; debug?: boolean },
|
||||||
|
);
|
||||||
readonly ready: Promise<void>;
|
readonly ready: Promise<void>;
|
||||||
createRows(options: { name: string; rows: number; stride: number; format: RowFormat }): Promise<SharedRows>;
|
createRows(options: {
|
||||||
createRowsBatch(rows: Array<{ name: string; rows: number; stride: number; format: RowFormat }>): Promise<SharedRows[]>;
|
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>;
|
deleteRows(name: string): Promise<void>;
|
||||||
allocateObject(name: string): Promise<number>;
|
allocateObject(name: string): Promise<number>;
|
||||||
deleteObject(name: string, id: number): Promise<void>;
|
deleteObject(name: string, id: number): Promise<void>;
|
||||||
@@ -52,23 +67,44 @@ declare module "@yawn/core" {
|
|||||||
declare module "@yawn/handles" {
|
declare module "@yawn/handles" {
|
||||||
import { SharedRows, YawnCore } from "@yawn/core";
|
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 {
|
export class Scene {
|
||||||
constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
|
constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
|
||||||
readonly core: YawnCore;
|
readonly core: YawnCore;
|
||||||
readonly ready: Promise<this>;
|
readonly ready: Promise<this>;
|
||||||
readonly hdr: boolean;
|
readonly hdr: boolean;
|
||||||
array(name: string): SharedRows;
|
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>;
|
reserve(additional: { nodes?: number; materials?: number }): Promise<void>;
|
||||||
batchWrites<T>(operation: () => T): T;
|
batchWrites<T>(operation: () => T): T;
|
||||||
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Vector3View { x: number; y: number; z: number; set(values: ArrayLike<number>): void }
|
export interface Vector3View {
|
||||||
export interface RotorView extends Vector3View { w: number }
|
x: number;
|
||||||
export interface NodeOptions { position?: ArrayLike<number>; rotor?: ArrayLike<number>; scale?: ArrayLike<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 {
|
export class Node {
|
||||||
constructor(scene: Scene, options?: NodeOptions);
|
constructor(scene: Scene, options?: NodeOptions);
|
||||||
readonly scene: Scene;
|
readonly scene: Scene;
|
||||||
@@ -103,7 +139,13 @@ declare module "@yawn/handles" {
|
|||||||
dispose(): Promise<void>;
|
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 {
|
export interface MeshOptions extends NodeOptions {
|
||||||
geometryId?: number;
|
geometryId?: number;
|
||||||
material?: PBRMaterial;
|
material?: PBRMaterial;
|
||||||
@@ -119,10 +161,55 @@ declare module "@yawn/handles" {
|
|||||||
material: PBRMaterial;
|
material: PBRMaterial;
|
||||||
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
||||||
setVertexData(kind: VertexKind, data: ArrayLike<number>): Promise<this>;
|
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 {
|
export class Camera extends Node {
|
||||||
fov: number;
|
fov: number;
|
||||||
near: number;
|
near: number;
|
||||||
@@ -130,30 +217,106 @@ declare module "@yawn/handles" {
|
|||||||
aspect: number;
|
aspect: number;
|
||||||
}
|
}
|
||||||
export class ArcRotateCamera extends Camera {
|
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;
|
alpha: number;
|
||||||
beta: number;
|
beta: number;
|
||||||
radius: number;
|
radius: number;
|
||||||
|
detachControls(): this;
|
||||||
}
|
}
|
||||||
export class FreeCamera extends Camera {
|
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 {
|
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 interface LightOptions extends NodeOptions {
|
||||||
export class PointLight extends Node { constructor(scene: Scene, options?: LightOptions & { range?: number }); intensity: number; range: number }
|
color?: ArrayLike<number>;
|
||||||
export class AmbientLight extends Node { constructor(scene: Scene, options?: LightOptions); intensity: number }
|
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 PointLight extends Node {
|
||||||
export class RectAreaLight extends Node { constructor(scene: Scene, options?: LightOptions & { width?: number; height?: number }) }
|
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 ComputePass {
|
||||||
export class FXAA { constructor(scene: Scene, options?: Record<string, unknown>); readonly ready: Promise<this>; dispose(): Promise<void> }
|
constructor(options: Record<string, unknown>);
|
||||||
export class ColorGrading extends FXAA { constructor(scene: Scene, options?: { toneMap?: "aces" | "reinhard" | "linear"; amount?: number }) }
|
readonly id: string;
|
||||||
export class DynamicExposure extends FXAA { constructor(scene: Scene, options?: { exposure?: number }) }
|
}
|
||||||
export class SSAO extends FXAA { constructor(scene: Scene, options?: { amount?: number }) }
|
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[]>;
|
export function importGltf(scene: Scene, url: string | URL): Promise<Mesh[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+238
-68
@@ -4,26 +4,43 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="description" content="Learn Yawn from your first WebGPU scene through shared memory and custom render graphs." />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Learn Yawn from your first WebGPU scene through shared memory and custom render graphs."
|
||||||
|
/>
|
||||||
<title>Learn Yawn — Documentation</title>
|
<title>Learn Yawn — Documentation</title>
|
||||||
|
<script src="/assets/theme.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/site.css" />
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
<link rel="stylesheet" href="/assets/docs.css" />
|
<link rel="stylesheet" href="/assets/docs.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/theme.css" />
|
||||||
<script type="module" src="/assets/docs.js"></script>
|
<script type="module" src="/assets/docs.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="docs-body">
|
<body class="docs-body">
|
||||||
<header class="site-header docs-header">
|
<header class="site-header docs-header">
|
||||||
<a class="wordmark" href="/" aria-label="Yawn home">
|
<a class="wordmark" href="/" aria-label="Yawn home">
|
||||||
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span><span>Yawn</span>
|
<span class="wordmark-mark" aria-hidden="true"
|
||||||
|
><i></i><i></i><i></i></span
|
||||||
|
><span>Yawn</span>
|
||||||
</a>
|
</a>
|
||||||
<span class="docs-product">Docs</span>
|
<span class="docs-product">Docs</span>
|
||||||
<label class="docs-search">
|
<label class="docs-search">
|
||||||
<span aria-hidden="true">⌕</span>
|
<span aria-hidden="true">⌕</span>
|
||||||
<input type="search" placeholder="Filter this guide" aria-label="Filter documentation sections" />
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Filter this guide"
|
||||||
|
aria-label="Filter documentation sections"
|
||||||
|
/>
|
||||||
<kbd>/</kbd>
|
<kbd>/</kbd>
|
||||||
</label>
|
</label>
|
||||||
<nav class="site-nav" aria-label="Documentation navigation">
|
<nav class="site-nav" aria-label="Documentation navigation">
|
||||||
<a href="/playground">Playground</a><a href="https://git.heaust.org/heaust/yawn">Source</a>
|
<a href="/playground">Playground</a
|
||||||
|
><a href="https://git.heaust.org/heaust/yawn">Source</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
<select class="theme-select" data-theme-select aria-label="Color theme">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="docs-layout">
|
<div class="docs-layout">
|
||||||
@@ -54,7 +71,12 @@
|
|||||||
<a href="#custom-rows">Craft your own API</a>
|
<a href="#custom-rows">Craft your own API</a>
|
||||||
<a href="#deployment">Deployment</a>
|
<a href="#deployment">Deployment</a>
|
||||||
</div>
|
</div>
|
||||||
<a class="sidebar-playground" href="/playground"><span>▶</span><div><strong>Try as you learn</strong><small>Open the playground</small></div></a>
|
<a class="sidebar-playground" href="/playground"
|
||||||
|
><span>▶</span>
|
||||||
|
<div>
|
||||||
|
<strong>Try as you learn</strong><small>Open the playground</small>
|
||||||
|
</div></a
|
||||||
|
>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="docs-main">
|
<main class="docs-main">
|
||||||
@@ -63,13 +85,20 @@
|
|||||||
<div class="doc-eyebrow">Start here · 5 minute read</div>
|
<div class="doc-eyebrow">Start here · 5 minute read</div>
|
||||||
<h1>Build your first Yawn scene</h1>
|
<h1>Build your first Yawn scene</h1>
|
||||||
<p class="doc-lede">
|
<p class="doc-lede">
|
||||||
This guide starts from an empty HTML file and ends with a WebGPU triangle you can
|
This guide starts from an empty HTML file and ends with a WebGPU
|
||||||
move, shade, and inspect. You do not need engine experience. Every new term is
|
triangle you can move, shade, and inspect. You do not need engine
|
||||||
explained where it first appears.
|
experience. Every new term is explained where it first appears.
|
||||||
</p>
|
</p>
|
||||||
<div class="callout callout-blue">
|
<div class="callout callout-blue">
|
||||||
<span class="callout-icon">i</span>
|
<span class="callout-icon">i</span>
|
||||||
<div><strong>New to programming?</strong><p>Type the examples exactly as shown. Words in <code>code style</code> are names the computer expects; the prose around them explains why.</p></div>
|
<div>
|
||||||
|
<strong>New to programming?</strong>
|
||||||
|
<p>
|
||||||
|
Type the examples exactly as shown. Words in
|
||||||
|
<code>code style</code> are names the computer expects; the
|
||||||
|
prose around them explains why.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -77,34 +106,55 @@
|
|||||||
<div class="section-kicker"><span>01</span> Installation</div>
|
<div class="section-kicker"><span>01</span> Installation</div>
|
||||||
<h2>Load Handles from Yawn’s CDN</h2>
|
<h2>Load Handles from Yawn’s CDN</h2>
|
||||||
<p>
|
<p>
|
||||||
You do not build or host Yawn yourself. An <em>import map</em> gives the CDN module a
|
You do not build or host Yawn yourself. An
|
||||||
short name that the rest of this tutorial can use. Put this inside your page’s
|
<em>import map</em> gives the CDN module a short name that the
|
||||||
|
rest of this tutorial can use. Put this inside your page’s
|
||||||
<code><head></code>:
|
<code><head></code>:
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="html">
|
<div class="code-block" data-language="html">
|
||||||
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>index.html</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code><script type="importmap">
|
<pre><code><script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@yawn/core": "https://yawn.heaust.org/pkg/core.js",
|
||||||
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script></code></pre>
|
</script></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
That URL serves a ready-to-use ES module. Its render worker, import worker, picking
|
Core and Handles stay separate: Handles uses the mapped Core
|
||||||
worker, and WebAssembly module all continue loading from Yawn’s CDN automatically.
|
module instead of containing its own copy. Their render worker,
|
||||||
|
import worker, picking worker, and WebAssembly module all continue
|
||||||
|
loading from Yawn’s CDN automatically.
|
||||||
</p>
|
</p>
|
||||||
<h3>Make a canvas</h3>
|
<h3>Make a canvas</h3>
|
||||||
<p>A <code>canvas</code> is the rectangle where WebGPU will draw. Add one to the page body:</p>
|
<p>
|
||||||
|
A <code>canvas</code> is the rectangle where WebGPU will draw. Add
|
||||||
|
one to the page body:
|
||||||
|
</p>
|
||||||
<div class="code-block" data-language="html">
|
<div class="code-block" data-language="html">
|
||||||
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>index.html</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code><canvas id="view" width="1280" height="720"></canvas>
|
<pre><code><canvas id="view" width="1280" height="720"></canvas>
|
||||||
<script type="module" src="/app.js"></script></code></pre>
|
<script type="module" src="/app.js"></script></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="callout">
|
<div class="callout">
|
||||||
<span class="callout-icon">!</span>
|
<span class="callout-icon">!</span>
|
||||||
<div><strong>Your page needs isolation headers</strong><p>Your own web host must send <code>COOP: same-origin</code> and <code>COEP: require-corp</code> so SharedArrayBuffer is available. The CDN already sends the matching CORS and resource-policy headers for Yawn’s files.</p></div>
|
<div>
|
||||||
|
<strong>Your page needs isolation headers</strong>
|
||||||
|
<p>
|
||||||
|
Your own web host must send <code>COOP: same-origin</code> and
|
||||||
|
<code>COEP: require-corp</code> so SharedArrayBuffer is
|
||||||
|
available. The CDN already sends the matching CORS and
|
||||||
|
resource-policy headers for Yawn’s files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -113,12 +163,16 @@
|
|||||||
<h2>Scene → material → mesh</h2>
|
<h2>Scene → material → mesh</h2>
|
||||||
<p>
|
<p>
|
||||||
A <strong>scene</strong> owns the shared data and render graph. A
|
A <strong>scene</strong> owns the shared data and render graph. A
|
||||||
<strong>material</strong> describes the surface. A <strong>mesh</strong> supplies points
|
<strong>material</strong> describes the surface. A
|
||||||
and tells Yawn which order to connect them.
|
<strong>mesh</strong> supplies points and tells Yawn which order
|
||||||
|
to connect them.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block code-block-large" data-language="typescript">
|
<div class="code-block code-block-large" data-language="typescript">
|
||||||
<div class="code-title"><span>app.js</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
<pre><code>import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
<span>app.js</span><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
|
<pre><code>import "@yawn/core";
|
||||||
|
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||||
|
|
||||||
const canvas = document.querySelector("#view");
|
const canvas = document.querySelector("#view");
|
||||||
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
||||||
@@ -141,23 +195,48 @@ const triangle = new Mesh(scene, {
|
|||||||
await triangle.ready;</code></pre>
|
await triangle.ready;</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="explain-grid">
|
<div class="explain-grid">
|
||||||
<div><code>new</code><p>Creates one object. Here that is a scene, material, or mesh handle.</p></div>
|
<div>
|
||||||
<div><code>await …ready</code><p>Waits for setup to finish before the next object depends on it.</p></div>
|
<code>new</code>
|
||||||
<div><code>[x, y, z]</code><p>One point in 3D space: horizontal, vertical, and depth.</p></div>
|
<p>
|
||||||
|
Creates one object. Here that is a scene, material, or mesh
|
||||||
|
handle.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="inline-play" href="/playground"><span>▶</span><div><strong>Run this example</strong><small>It is already loaded in the playground</small></div><b>Open →</b></a>
|
<div>
|
||||||
|
<code>await …ready</code>
|
||||||
|
<p>
|
||||||
|
Waits for setup to finish before the next object depends on
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<code>[x, y, z]</code>
|
||||||
|
<p>One point in 3D space: horizontal, vertical, and depth.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="inline-play" href="/playground"
|
||||||
|
><span>▶</span>
|
||||||
|
<div>
|
||||||
|
<strong>Run this example</strong
|
||||||
|
><small>It is already loaded in the playground</small>
|
||||||
|
</div>
|
||||||
|
<b>Open →</b></a
|
||||||
|
>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="camera" data-title="Add a camera">
|
<section id="camera" data-title="Add a camera">
|
||||||
<div class="section-kicker"><span>03</span> Move around</div>
|
<div class="section-kicker"><span>03</span> Move around</div>
|
||||||
<h2>Add a camera you can orbit</h2>
|
<h2>Add a camera you can orbit</h2>
|
||||||
<p>
|
<p>
|
||||||
The starter triangle is already in clip space, so it is visible without a camera.
|
The starter triangle is already in clip space, so it is visible
|
||||||
For a 3D world, add an <code>ArcRotateCamera</code>. Drag to orbit and use the wheel to
|
without a camera. For a 3D world, add an
|
||||||
|
<code>ArcRotateCamera</code>. Drag to orbit and use the wheel to
|
||||||
zoom.
|
zoom.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>app.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>app.ts</span><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>import { ArcRotateCamera } from "@yawn/handles";
|
<pre><code>import { ArcRotateCamera } from "@yawn/handles";
|
||||||
|
|
||||||
const camera = new ArcRotateCamera(scene, {
|
const camera = new ArcRotateCamera(scene, {
|
||||||
@@ -175,12 +254,16 @@ await camera.ready;</code></pre>
|
|||||||
<div class="section-kicker"><span>04</span> Build a world</div>
|
<div class="section-kicker"><span>04</span> Build a world</div>
|
||||||
<h2>Clone geometry, not work</h2>
|
<h2>Clone geometry, not work</h2>
|
||||||
<p>
|
<p>
|
||||||
Every <code>Mesh</code> is an instance. Calling <code>clone()</code> shares its geometry
|
Every <code>Mesh</code> is an instance. Calling
|
||||||
and creates only a new transform and mesh slot. If one clone later changes its vertex
|
<code>clone()</code> shares its geometry and creates only a new
|
||||||
|
transform and mesh slot. If one clone later changes its vertex
|
||||||
data, Yawn makes that geometry unique automatically.
|
data, Yawn makes that geometry unique automatically.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>instances.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>instances.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>for (let x = -4; x <= 4; x++) {
|
<pre><code>for (let x = -4; x <= 4; x++) {
|
||||||
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
|
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
|
||||||
await copy.ready;
|
await copy.ready;
|
||||||
@@ -191,11 +274,14 @@ await camera.ready;</code></pre>
|
|||||||
<section id="materials" data-title="Materials and lights">
|
<section id="materials" data-title="Materials and lights">
|
||||||
<h2>Materials and lights are ordinary handles</h2>
|
<h2>Materials and lights are ordinary handles</h2>
|
||||||
<p>
|
<p>
|
||||||
Change a material after it is ready and Yawn writes the new value directly into its
|
Change a material after it is ready and Yawn writes the new value
|
||||||
shared row. Lights use the same pattern.
|
directly into its shared row. Lights use the same pattern.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>lighting.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>lighting.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>import { AmbientLight, PointLight } from "@yawn/handles";
|
<pre><code>import { AmbientLight, PointLight } from "@yawn/handles";
|
||||||
|
|
||||||
const key = new PointLight(scene, {
|
const key = new PointLight(scene, {
|
||||||
@@ -217,11 +303,15 @@ sky.roughness = 0.5; // one shared-memory write</code></pre>
|
|||||||
<section id="models" data-title="Import a glTF model">
|
<section id="models" data-title="Import a glTF model">
|
||||||
<h2>Bring in a glTF model</h2>
|
<h2>Bring in a glTF model</h2>
|
||||||
<p>
|
<p>
|
||||||
<code>importGltf</code> fetches and parses <code>.gltf</code> or <code>.glb</code> data in
|
<code>importGltf</code> fetches and parses <code>.gltf</code> or
|
||||||
a worker, then creates ordinary Yawn meshes and materials.
|
<code>.glb</code> data in a worker, then creates ordinary Yawn
|
||||||
|
meshes and materials.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>model.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>model.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>import { importGltf } from "@yawn/handles";
|
<pre><code>import { importGltf } from "@yawn/handles";
|
||||||
|
|
||||||
const meshes = await importGltf(scene, "/models/robot.glb");
|
const meshes = await importGltf(scene, "/models/robot.glb");
|
||||||
@@ -231,9 +321,15 @@ meshes[0].position.y = 0.5;</code></pre>
|
|||||||
|
|
||||||
<section id="post-processing" data-title="Post processing">
|
<section id="post-processing" data-title="Post processing">
|
||||||
<h2>Add effects without leaving the scene API</h2>
|
<h2>Add effects without leaving the scene API</h2>
|
||||||
<p>Effect handles add passes to the same render graph. Batch related additions to rebuild that graph once.</p>
|
<p>
|
||||||
|
Effect handles add passes to the same render graph. Batch related
|
||||||
|
additions to rebuild that graph once.
|
||||||
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>effects.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>effects.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>import { ColorGrading, FXAA } from "@yawn/handles";
|
<pre><code>import { ColorGrading, FXAA } from "@yawn/handles";
|
||||||
|
|
||||||
await scene.batchGraphUpdates(async () => {
|
await scene.batchGraphUpdates(async () => {
|
||||||
@@ -248,27 +344,40 @@ await scene.batchGraphUpdates(async () => {
|
|||||||
<div class="section-kicker"><span>05</span> Understand Yawn</div>
|
<div class="section-kicker"><span>05</span> Understand Yawn</div>
|
||||||
<h2>Setup is messages. Motion is memory.</h2>
|
<h2>Setup is messages. Motion is memory.</h2>
|
||||||
<p>
|
<p>
|
||||||
Expensive structural changes—creating rows, allocating IDs, or replacing a render
|
Expensive structural changes—creating rows, allocating IDs, or
|
||||||
graph—go to the render worker as messages. Values that already exist—positions,
|
replacing a render graph—go to the render worker as messages.
|
||||||
colors, camera matrices, light strengths—change in shared memory.
|
Values that already exist—positions, colors, camera matrices,
|
||||||
|
light strengths—change in shared memory.
|
||||||
</p>
|
</p>
|
||||||
<div class="mental-model">
|
<div class="mental-model">
|
||||||
<div><strong>Infrequent control</strong><span>create · allocate · compile · switch</span></div>
|
<div>
|
||||||
|
<strong>Infrequent control</strong
|
||||||
|
><span>create · allocate · compile · switch</span>
|
||||||
|
</div>
|
||||||
<i>→</i>
|
<i>→</i>
|
||||||
<div class="mental-shared"><strong>Shared rows</strong><span>transform · shade · animate</span></div>
|
<div class="mental-shared">
|
||||||
|
<strong>Shared rows</strong
|
||||||
|
><span>transform · shade · animate</span>
|
||||||
|
</div>
|
||||||
<i>→</i>
|
<i>→</i>
|
||||||
<div><strong>Rust/WASM core</strong><span>schedule · upload · render</span></div>
|
<div>
|
||||||
|
<strong>Rust/WASM core</strong
|
||||||
|
><span>schedule · upload · render</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="shared-memory" data-title="Shared memory">
|
<section id="shared-memory" data-title="Shared memory">
|
||||||
<h2>Write your own hot data</h2>
|
<h2>Write your own hot data</h2>
|
||||||
<p>
|
<p>
|
||||||
Handles are views over named rows. Your application can add rows too. Each row is
|
Handles are views over named rows. Your application can add rows
|
||||||
aligned for predictable CPU and GPU use.
|
too. Each row is aligned for predictable CPU and GPU use.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>simulation.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>simulation.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>const velocity = await scene.ensureRows(
|
<pre><code>const velocity = await scene.ensureRows(
|
||||||
"app.velocity", 10_000, 16, "f32"
|
"app.velocity", 10_000, 16, "f32"
|
||||||
);
|
);
|
||||||
@@ -276,31 +385,48 @@ await scene.batchGraphUpdates(async () => {
|
|||||||
velocity.row(42).set([1, 0, 0, 0]);</code></pre>
|
velocity.row(42).set([1, 0, 0, 0]);</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
That final line changes four floats without cloning an object or posting a worker
|
That final line changes four floats without cloning an object or
|
||||||
message. The scene marks shared data dirty so the next frame sees it.
|
posting a worker message. The scene marks shared data dirty so the
|
||||||
|
next frame sees it.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="profiler" data-title="GPU profiler">
|
<section id="profiler" data-title="GPU profiler">
|
||||||
<h2>Measure the passes the GPU actually ran</h2>
|
<h2>Measure the passes the GPU actually ran</h2>
|
||||||
<p>
|
<p>
|
||||||
Open <strong>Profile</strong> in the playground to enable timestamp queries. Yawn
|
Open <strong>Profile</strong> in the playground to enable
|
||||||
reports the physical pass names and GPU milliseconds without serializing the render
|
timestamp queries. Yawn reports the physical pass names and GPU
|
||||||
queue. Support depends on the browser and adapter.
|
milliseconds without serializing the render queue. Support depends
|
||||||
|
on the browser and adapter.
|
||||||
</p>
|
</p>
|
||||||
<div class="callout callout-blue"><span class="callout-icon">⌁</span><div><strong>Profile a real scene</strong><p>The playground’s profiler uses <code>core.onProfile()</code> and <code>core.setProfiler(true)</code>—the same public APIs available to your app.</p></div></div>
|
<div class="callout callout-blue">
|
||||||
|
<span class="callout-icon">⌁</span>
|
||||||
|
<div>
|
||||||
|
<strong>Profile a real scene</strong>
|
||||||
|
<p>
|
||||||
|
The playground’s profiler uses
|
||||||
|
<code>core.onProfile()</code> and
|
||||||
|
<code>core.setProfiler(true)</code>—the same public APIs
|
||||||
|
available to your app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="core-deep-dive" data-title="Using core directly">
|
<section id="core-deep-dive" data-title="Using core directly">
|
||||||
<div class="section-kicker"><span>06</span> Deep dive</div>
|
<div class="section-kicker"><span>06</span> Deep dive</div>
|
||||||
<h2>Outgrow handles without outgrowing Yawn</h2>
|
<h2>Outgrow handles without outgrowing Yawn</h2>
|
||||||
<p>
|
<p>
|
||||||
<code>@yawn/core</code> has no scene, mesh, material, camera, or built-in shader. It
|
<code>@yawn/core</code> has no scene, mesh, material, camera, or
|
||||||
owns a shared arena and a render-graph runtime. Handles are one replaceable frontend
|
built-in shader. It owns a shared arena and a render-graph
|
||||||
that builds on those two primitives.
|
runtime. Handles are one replaceable frontend that builds on those
|
||||||
|
two primitives.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>core.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>core.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>import { YawnCore } from "https://yawn.heaust.org/pkg/core.js";
|
<pre><code>import { YawnCore } from "https://yawn.heaust.org/pkg/core.js";
|
||||||
|
|
||||||
const core = new YawnCore(canvas, {
|
const core = new YawnCore(canvas, {
|
||||||
@@ -316,20 +442,25 @@ const particles = await core.createRows({
|
|||||||
});</code></pre>
|
});</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
From here, your frontend supplies WGSL, resources, pipelines, and a pass DAG. Core
|
From here, your frontend supplies WGSL, resources, pipelines, and
|
||||||
compiles that description into an up-front loadout, aliases compatible transient
|
a pass DAG. Core compiles that description into an up-front
|
||||||
textures, and records render work.
|
loadout, aliases compatible transient textures, and records render
|
||||||
|
work.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="custom-rows" data-title="Craft your own API">
|
<section id="custom-rows" data-title="Craft your own API">
|
||||||
<h2>Craft an API for your problem</h2>
|
<h2>Craft an API for your problem</h2>
|
||||||
<p>
|
<p>
|
||||||
A custom handle can be as small as an ID plus getters and setters into shared rows.
|
A custom handle can be as small as an ID plus getters and setters
|
||||||
Keep domain policy in your code and send only structural changes to core.
|
into shared rows. Keep domain policy in your code and send only
|
||||||
|
structural changes to core.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-block" data-language="typescript">
|
<div class="code-block" data-language="typescript">
|
||||||
<div class="code-title"><span>Particle.ts</span><button type="button" data-copy>Copy</button></div>
|
<div class="code-title">
|
||||||
|
<span>Particle.ts</span
|
||||||
|
><button type="button" data-copy>Copy</button>
|
||||||
|
</div>
|
||||||
<pre><code>class Particle {
|
<pre><code>class Particle {
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: number,
|
readonly id: number,
|
||||||
@@ -346,11 +477,50 @@ const particles = await core.createRows({
|
|||||||
<section id="deployment" data-title="Deployment">
|
<section id="deployment" data-title="Deployment">
|
||||||
<h2>Deployment checklist</h2>
|
<h2>Deployment checklist</h2>
|
||||||
<ul class="deploy-list">
|
<ul class="deploy-list">
|
||||||
<li><span>1</span><div><strong>Serve over HTTPS</strong><p>WebGPU and cross-origin isolation require a secure browser context outside local development.</p></div></li>
|
<li>
|
||||||
<li><span>2</span><div><strong>Keep the isolation headers</strong><p>Send <code>Cross-Origin-Opener-Policy: same-origin</code> and <code>Cross-Origin-Embedder-Policy: require-corp</code>.</p></div></li>
|
<span>1</span>
|
||||||
<li><span>3</span><div><strong>Allow Yawn’s CDN</strong><p>The imported module keeps every worker and WASM request on <code>yawn.heaust.org</code>. If you use a Content Security Policy, allow that origin and <code>blob:</code> workers.</p></div></li>
|
<div>
|
||||||
|
<strong>Serve over HTTPS</strong>
|
||||||
|
<p>
|
||||||
|
WebGPU and cross-origin isolation require a secure browser
|
||||||
|
context outside local development.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>2</span>
|
||||||
|
<div>
|
||||||
|
<strong>Keep the isolation headers</strong>
|
||||||
|
<p>
|
||||||
|
Send
|
||||||
|
<code>Cross-Origin-Opener-Policy: same-origin</code> and
|
||||||
|
<code>Cross-Origin-Embedder-Policy: require-corp</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>3</span>
|
||||||
|
<div>
|
||||||
|
<strong>Allow Yawn’s CDN</strong>
|
||||||
|
<p>
|
||||||
|
The imported module keeps every worker and WASM request on
|
||||||
|
<code>yawn.heaust.org</code>. If you use a Content Security
|
||||||
|
Policy, allow that origin and <code>blob:</code> workers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="next-card"><div><small>Next step</small><h3>Make the starter scene yours.</h3><p>Open the playground, choose a snippet, and save a revision you can share.</p></div><a class="button" href="/playground">Open playground →</a></div>
|
<div class="next-card">
|
||||||
|
<div>
|
||||||
|
<small>Next step</small>
|
||||||
|
<h3>Make the starter scene yours.</h3>
|
||||||
|
<p>
|
||||||
|
Open the playground, choose a snippet, and save a revision you
|
||||||
|
can share.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a class="button" href="/playground">Open playground →</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+104
-37
@@ -9,13 +9,17 @@
|
|||||||
content="Yawn is a WebGPU engine that starts with friendly scene handles and scales down to a programmable Rust/WASM render core."
|
content="Yawn is a WebGPU engine that starts with friendly scene handles and scales down to a programmable Rust/WASM render core."
|
||||||
/>
|
/>
|
||||||
<title>Yawn — a WebGPU engine that grows with you</title>
|
<title>Yawn — a WebGPU engine that grows with you</title>
|
||||||
|
<script src="/assets/theme.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/site.css" />
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/theme.css" />
|
||||||
<script type="module" src="/assets/site.js"></script>
|
<script type="module" src="/assets/site.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a class="wordmark" href="/" aria-label="Yawn home">
|
<a class="wordmark" href="/" aria-label="Yawn home">
|
||||||
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
<span class="wordmark-mark" aria-hidden="true"
|
||||||
|
><i></i><i></i><i></i
|
||||||
|
></span>
|
||||||
<span>Yawn</span>
|
<span>Yawn</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="site-nav" aria-label="Main navigation">
|
<nav class="site-nav" aria-label="Main navigation">
|
||||||
@@ -23,6 +27,11 @@
|
|||||||
<a href="/playground">Playground</a>
|
<a href="/playground">Playground</a>
|
||||||
<a href="https://git.heaust.org/heaust/yawn">Source</a>
|
<a href="https://git.heaust.org/heaust/yawn">Source</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
<select class="theme-select" data-theme-select aria-label="Color theme">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
<a class="button button-small" href="/docs#first-scene">Get started</a>
|
<a class="button button-small" href="/docs#first-scene">Get started</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -32,17 +41,24 @@
|
|||||||
<div class="eyebrow"><span></span> WebGPU without the ceremony</div>
|
<div class="eyebrow"><span></span> WebGPU without the ceremony</div>
|
||||||
<h1>Start with a scene.<br />Scale into an engine.</h1>
|
<h1>Start with a scene.<br />Scale into an engine.</h1>
|
||||||
<p class="hero-lede">
|
<p class="hero-lede">
|
||||||
Yawn gives you approachable TypeScript handles on day one, direct shared-memory
|
Yawn gives you approachable TypeScript handles on day one, direct
|
||||||
performance when you need it, and a programmable render core when your ideas outgrow
|
shared-memory performance when you need it, and a programmable
|
||||||
conventions.
|
render core when your ideas outgrow conventions.
|
||||||
</p>
|
</p>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a class="button" href="/playground">Open the playground <span>→</span></a>
|
<a class="button" href="/playground"
|
||||||
<a class="button button-quiet" href="/docs#installation">Read the 5-minute setup</a>
|
>Open the playground <span>→</span></a
|
||||||
|
>
|
||||||
|
<a class="button button-quiet" href="/docs#installation"
|
||||||
|
>Read the 5-minute setup</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-note">
|
<div class="hero-note">
|
||||||
<span class="browser-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
<span class="browser-dots" aria-hidden="true"
|
||||||
One CDN import · No framework required · Rust/WASM core
|
><i></i><i></i><i></i
|
||||||
|
></span>
|
||||||
|
Separate Core + Handles modules · No framework required · Rust/WASM
|
||||||
|
core
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,7 +94,9 @@
|
|||||||
<section class="trust-strip" aria-label="Yawn principles">
|
<section class="trust-strip" aria-label="Yawn principles">
|
||||||
<div><strong>One</strong><span>shared arena</span></div>
|
<div><strong>One</strong><span>shared arena</span></div>
|
||||||
<div><strong>Zero</strong><span>messages for hot state</span></div>
|
<div><strong>Zero</strong><span>messages for hot state</span></div>
|
||||||
<div><strong>Any</strong><span>render graph you can describe</span></div>
|
<div>
|
||||||
|
<strong>Any</strong><span>render graph you can describe</span>
|
||||||
|
</div>
|
||||||
<div><strong>Small</strong><span>Rust/WASM foundation</span></div>
|
<div><strong>Small</strong><span>Rust/WASM foundation</span></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -87,8 +105,9 @@
|
|||||||
<div class="eyebrow"><span></span> Progressive by design</div>
|
<div class="eyebrow"><span></span> Progressive by design</div>
|
||||||
<h2>Use exactly as much engine as you need.</h2>
|
<h2>Use exactly as much engine as you need.</h2>
|
||||||
<p>
|
<p>
|
||||||
You do not have to choose between an easy API and a serious foundation. Yawn keeps
|
You do not have to choose between an easy API and a serious
|
||||||
those layers separate, so learning more never means starting over.
|
foundation. Yawn keeps those layers separate, so learning more never
|
||||||
|
means starting over.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="gear-grid">
|
<div class="gear-grid">
|
||||||
@@ -97,8 +116,9 @@
|
|||||||
<div class="gear-icon"><span class="cube-icon"></span></div>
|
<div class="gear-icon"><span class="cube-icon"></span></div>
|
||||||
<h3>Build with handles</h3>
|
<h3>Build with handles</h3>
|
||||||
<p>
|
<p>
|
||||||
Start with familiar scenes, meshes, materials, lights, cameras, glTF imports, and
|
Start with familiar scenes, meshes, materials, lights, cameras,
|
||||||
post effects. Constructors are explicit and every async boundary is visible.
|
glTF imports, and post effects. Constructors are explicit and
|
||||||
|
every async boundary is visible.
|
||||||
</p>
|
</p>
|
||||||
<a href="/docs#first-scene">Make your first scene <span>→</span></a>
|
<a href="/docs#first-scene">Make your first scene <span>→</span></a>
|
||||||
</article>
|
</article>
|
||||||
@@ -107,18 +127,22 @@
|
|||||||
<div class="gear-icon"><span class="rows-icon"></span></div>
|
<div class="gear-icon"><span class="rows-icon"></span></div>
|
||||||
<h3>Move through shared data</h3>
|
<h3>Move through shared data</h3>
|
||||||
<p>
|
<p>
|
||||||
Positions, cameras, lights, materials, and your own application rows live in one
|
Positions, cameras, lights, materials, and your own application
|
||||||
SharedArrayBuffer. Hot updates become direct typed-array writes.
|
rows live in one SharedArrayBuffer. Hot updates become direct
|
||||||
|
typed-array writes.
|
||||||
</p>
|
</p>
|
||||||
<a href="/docs#shared-memory">Understand the fast path <span>→</span></a>
|
<a href="/docs#shared-memory"
|
||||||
|
>Understand the fast path <span>→</span></a
|
||||||
|
>
|
||||||
</article>
|
</article>
|
||||||
<article class="gear-card">
|
<article class="gear-card">
|
||||||
<div class="gear-number">03</div>
|
<div class="gear-number">03</div>
|
||||||
<div class="gear-icon"><span class="graph-icon"></span></div>
|
<div class="gear-icon"><span class="graph-icon"></span></div>
|
||||||
<h3>Author the whole graph</h3>
|
<h3>Author the whole graph</h3>
|
||||||
<p>
|
<p>
|
||||||
Use core directly when you need custom resources, WGSL pipelines, compute, pass
|
Use core directly when you need custom resources, WGSL pipelines,
|
||||||
dependencies, transient aliasing, and up-front GPU loadouts.
|
compute, pass dependencies, transient aliasing, and up-front GPU
|
||||||
|
loadouts.
|
||||||
</p>
|
</p>
|
||||||
<a href="/docs#core-deep-dive">Go beneath handles <span>→</span></a>
|
<a href="/docs#core-deep-dive">Go beneath handles <span>→</span></a>
|
||||||
</article>
|
</article>
|
||||||
@@ -128,18 +152,30 @@
|
|||||||
<section class="architecture-section">
|
<section class="architecture-section">
|
||||||
<div class="section-shell architecture-grid">
|
<div class="section-shell architecture-grid">
|
||||||
<div class="architecture-copy">
|
<div class="architecture-copy">
|
||||||
<div class="eyebrow eyebrow-light"><span></span> A quiet hot path</div>
|
<div class="eyebrow eyebrow-light">
|
||||||
|
<span></span> A quiet hot path
|
||||||
|
</div>
|
||||||
<h2>Your frame should move data, not negotiate it.</h2>
|
<h2>Your frame should move data, not negotiate it.</h2>
|
||||||
<p>
|
<p>
|
||||||
Yawn pays setup costs when your scene or graph changes. During play, ordinary updates
|
Yawn pays setup costs when your scene or graph changes. During
|
||||||
stay in shared rows that JavaScript and the render worker can both see.
|
play, ordinary updates stay in shared rows that JavaScript and the
|
||||||
|
render worker can both see.
|
||||||
</p>
|
</p>
|
||||||
<ul class="check-list">
|
<ul class="check-list">
|
||||||
<li><span>✓</span> Structure-of-arrays layout with aligned rows</li>
|
<li>
|
||||||
<li><span>✓</span> Render graphs compiled before they become active</li>
|
<span>✓</span> Structure-of-arrays layout with aligned rows
|
||||||
<li><span>✓</span> Optional timestamp profiling for physical GPU passes</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>✓</span> Render graphs compiled before they become active
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>✓</span> Optional timestamp profiling for physical GPU
|
||||||
|
passes
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a class="text-link-light" href="/docs#mental-model">See how the pieces connect →</a>
|
<a class="text-link-light" href="/docs#mental-model"
|
||||||
|
>See how the pieces connect →</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="architecture-visual" aria-label="Yawn data flow">
|
<div class="architecture-visual" aria-label="Yawn data flow">
|
||||||
<div class="flow-label">Your application</div>
|
<div class="flow-label">Your application</div>
|
||||||
@@ -148,11 +184,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flow-line"><i></i><i></i><i></i></div>
|
<div class="flow-line"><i></i><i></i><i></i></div>
|
||||||
<div class="arena-card">
|
<div class="arena-card">
|
||||||
<div><span></span><strong>SharedArrayBuffer</strong><small>aligned SOA arena</small></div>
|
<div>
|
||||||
<div class="arena-rows"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
<span></span><strong>SharedArrayBuffer</strong
|
||||||
|
><small>aligned SOA arena</small>
|
||||||
|
</div>
|
||||||
|
<div class="arena-rows">
|
||||||
|
<i></i><i></i><i></i><i></i><i></i><i></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-line flow-line-bottom"><i></i></div>
|
<div class="flow-line flow-line-bottom"><i></i></div>
|
||||||
<div class="core-card"><span>Rust / WASM</span><strong>Render graph core</strong></div>
|
<div class="core-card">
|
||||||
|
<span>Rust / WASM</span><strong>Render graph core</strong>
|
||||||
|
</div>
|
||||||
<div class="gpu-line"></div>
|
<div class="gpu-line"></div>
|
||||||
<div class="gpu-card">WebGPU</div>
|
<div class="gpu-card">WebGPU</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,17 +210,30 @@
|
|||||||
<ol class="steps">
|
<ol class="steps">
|
||||||
<li>
|
<li>
|
||||||
<span class="step-index">1</span>
|
<span class="step-index">1</span>
|
||||||
<div><h3>Create a scene</h3><p>Give Yawn a canvas and await one clear readiness promise.</p></div>
|
<div>
|
||||||
|
<h3>Create a scene</h3>
|
||||||
|
<p>Give Yawn a canvas and await one clear readiness promise.</p>
|
||||||
|
</div>
|
||||||
<code>new Scene(canvas)</code>
|
<code>new Scene(canvas)</code>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="step-index">2</span>
|
<span class="step-index">2</span>
|
||||||
<div><h3>Add what you can see</h3><p>Materials and meshes own typed handles, not hidden global state.</p></div>
|
<div>
|
||||||
|
<h3>Add what you can see</h3>
|
||||||
|
<p>
|
||||||
|
Materials and meshes own typed handles, not hidden global state.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<code>new Mesh(scene, options)</code>
|
<code>new Mesh(scene, options)</code>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="step-index">3</span>
|
<span class="step-index">3</span>
|
||||||
<div><h3>Change values directly</h3><p>After setup, ordinary property updates write into shared rows.</p></div>
|
<div>
|
||||||
|
<h3>Change values directly</h3>
|
||||||
|
<p>
|
||||||
|
After setup, ordinary property updates write into shared rows.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<code>mesh.position.x += 0.1</code>
|
<code>mesh.position.x += 0.1</code>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -185,21 +241,32 @@
|
|||||||
|
|
||||||
<section class="cta-section section-shell">
|
<section class="cta-section section-shell">
|
||||||
<div>
|
<div>
|
||||||
<div class="eyebrow eyebrow-light"><span></span> The best way to understand it</div>
|
<div class="eyebrow eyebrow-light">
|
||||||
|
<span></span> The best way to understand it
|
||||||
|
</div>
|
||||||
<h2>Change the code. Watch the passes.</h2>
|
<h2>Change the code. Watch the passes.</h2>
|
||||||
<p>
|
<p>
|
||||||
The playground includes TypeScript intelligence, reusable snippets, live output,
|
The playground includes TypeScript intelligence, reusable snippets,
|
||||||
shareable SQLite-backed revisions, and the engine’s real GPU profiler.
|
live output, shareable SQLite-backed revisions, and the engine’s
|
||||||
|
real GPU profiler.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-white" href="/playground">Launch playground <span>↗</span></a>
|
<a class="button button-white" href="/playground"
|
||||||
|
>Launch playground <span>↗</span></a
|
||||||
|
>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer section-shell">
|
<footer class="site-footer section-shell">
|
||||||
<a class="wordmark" href="/"><span class="wordmark-mark"><i></i><i></i><i></i></span><span>Yawn</span></a>
|
<a class="wordmark" href="/"
|
||||||
|
><span class="wordmark-mark"><i></i><i></i><i></i></span
|
||||||
|
><span>Yawn</span></a
|
||||||
|
>
|
||||||
<p>Start obvious. Optimize deliberately.</p>
|
<p>Start obvious. Optimize deliberately.</p>
|
||||||
<nav aria-label="Footer navigation"><a href="/docs">Docs</a><a href="/playground">Playground</a><a href="https://git.heaust.org/heaust/yawn">Source</a></nav>
|
<nav aria-label="Footer navigation">
|
||||||
|
<a href="/docs">Docs</a><a href="/playground">Playground</a
|
||||||
|
><a href="https://git.heaust.org/heaust/yawn">Source</a>
|
||||||
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+178
-28
@@ -4,9 +4,22 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#f7fbff" />
|
<meta name="theme-color" content="#f7fbff" />
|
||||||
<meta name="description" content="Write, run, profile, and share Yawn WebGPU scenes." />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Write, run, profile, and share Yawn WebGPU scenes."
|
||||||
|
/>
|
||||||
<title>Yawn Playground</title>
|
<title>Yawn Playground</title>
|
||||||
|
<script src="/assets/theme.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/playground.css" />
|
<link rel="stylesheet" href="/assets/playground.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/theme.css" />
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@yawn/core": "/pkg/core.js",
|
||||||
|
"@yawn/handles": "/pkg/handles.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script src="/assets/monaco/vs/loader.js"></script>
|
<script src="/assets/monaco/vs/loader.js"></script>
|
||||||
<script type="module" src="/assets/playground.js"></script>
|
<script type="module" src="/assets/playground.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -14,11 +27,18 @@
|
|||||||
<main class="playground-app">
|
<main class="playground-app">
|
||||||
<header class="playground-header">
|
<header class="playground-header">
|
||||||
<a class="playground-brand" href="/" aria-label="Yawn home">
|
<a class="playground-brand" href="/" aria-label="Yawn home">
|
||||||
<span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span><strong>Yawn</strong>
|
<span class="brand-mark" aria-hidden="true"
|
||||||
|
><i></i><i></i><i></i></span
|
||||||
|
><strong>Yawn</strong>
|
||||||
</a>
|
</a>
|
||||||
<div class="header-divider"></div>
|
<div class="header-divider"></div>
|
||||||
<div class="project-name">
|
<div class="project-name">
|
||||||
<input id="project-title" maxlength="80" value="Sky triangle" aria-label="Playground title" />
|
<input
|
||||||
|
id="project-title"
|
||||||
|
maxlength="80"
|
||||||
|
value="Three-light icosphere"
|
||||||
|
aria-label="Playground title"
|
||||||
|
/>
|
||||||
<span id="dirty-indicator" title="Unsaved changes"></span>
|
<span id="dirty-indicator" title="Unsaved changes"></span>
|
||||||
<span id="revision-label">Draft</span>
|
<span id="revision-label">Draft</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,6 +46,11 @@
|
|||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="https://git.heaust.org/heaust/yawn">Source</a>
|
<a href="https://git.heaust.org/heaust/yawn">Source</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
<select class="theme-select" data-theme-select aria-label="Color theme">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
<button id="share-button" class="tool-button" type="button" hidden>
|
<button id="share-button" class="tool-button" type="button" hidden>
|
||||||
<span aria-hidden="true">↗</span> Share
|
<span aria-hidden="true">↗</span> Share
|
||||||
</button>
|
</button>
|
||||||
@@ -39,9 +64,15 @@
|
|||||||
|
|
||||||
<div class="playground-tools">
|
<div class="playground-tools">
|
||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
<button id="snippets-button" type="button"><span class="braces">{ }</span> Snippets</button>
|
<button id="snippets-button" type="button">
|
||||||
<button id="reset-button" type="button"><span aria-hidden="true">↶</span> Reset</button>
|
<span class="braces">{ }</span> Snippets
|
||||||
<button id="format-button" type="button"><span aria-hidden="true">≡</span> Format</button>
|
</button>
|
||||||
|
<button id="reset-button" type="button">
|
||||||
|
<span aria-hidden="true">↶</span> Reset
|
||||||
|
</button>
|
||||||
|
<button id="format-button" type="button">
|
||||||
|
<span aria-hidden="true">≡</span> Format
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-context">
|
<div class="tool-context">
|
||||||
<span class="language-dot"></span>
|
<span class="language-dot"></span>
|
||||||
@@ -50,17 +81,29 @@
|
|||||||
<span id="type-status">Starting language service…</span>
|
<span id="type-status">Starting language service…</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-group tool-group-right">
|
<div class="tool-group tool-group-right">
|
||||||
<button id="inspector-button" type="button" aria-pressed="true"><span aria-hidden="true">▤</span> Inspector</button>
|
<button id="inspector-button" type="button" aria-pressed="true">
|
||||||
<button id="fullscreen-button" type="button" aria-label="Toggle fullscreen"><span aria-hidden="true">⛶</span></button>
|
<span aria-hidden="true">▤</span> Inspector
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="fullscreen-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle fullscreen"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">⛶</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workbench" id="workbench">
|
<div class="workbench" id="workbench">
|
||||||
<section class="editor-pane" aria-label="TypeScript editor">
|
<section class="editor-pane" aria-label="TypeScript editor">
|
||||||
<div class="pane-tabs">
|
<div class="pane-tabs">
|
||||||
<button class="pane-tab active" type="button"><span class="ts-icon">TS</span> scene.ts <i id="tab-dirty"></i></button>
|
<button class="pane-tab active" type="button">
|
||||||
|
<span class="ts-icon">TS</span> scene.ts <i id="tab-dirty"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="editor" class="editor">
|
||||||
|
<div class="editor-loading">Loading TypeScript editor…</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="editor" class="editor"><div class="editor-loading">Loading TypeScript editor…</div></div>
|
|
||||||
<footer class="editor-status">
|
<footer class="editor-status">
|
||||||
<span id="cursor-status">Ln 1, Col 1</span>
|
<span id="cursor-status">Ln 1, Col 1</span>
|
||||||
<span>Spaces: 2</span>
|
<span>Spaces: 2</span>
|
||||||
@@ -70,7 +113,13 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="resize-handle" id="resize-handle" role="separator" aria-orientation="vertical" aria-label="Resize editor and preview"></div>
|
<div
|
||||||
|
class="resize-handle"
|
||||||
|
id="resize-handle"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label="Resize editor and preview"
|
||||||
|
></div>
|
||||||
|
|
||||||
<section class="stage-pane" aria-label="Yawn output">
|
<section class="stage-pane" aria-label="Yawn output">
|
||||||
<div class="stage-toolbar">
|
<div class="stage-toolbar">
|
||||||
@@ -95,22 +144,73 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="inspector" id="inspector">
|
<div class="inspector" id="inspector">
|
||||||
<div class="inspector-tabs" role="tablist">
|
<div class="inspector-tabs" role="tablist">
|
||||||
<button type="button" class="active" role="tab" aria-selected="true" data-panel="console">Console <span id="console-count">0</span></button>
|
<button
|
||||||
<button type="button" role="tab" aria-selected="false" data-panel="profile">GPU profiler</button>
|
type="button"
|
||||||
<button id="clear-console" class="clear-console" type="button">Clear</button>
|
class="active"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
data-panel="console"
|
||||||
|
>
|
||||||
|
Console <span id="console-count">0</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
data-panel="profile"
|
||||||
|
>
|
||||||
|
GPU profiler
|
||||||
|
</button>
|
||||||
|
<button id="clear-console" class="clear-console" type="button">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="console-panel inspector-panel active"
|
||||||
|
id="console-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div class="console-empty" id="console-empty">
|
||||||
|
<span>›_</span> Output from <code>log()</code> appears here.
|
||||||
</div>
|
</div>
|
||||||
<div class="console-panel inspector-panel active" id="console-panel" role="tabpanel">
|
|
||||||
<div class="console-empty" id="console-empty"><span>›_</span> Output from <code>log()</code> appears here.</div>
|
|
||||||
<ol id="console-lines"></ol>
|
<ol id="console-lines"></ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-panel inspector-panel" id="profile-panel" role="tabpanel">
|
<div
|
||||||
|
class="profile-panel inspector-panel"
|
||||||
|
id="profile-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
<div class="profile-empty" id="profile-empty">
|
<div class="profile-empty" id="profile-empty">
|
||||||
<span class="pulse-icon">⌁</span>
|
<span class="pulse-icon">⌁</span>
|
||||||
<div><strong>Waiting for GPU timestamps</strong><small>Run the scene to inspect its physical passes.</small></div>
|
<div>
|
||||||
|
<strong>Waiting for GPU timestamps</strong
|
||||||
|
><small>Run the scene to inspect its physical passes.</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-results" id="profile-results" hidden>
|
<div class="profile-results" id="profile-results" hidden>
|
||||||
<div class="profile-summary"><div><small>Total GPU time</small><strong id="profile-total">—</strong></div><div><small>Readback latency</small><strong id="profile-latency">—</strong></div><div><small>Adapter</small><strong id="profile-adapter">—</strong></div></div>
|
<div class="profile-summary">
|
||||||
<table><thead><tr><th>Physical pass</th><th>GPU time</th></tr></thead><tbody id="profile-passes"></tbody></table>
|
<div>
|
||||||
|
<small>Total GPU time</small
|
||||||
|
><strong id="profile-total">—</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small>Readback latency</small
|
||||||
|
><strong id="profile-latency">—</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small>Adapter</small
|
||||||
|
><strong id="profile-adapter">—</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Physical pass</th>
|
||||||
|
<th>GPU time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="profile-passes"></tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,16 +220,66 @@
|
|||||||
|
|
||||||
<dialog id="snippets-dialog" class="snippets-dialog">
|
<dialog id="snippets-dialog" class="snippets-dialog">
|
||||||
<div class="dialog-heading">
|
<div class="dialog-heading">
|
||||||
<div><span>Code library</span><h2>Snippets</h2><p>Insert a focused building block at your cursor.</p></div>
|
<div>
|
||||||
<button type="button" data-close-dialog aria-label="Close snippets">×</button>
|
<span>Code library</span>
|
||||||
|
<h2>Snippets</h2>
|
||||||
|
<p>Insert a focused building block at your cursor.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-close-dialog aria-label="Close snippets">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="snippet-search">
|
||||||
|
<span>⌕</span
|
||||||
|
><input
|
||||||
|
type="search"
|
||||||
|
placeholder="Find a snippet"
|
||||||
|
aria-label="Find a snippet"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="snippet-search"><span>⌕</span><input type="search" placeholder="Find a snippet" aria-label="Find a snippet" /></div>
|
|
||||||
<div class="snippet-grid">
|
<div class="snippet-grid">
|
||||||
<button type="button" data-snippet="camera"><span class="snippet-icon">◎</span><div><strong>Orbit camera</strong><small>Pointer orbit and wheel zoom</small><code>ArcRotateCamera</code></div><b>+</b></button>
|
<button type="button" data-snippet="camera">
|
||||||
<button type="button" data-snippet="light"><span class="snippet-icon">☀</span><div><strong>Point light</strong><small>Warm, local illumination</small><code>PointLight</code></div><b>+</b></button>
|
<span class="snippet-icon">◎</span>
|
||||||
<button type="button" data-snippet="instances"><span class="snippet-icon">◇</span><div><strong>Mesh instances</strong><small>Clone shared geometry</small><code>mesh.clone()</code></div><b>+</b></button>
|
<div>
|
||||||
<button type="button" data-snippet="rows"><span class="snippet-icon">≋</span><div><strong>Shared rows</strong><small>Add application-owned hot data</small><code>scene.ensureRows()</code></div><b>+</b></button>
|
<strong>Orbit camera</strong
|
||||||
<button type="button" data-snippet="post"><span class="snippet-icon">✦</span><div><strong>Post processing</strong><small>Color grading and FXAA</small><code>batchGraphUpdates()</code></div><b>+</b></button>
|
><small>Pointer orbit and wheel zoom</small
|
||||||
|
><code>ArcRotateCamera</code>
|
||||||
|
</div>
|
||||||
|
<b>+</b>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-snippet="light">
|
||||||
|
<span class="snippet-icon">☀</span>
|
||||||
|
<div>
|
||||||
|
<strong>Point light</strong><small>Warm, local illumination</small
|
||||||
|
><code>PointLight</code>
|
||||||
|
</div>
|
||||||
|
<b>+</b>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-snippet="instances">
|
||||||
|
<span class="snippet-icon">◇</span>
|
||||||
|
<div>
|
||||||
|
<strong>Mesh instances</strong><small>Clone shared geometry</small
|
||||||
|
><code>mesh.clone()</code>
|
||||||
|
</div>
|
||||||
|
<b>+</b>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-snippet="rows">
|
||||||
|
<span class="snippet-icon">≋</span>
|
||||||
|
<div>
|
||||||
|
<strong>Shared rows</strong
|
||||||
|
><small>Add application-owned hot data</small
|
||||||
|
><code>scene.ensureRows()</code>
|
||||||
|
</div>
|
||||||
|
<b>+</b>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-snippet="post">
|
||||||
|
<span class="snippet-icon">✦</span>
|
||||||
|
<div>
|
||||||
|
<strong>Post processing</strong><small>Color grading and FXAA</small
|
||||||
|
><code>batchGraphUpdates()</code>
|
||||||
|
</div>
|
||||||
|
<b>+</b>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user