Files
AgentCompany/landing/components/world/sky.ts
T
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

103 lines
4.8 KiB
TypeScript

import { Color, Mesh, PlaneGeometry, ShaderMaterial, Vector2 } from "three";
import { INK, type DayPart } from "../../lib/palette";
const ORB: Record<DayPart, [number, number]> = { dawn: [0.16, 0.44], day: [0.18, 0.8], dusk: [0.84, 0.4], night: [0.8, 0.8] };
/** Screen-space backdrop: banded sky, sun or moon, drifting clouds, stars and a two-layer skyline with lit windows. */
export function createSky(): Mesh<PlaneGeometry, ShaderMaterial> {
const colour = () => ({ value: new Color() });
const material = new ShaderMaterial({
uniforms: {
sky0: colour(), sky1: colour(), sky2: colour(), sky3: colour(), orb: colour(), star: colour(),
cloud: colour(), far: colour(), near: colour(), lit: colour(),
night: { value: 0 }, moon: { value: 0 }, time: { value: 0 },
resolution: { value: new Vector2(320, 180) }, orbPos: { value: new Vector2() }, offset: { value: new Vector2() },
},
depthTest: false,
depthWrite: false,
vertexShader: /* glsl */ `
void main() { gl_Position = vec4(position.xy, 1.0, 1.0); }
`,
fragmentShader: /* glsl */ `
uniform vec3 sky0, sky1, sky2, sky3, orb, star, cloud, far, near, lit;
uniform float night, moon, time;
uniform vec2 resolution, orbPos, offset;
const float BAYER[16] = float[16](0., 8., 2., 10., 12., 4., 14., 6., 3., 11., 1., 9., 15., 7., 13., 5.);
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
vec3 band(vec3 low, vec3 high, float t, float threshold) {
return clamp((t - 0.5) * 2.2 + 0.5, 0.0, 1.0) > threshold ? high : low;
}
bool cloudAt(vec2 px, float k) {
float span = resolution.x + 80.0;
vec2 c = vec2(mod(k * 137.0 + time * (1.5 + k * 0.6), span) - 40.0, resolution.y * (0.6 + 0.09 * k));
if (px.y < c.y - 1.0) return false;
vec2 d = px - c;
return length(d * vec2(0.55, 1.0)) < 5.0 + k || length(d - vec2(8.0, -1.0)) < 4.0 || length(d + vec2(9.0, 1.0)) < 3.5;
}
void main() {
vec2 px = floor(gl_FragCoord.xy);
float v = px.y / resolution.y;
ivec2 cell4 = ivec2(mod(px, 4.0));
float threshold = (BAYER[cell4.y * 4 + cell4.x] + 0.5) / 16.0;
vec3 c = v > 0.74 ? band(sky1, sky0, (v - 0.74) / 0.26, threshold)
: v > 0.48 ? band(sky2, sky1, (v - 0.48) / 0.26, threshold)
: band(sky3, sky2, clamp((v - 0.2) / 0.28, 0.0, 1.0), threshold);
float h = hash(px + floor(offset * 0.3));
if (moon > 0.5 && v > 0.42 && h > 0.996) c = mix(c, star, step(0.35, fract(h * 97.0 + time * 0.4)));
for (float k = 0.0; k < 3.0; k++) if (cloudAt(px + floor(offset * 0.25), k)) c = cloud;
vec2 o = orbPos * resolution + floor(offset * 0.2);
float d = length(px + 0.5 - o);
if (moon > 0.5) {
if (d < 7.0 && length(px + 0.5 - o - vec2(3.0, 2.0)) > 6.0) c = orb;
} else {
if (d < 14.0 && threshold < 0.3) c = orb;
if (d < 9.0) c = orb;
}
float unit = min(resolution.y, resolution.x * 0.9);
float fx = px.x + floor(offset.x * 0.35);
float fid = floor(fx / 9.0);
if (px.y < unit * (0.2 + hash(vec2(fid, 3.0)) * 0.16)) {
c = far;
vec2 cell = vec2(mod(fx, 9.0), mod(px.y, 5.0));
if (night > 0.5 && cell.x > 2.0 && cell.x < 7.0 && cell.y == 2.0 && hash(vec2(fid, floor(px.y / 5.0))) > 0.7) c = lit;
}
float nx = px.x + floor(offset.x * 0.6);
float nid = floor(nx / 13.0);
float top = unit * (0.08 + hash(vec2(nid, 9.0)) * 0.15);
if (px.y < top) {
c = near;
vec2 cell = vec2(mod(nx, 13.0), mod(px.y, 4.0));
bool window = cell.x > 1.0 && cell.x < 11.0 && mod(cell.x, 3.0) == 2.0 && cell.y == 1.0 && px.y < top - 2.0;
if (window) c = night > 0.5 && hash(vec2(nx, floor(px.y / 4.0))) > 0.45 ? lit : mix(near, sky3, 0.35);
}
gl_FragColor = vec4(c, 1.0);
}
`,
});
const mesh = new Mesh(new PlaneGeometry(2, 2), material);
mesh.frustumCulled = false;
mesh.renderOrder = -1;
return mesh;
}
export function setSkyPalette(material: ShaderMaterial, colours: string[], part: DayPart): void {
const u = material.uniforms;
const set = (name: string, index: number) => (u[name]!.value as Color).set(colours[index]!);
set("sky0", INK.sky0); set("sky1", INK.sky1); set("sky2", INK.sky2); set("sky3", INK.sky3);
set("orb", INK.sun); set("star", INK.star); set("cloud", INK.cloud); set("lit", INK.gold);
(u.far!.value as Color).set(colours[INK.sky1]!).lerp(new Color(colours[INK.stone]!), 0.5);
(u.near!.value as Color).set(colours[INK.inkSoft]!).lerp(new Color(colours[INK.sky0]!), 0.3);
u.night!.value = part === "night" || part === "dusk" ? 1 : 0;
u.moon!.value = part === "night" ? 1 : 0;
(u.orbPos!.value as Vector2).set(...ORB[part]);
}