Files
AgentCompany/landing/lib/palette.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

87 lines
4.0 KiB
TypeScript

/** One 32-colour palette; the scene keeps its indices while day part and season swap the colours. */
export type DayPart = "dawn" | "day" | "dusk" | "night";
export type Season = "spring" | "summer" | "autumn" | "winter";
export const PALETTE_SIZE = 32;
/** Named indices every surface draws with; art never uses raw colours. */
export const INK = {
void: 0, shadow: 1, ink: 2, inkSoft: 3, stone: 4, stoneLight: 5, paper: 6, white: 7,
sky0: 8, sky1: 9, sky2: 10, sky3: 11, sun: 12, glow: 13, cloud: 14, star: 15,
wood: 16, woodDark: 17, brick: 18, rug: 19, leaf0: 20, leaf1: 21, leaf2: 22, bloom: 23,
accent: 24, accentDark: 25, gold: 26, mint: 27, sea: 28, berry: 29, skin: 30, screen: 31,
} as const;
export type InkName = keyof typeof INK;
const DAY: string[] = [
"#0b0a12", "#1d1b2b", "#2f2c40", "#4a4660", "#6d6884", "#9a95ad", "#e9e2cf", "#fbf7ec",
"#3f7fd1", "#68a2e6", "#9cc6f2", "#d4e8fa", "#ffe08a", "#fff2c2", "#f4f7fb", "#ffffff",
"#a8693c", "#6e3f23", "#b4533f", "#5d6ea8", "#2f6b3a", "#4f9a4a", "#8cc95f", "#f2a3c0",
"#e0663d", "#9c3b22", "#f2b632", "#5fd3a8", "#2a8fa8", "#c4466f", "#f1c79a", "#7ef0c2",
];
/** How a day part shifts each index: sky and light are replaced, everything else is tinted. */
const DAY_PART_SKY: Record<DayPart, string[]> = {
dawn: ["#5b4a7a", "#c77a8e", "#f2a98a", "#fbd9b0", "#ffb36b", "#ffd9a8", "#f7d6d8", "#fff4e6"],
day: DAY.slice(8, 16),
dusk: ["#2d2350", "#7a3d6e", "#d0625a", "#f4a259", "#ff8c42", "#ffc078", "#e9b0a0", "#ffe7c7"],
night: ["#05060f", "#0d1330", "#1b2650", "#2c3b70", "#e6e9ff", "#9fb2ff", "#3a4470", "#fff8d6"],
};
const DAY_PART_TINT: Record<DayPart, { mix: string; amount: number }> = {
dawn: { mix: "#f3b5a8", amount: 0.18 },
day: { mix: "#ffffff", amount: 0 },
dusk: { mix: "#f08a4b", amount: 0.22 },
night: { mix: "#1a2350", amount: 0.55 },
};
const SEASON_LEAVES: Record<Season, [string, string, string, string]> = {
spring: ["#3f7a3c", "#5fae52", "#9ed46c", "#f6a8c8"],
summer: ["#235c2e", "#3f8f3f", "#78c050", "#ffd35c"],
autumn: ["#7a3a1c", "#c2622a", "#e8a33d", "#d9483b"],
winter: ["#4c6670", "#8aa6ad", "#dfeaee", "#ffffff"],
};
export function dayPartAt(date: Date): DayPart {
const hour = date.getHours();
if (hour >= 5 && hour < 8) return "dawn";
if (hour >= 8 && hour < 17) return "day";
if (hour >= 17 && hour < 20) return "dusk";
return "night";
}
export function seasonAt(date: Date): Season {
const month = date.getMonth();
if (month >= 2 && month <= 4) return "spring";
if (month >= 5 && month <= 7) return "summer";
if (month >= 8 && month <= 10) return "autumn";
return "winter";
}
/** The 32 colours for a moment; index 31 (screens) and the accents stay lit at night. */
export function paletteFor(part: DayPart, season: Season): string[] {
const tint = DAY_PART_TINT[part];
const lit = new Set<number>([INK.screen, INK.accent, INK.gold, INK.mint, INK.glow, INK.star]);
const colours = DAY.map((colour, index) => (lit.has(index) ? colour : mix(colour, tint.mix, tint.amount)));
DAY_PART_SKY[part].forEach((colour, offset) => (colours[INK.sky0 + offset] = colour));
SEASON_LEAVES[season].forEach((colour, offset) => (colours[INK.leaf0 + offset] = colour));
return colours;
}
/** CSS custom properties --p0..--p31 plus named aliases (--ink-accent, ...). */
export function paletteCssVars(colours: string[]): Record<string, string> {
const vars: Record<string, string> = {};
colours.forEach((colour, index) => (vars[`--p${index}`] = colour));
for (const [name, index] of Object.entries(INK)) vars[`--ink-${kebab(name)}`] = colours[index]!;
return vars;
}
function kebab(name: string): string {
return name.replace(/[A-Z0-9]/g, (char) => `-${char.toLowerCase()}`);
}
function mix(a: string, b: string, amount: number): string {
const pa = Number.parseInt(a.slice(1), 16);
const pb = Number.parseInt(b.slice(1), 16);
const channel = (shift: number) => Math.round(((pa >> shift) & 255) * (1 - amount) + ((pb >> shift) & 255) * amount);
return `#${((channel(16) << 16) | (channel(8) << 8) | channel(0)).toString(16).padStart(6, "0")}`;
}