Website: 16-bit pixel-art rebuild with a live Three.js office, downloads from Gitea v1.5.1
CI / Typecheck, lint, tests, e2e (push) Failing after 3m51s
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>
This commit is contained in:
1 parent
e374bc1e8e
commit
2d7af224dd
112 files changed
+5692
-4223
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
BasicShadowMap, Camera, HalfFloatType, Mesh, NearestFilter, OrthographicCamera, PlaneGeometry, Scene, Vector2,
|
||||
WebGLRenderer, WebGLRenderTarget,
|
||||
} from "three";
|
||||
import type { DayPart } from "../../lib/palette";
|
||||
import { createAgents } from "./agents";
|
||||
import { applyLighting, createLights } from "./lighting";
|
||||
import { buildOffice } from "./office";
|
||||
import { createQuantizeMaterial, setQuantizePalette } from "./quantize";
|
||||
import { createSky, setSkyPalette } from "./sky";
|
||||
import { UNIT_BOX, createInks, paintInks } from "./voxel";
|
||||
|
||||
export interface World {
|
||||
setPalette(colours: string[], part: DayPart): void;
|
||||
resize(width: number, height: number): void;
|
||||
setPointer(x: number, y: number): void;
|
||||
setScroll(progress: number): void;
|
||||
setFocusRight(right: boolean): void;
|
||||
setRunning(running: boolean): void;
|
||||
render(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
const ART_PIXELS = 320 * 180;
|
||||
const MAX_DPR = 2;
|
||||
const TARGET = { x: 1.6, y: 0.2, z: 0.4 };
|
||||
const RIGHT_SHARE = 0.55;
|
||||
|
||||
/** Real-time office diorama drawn at ~320×180, then palette-quantized onto the canvas. */
|
||||
export function createWorld(canvas: HTMLCanvasElement): World {
|
||||
const renderer = new WebGLRenderer({ canvas, antialias: false, alpha: false, powerPreference: "low-power" });
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = BasicShadowMap;
|
||||
|
||||
const target = new WebGLRenderTarget(320, 180, { type: HalfFloatType, minFilter: NearestFilter, magFilter: NearestFilter });
|
||||
const quantize = createQuantizeMaterial(target.texture);
|
||||
const post = new Scene();
|
||||
const postQuad = new Mesh(new PlaneGeometry(2, 2), quantize);
|
||||
postQuad.frustumCulled = false;
|
||||
post.add(postQuad);
|
||||
const postCamera = new Camera();
|
||||
|
||||
const scene = new Scene();
|
||||
const inks = createInks();
|
||||
const office = buildOffice(inks);
|
||||
const agents = createAgents(inks);
|
||||
const lights = createLights();
|
||||
const sky = createSky();
|
||||
scene.add(sky, office.root, agents.root, lights.sun, lights.sun.target, lights.sky);
|
||||
|
||||
const camera = new OrthographicCamera(-1, 1, 1, -1, 1, 120);
|
||||
const pointer = new Vector2();
|
||||
const eased = new Vector2();
|
||||
let scroll = 0;
|
||||
let focusRight = false;
|
||||
let aspect = 16 / 9;
|
||||
let time = 0;
|
||||
let last = 0;
|
||||
let frame = 0;
|
||||
|
||||
function place(): void {
|
||||
eased.lerp(pointer, 0.06);
|
||||
const yaw = Math.PI / 4 + Math.sin(time * 0.08) * 0.16 + eased.x * 0.12;
|
||||
const pitch = 0.56 + eased.y * 0.05;
|
||||
camera.position.set(
|
||||
TARGET.x + Math.sin(yaw) * Math.cos(pitch) * 50,
|
||||
TARGET.y + Math.sin(pitch) * 50,
|
||||
TARGET.z + Math.cos(yaw) * Math.cos(pitch) * 50,
|
||||
);
|
||||
camera.lookAt(TARGET.x, TARGET.y, TARGET.z);
|
||||
const share = focusRight ? RIGHT_SHARE : 1;
|
||||
const half = Math.max(10.6, 13 / (aspect * share)) / (1 + scroll * 0.18);
|
||||
const width = half * aspect * 2;
|
||||
const centre = focusRight ? 1 - share / 2 : 0.5;
|
||||
camera.top = half;
|
||||
camera.bottom = -half;
|
||||
camera.left = -width * centre;
|
||||
camera.right = width * (1 - centre);
|
||||
camera.updateProjectionMatrix();
|
||||
(sky.material.uniforms.offset!.value as Vector2).set(-eased.x * 14 - Math.sin(time * 0.08) * 16, 0);
|
||||
sky.material.uniforms.time!.value = time;
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
place();
|
||||
office.update(time);
|
||||
renderer.setRenderTarget(target);
|
||||
renderer.render(scene, camera);
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.render(post, postCamera);
|
||||
}
|
||||
|
||||
function tick(now: number): void {
|
||||
const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;
|
||||
last = now;
|
||||
time += dt;
|
||||
agents.update(dt, time);
|
||||
render();
|
||||
frame = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
return {
|
||||
setPalette(colours, part) {
|
||||
paintInks(inks, colours);
|
||||
setSkyPalette(sky.material, colours, part);
|
||||
setQuantizePalette(quantize, colours);
|
||||
applyLighting(lights, office, colours, part);
|
||||
},
|
||||
resize(width, height) {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
|
||||
const deviceWidth = width * dpr;
|
||||
const deviceHeight = height * dpr;
|
||||
const scale = Math.max(1, Math.round(Math.sqrt((deviceWidth * deviceHeight) / ART_PIXELS)));
|
||||
const artWidth = Math.ceil(deviceWidth / scale);
|
||||
const artHeight = Math.ceil(deviceHeight / scale);
|
||||
aspect = artWidth / artHeight;
|
||||
renderer.setSize(artWidth, artHeight, false);
|
||||
target.setSize(artWidth, artHeight);
|
||||
(sky.material.uniforms.resolution!.value as Vector2).set(artWidth, artHeight);
|
||||
canvas.style.width = `${(artWidth * scale) / dpr}px`;
|
||||
canvas.style.height = `${(artHeight * scale) / dpr}px`;
|
||||
},
|
||||
setPointer(x, y) {
|
||||
pointer.set(x, y);
|
||||
},
|
||||
setScroll(progress) {
|
||||
scroll = progress;
|
||||
},
|
||||
setFocusRight(right) {
|
||||
focusRight = right;
|
||||
},
|
||||
setRunning(running) {
|
||||
cancelAnimationFrame(frame);
|
||||
last = 0;
|
||||
if (running) frame = requestAnimationFrame(tick);
|
||||
},
|
||||
render() {
|
||||
eased.copy(pointer);
|
||||
render();
|
||||
},
|
||||
dispose() {
|
||||
cancelAnimationFrame(frame);
|
||||
office.dispose();
|
||||
inks.forEach((material) => material.dispose());
|
||||
UNIT_BOX.dispose();
|
||||
sky.geometry.dispose();
|
||||
sky.material.dispose();
|
||||
postQuad.geometry.dispose();
|
||||
quantize.dispose();
|
||||
lights.sun.dispose();
|
||||
lights.sky.dispose();
|
||||
target.dispose();
|
||||
renderer.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user