1
0

The surface library can be photographed

`/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>
This commit is contained in:
2026-08-06 23:50:23 -07:00
parent 06455f7424
commit 18dadda917
2 changed files with 390 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
<!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>