Files
ZacharyZhang-NYandClaude Opus 5.5 2d7af224dd
CI / Typecheck, lint, tests, e2e (push) Failing after 3m51s
Website: 16-bit pixel-art rebuild with a live Three.js office, downloads from Gitea v1.5.1
- Title screen over a real-time voxel office rendered at low resolution and quantized to a rotating 32-colour palette with Bayer dithering; day part and season re-light the scene and every surface.
- 16-bit UI kit: name-plate dialogs, menu cursor, inventory, XP bar, badges, NPC speech, GAME OVER with CONTINUE, hand-placed 12x12 icons, Fusion Pixel font (Latin subset preloaded).
- Sections as a game: quest log (onboarding), character select (AI CEO / AI Co-Founder), inventory (features), party roster (providers), live download XP, save point (all v1.5.1 builds), FAQ as NPC dialog, credits.
- Downloads and source point to the Gitea release; the download function only redirects to git.zacharyzhang.com.
- Tailwind and the old components removed; unit tests for platform/downloads, rewritten e2e suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:37:23 -04:00

65 lines
2.4 KiB
TypeScript

import { ShaderMaterial, Vector3, type Texture } from "three";
import { PALETTE_SIZE } from "../../lib/palette";
/** Snaps every scene pixel to the two nearest palette colours, choosing between them with a 4x4 Bayer matrix. */
export function createQuantizeMaterial(scene: Texture): ShaderMaterial {
return new ShaderMaterial({
uniforms: {
tScene: { value: scene },
palette: { value: Array.from({ length: PALETTE_SIZE }, () => new Vector3()) },
},
depthTest: false,
depthWrite: false,
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tScene;
uniform vec3 palette[${PALETTE_SIZE}];
varying vec2 vUv;
const float BAYER[16] = float[16](0., 8., 2., 10., 12., 4., 14., 6., 3., 11., 1., 9., 15., 7., 13., 5.);
const vec3 WEIGHT = vec3(0.55, 0.75, 0.4);
vec3 toSrgb(vec3 c) {
c = clamp(c, 0.0, 1.0);
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}
void main() {
vec3 colour = toSrgb(texture2D(tScene, vUv).rgb);
float best = 1e9;
float next = 1e9;
vec3 first = palette[0];
vec3 second = palette[0];
for (int i = 0; i < ${PALETTE_SIZE}; i++) {
vec3 delta = (palette[i] - colour) * WEIGHT;
float distance = dot(delta, delta);
if (distance < best) {
next = best; second = first;
best = distance; first = palette[i];
} else if (distance < next) {
next = distance; second = palette[i];
}
}
vec3 span = second - first;
float along = clamp(dot(colour - first, span) / max(dot(span, span), 1e-5) * 2.0 - 0.45, 0.0, 1.0);
ivec2 cell = ivec2(mod(gl_FragCoord.xy, 4.0));
float threshold = (BAYER[cell.y * 4 + cell.x] + 0.5) / 16.0;
gl_FragColor = vec4(along > threshold ? second : first, 1.0);
}
`,
});
}
export function setQuantizePalette(material: ShaderMaterial, colours: string[]): void {
const slots = material.uniforms.palette!.value as Vector3[];
colours.forEach((hex, index) => {
const value = Number.parseInt(hex.slice(1), 16);
slots[index]!.set(((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255);
});
}