18dadda917
`/simulate/assets` on lumbridgecorp.com argues that the art in this repository is code — eight textures drawn on a 2D canvas from seeded value noise, thirty-five surface roles pairing one of those with a colour and a roughness, and no binary art anywhere, enforced by a CI job. It made that argument in about four thousand pixels of prose with nothing on it to look at, which is a strange way to talk about the appearance of things. `textures.mjs` writes the library out: one tile per texture kind drawn by `TextureBin`, multiplied by the colour of the first role that carries it, and a neutral tile beside it so a reader can see for themselves that the map has no hue of its own. The role table goes with them as data, so the site sets it in its own type rather than baking labels into an image. It is the one capture here that runs against Vite dev rather than `dist/`, and the reason is in the header of both files. `shots.mjs` and `films.mjs` photograph the *application*, and the application a visitor gets is the built bundle. This photographs a *module*. `TextureBin` is not reachable from the bundle — it exposes no names — and making it reachable would mean a third Vite entry, which would ship a texture-sheet page to tera.lumbridgecorp.com so that a script could screenshot it. Vite dev transforms `/src/assets/*.ts` on request, so the page imports the same files a reader opens on the repository. Nothing in it re-implements the tables in `materials.ts`: it asks `MaterialRegistry` for every role in `DEFAULT_INTERIOR_PALETTE` and reports what comes back, so a role that changes its texture or its roughness changes the sheet on the website with it. No GPU, no WebGL, no renderer string to check — Canvas2D and the browser's own WebP encoder, about two seconds for all sixteen tiles at 97 kB total. `check-no-binaries` still passes: these live under `scripts/`, and the tiles are written into the lumbridge-v4 checkout, not this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
4.8 KiB
HTML
122 lines
4.8 KiB
HTML
<!doctype html>
|
|
<!--
|
|
The surface library, drawn by the engine that ships it.
|
|
|
|
Served by Vite's dev server rather than out of `dist/`, which is the one place
|
|
this differs from every other script in here. The others photograph the built
|
|
bundle because what they are photographing is the *app* — a camera pose, a
|
|
city, an hour of light — and the built bundle is what a visitor gets.
|
|
|
|
This photographs a module. `TextureBin` and `MaterialRegistry` are not reachable
|
|
from `dist/index.js`: the bundle exposes no names, and adding a third Vite entry
|
|
to reach them would ship a texture-sheet page to tera.lumbridgecorp.com for the
|
|
sake of a screenshot. Vite dev transforms `/src/assets/*.ts` on request, so the
|
|
page below imports the very files a reader will open on the repository — which
|
|
is a shorter chain of custody than the bundle, not a longer one.
|
|
|
|
Nothing here is loaded by the app and nothing here is built.
|
|
-->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>tera — surface library</title>
|
|
<style>
|
|
body { margin: 0; background: #08090b; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script type="module">
|
|
import { MaterialRegistry } from "/src/assets/materials.ts";
|
|
import { DEFAULT_INTERIOR_PALETTE } from "/src/assets/palette.ts";
|
|
|
|
/**
|
|
* The registry is asked for every role, in the order the palette declares
|
|
* them, and answers with the material the office actually renders — the
|
|
* same object, from the same cache. Nothing here re-implements the table
|
|
* in `materials.ts`; if a role changes its texture or its roughness, this
|
|
* page changes with it and so does the sheet on the website.
|
|
*/
|
|
const registry = new MaterialRegistry({ quality: "high" });
|
|
const roles = Object.keys(DEFAULT_INTERIOR_PALETTE);
|
|
|
|
/** role -> what the engine gives it. */
|
|
const surfaces = roles.map((role) => {
|
|
const material = registry.get(role);
|
|
return {
|
|
role,
|
|
// `getHexString` converts back out of the linear working space, so
|
|
// this is the sRGB value the palette declared rather than three's
|
|
// internal one.
|
|
color: `#${material.color.getHexString()}`,
|
|
texture: material.map?.name ?? null,
|
|
roughness: material.roughness ?? null,
|
|
metalness: material.metalness ?? null,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* A tile: the texture as the map, multiplied by the colour of a role that
|
|
* carries it.
|
|
*
|
|
* Multiply is not an approximation of the shader — it is what the shader
|
|
* does. `map * color` is the whole of the diffuse term here, and drawing
|
|
* the map on its own would be truthful about `textures.ts` and misleading
|
|
* about what a floor looks like, because every texture in the library is
|
|
* deliberately near-white and modulates downward.
|
|
*
|
|
* The neutral map is emitted too, so the page can put the two side by
|
|
* side and let the reader see the split rather than read about it.
|
|
*/
|
|
function tile(kind, hex) {
|
|
const map = registry.textures.get(kind);
|
|
if (!map) throw new Error(`no texture drawn for ${kind}`);
|
|
const source = map.image;
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = source.width;
|
|
canvas.height = source.height;
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.drawImage(source, 0, 0);
|
|
if (hex) {
|
|
ctx.globalCompositeOperation = "multiply";
|
|
ctx.fillStyle = hex;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
return canvas.toDataURL("image/webp", 0.92);
|
|
}
|
|
|
|
window.__library = () => {
|
|
/** kind -> the roles that carry it, in palette order. */
|
|
const byKind = new Map();
|
|
for (const surface of surfaces) {
|
|
if (!surface.texture) continue;
|
|
const list = byKind.get(surface.texture) ?? [];
|
|
list.push(surface.role);
|
|
byKind.set(surface.texture, list);
|
|
}
|
|
|
|
const textures = [...byKind.entries()].map(([kind, carriedBy]) => {
|
|
// The first role in palette order, so the representative colour is
|
|
// decided by the data rather than by whoever ran the script.
|
|
const lead = surfaces.find((s) => s.role === carriedBy[0]);
|
|
return {
|
|
kind,
|
|
carriedBy,
|
|
leadRole: lead.role,
|
|
leadColor: lead.color,
|
|
tinted: tile(kind, lead.color),
|
|
neutral: tile(kind, null),
|
|
size: registry.textures.get(kind).image.width,
|
|
};
|
|
});
|
|
|
|
return { textures, surfaces };
|
|
};
|
|
|
|
// The tiles are drawn synchronously on import, so the harness has nothing
|
|
// to wait on but this flag.
|
|
document.body.dataset.ready = "1";
|
|
</script>
|
|
</body>
|
|
</html>
|