CI / Typecheck, lint, tests, e2e (push) Failing after 3m51s
- 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>
65 lines
2.4 KiB
TypeScript
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);
|
|
});
|
|
}
|