SOURCE / PINNED RELEASE
Made of little things.
Pursuit PIT
- Release
- 565f4981bdc3…
- Author-recorded commit
- 197afdbed44b…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7
Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// The time-of-day picker's pictures: the interstate from behind a car, by day, at dusk and at
// night. Drawn on a canvas (flat low-poly, like the title backdrop) so they need no image files.
import type { TimeOfDay } from './game/render.js';
interface Palette {
sky: [string, string, string]; // top, middle, horizon
hills: string;
ground: string;
road: string;
line: string;
car: string;
lit: boolean; // headlights, tail lights and street lamps on
stars: boolean;
orb?: { x: number; y: number; r: number; color: string; glow: string };
}
const PALETTES: Record<TimeOfDay, Palette> = {
day: {
sky: ['#4f93d8', '#8cc0ea', '#d6e8f2'], hills: '#7f9a62', ground: '#7d9150', road: '#5a5c60', line: '#f2f0e6',
car: '#2f5fb8', lit: false, stars: false, orb: { x: 0.8, y: 0.18, r: 0.07, color: '#fffbe8', glow: 'rgba(255,250,220,0.45)' },
},
dusk: {
sky: ['#2c2f66', '#b8577a', '#f6a35c'], hills: '#4a3f5c', ground: '#5a5a3c', road: '#3f3e44', line: '#d9cbb6',
car: '#2f5fb8', lit: true, stars: false, orb: { x: 0.22, y: 0.5, r: 0.09, color: '#ffd28a', glow: 'rgba(255,170,90,0.5)' },
},
night: {
sky: ['#03050f', '#0a1024', '#18213c'], hills: '#0d1220', ground: '#10161c', road: '#1c1e24', line: '#6d6f74',
car: '#1f3570', lit: true, stars: true, orb: { x: 0.78, y: 0.16, r: 0.045, color: '#eef0f6', glow: 'rgba(200,215,255,0.25)' },
},
};
export function drawTimeCard(canvas: HTMLCanvasElement, time: TimeOfDay): void {
const dpr = Math.min(2, window.devicePixelRatio || 1);
const W = Math.max(1, Math.round(canvas.clientWidth * dpr)), H = Math.max(1, Math.round(canvas.clientHeight * dpr));
canvas.width = W;
canvas.height = H;
const g = canvas.getContext('2d');
if (!g) return;
const p = PALETTES[time];
const horizon = H * 0.55, cx = W / 2;
// Sky, then the sun or moon and (at night) stars.
const sky = g.createLinearGradient(0, 0, 0, horizon);
sky.addColorStop(0, p.sky[0]);
sky.addColorStop(0.6, p.sky[1]);
sky.addColorStop(1, p.sky[2]);
g.fillStyle = sky;
g.fillRect(0, 0, W, horizon + 1);
let seed = 7;
const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
if (p.stars) {
g.fillStyle = '#e8ecf8';
for (let i = 0; i < 70; i++) g.fillRect(rnd() * W, rnd() * horizon * 0.85, rnd() < 0.15 ? 1.6 * dpr : 0.9 * dpr, 0.9 * dpr);
}
if (p.orb) {
const ox = p.orb.x * W, oy = p.orb.y * H, r = p.orb.r * H;
const glow = g.createRadialGradient(ox, oy, r * 0.5, ox, oy, r * 4);
glow.addColorStop(0, p.orb.glow);
glow.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = glow;
g.fillRect(ox - r * 4, oy - r * 4, r * 8, r * 8);
g.fillStyle = p.orb.color;
g.beginPath();
g.arc(ox, oy, r, 0, Math.PI * 2);
g.fill();
}
// Low hills along the horizon, then the ground.
g.fillStyle = p.hills;
g.beginPath();
g.moveTo(0, horizon);
for (let x = 0; x <= W; x += W / 12) g.lineTo(x, horizon - (0.04 + 0.05 * Math.abs(Math.sin(x * 0.013 + 1))) * H);
g.lineTo(W, horizon);
g.fill();
g.fillStyle = p.ground;
g.fillRect(0, horizon, W, H - horizon);
// The road running to the vanishing point, with dashed lane lines.
const half = (y: number) => ((y - horizon) / (H - horizon)) * W * 0.62;
g.fillStyle = p.road;
g.beginPath();
g.moveTo(cx - 2, horizon);
g.lineTo(cx + 2, horizon);
g.lineTo(cx + half(H), H);
g.lineTo(cx - half(H), H);
g.fill();
g.strokeStyle = p.line;
for (const k of [-1 / 3, 1 / 3]) {
for (let i = 0; i < 9; i++) {
const y0 = horizon + (H - horizon) * (i / 9) ** 2, y1 = horizon + (H - horizon) * ((i + 0.5) / 9) ** 2;
g.lineWidth = Math.max(1, ((y1 - horizon) / (H - horizon)) * 4 * dpr);
g.beginPath();
g.moveTo(cx + k * half(y0), y0);
g.lineTo(cx + k * half(y1), y1);
g.stroke();
}
}
// Street lamps down the left edge, glowing at night and dusk.
for (let i = 1; i < 5; i++) {
const y = horizon + (H - horizon) * (i / 5) ** 2;
const x = cx - half(y) * 1.08, h = (y - horizon) * 1.6;
g.strokeStyle = time === 'night' ? '#2a2d34' : '#55585c';
g.lineWidth = Math.max(1, (h / H) * 6 * dpr);
g.beginPath();
g.moveTo(x, y);
g.lineTo(x, y - h);
g.lineTo(x + h * 0.25, y - h);
g.stroke();
if (p.lit) {
const lx = x + h * 0.25, ly = y - h;
const glow = g.createRadialGradient(lx, ly, 0, lx, ly, h * 0.35);
glow.addColorStop(0, 'rgba(255,214,150,0.9)');
glow.addColorStop(1, 'rgba(255,214,150,0)');
g.fillStyle = glow;
g.fillRect(lx - h * 0.35, ly - h * 0.35, h * 0.7, h * 0.7);
}
}
// A car ahead, seen from behind, lighting the road in front of it after dark.
const cy = horizon + (H - horizon) * 0.62, cw = W * 0.2, ch = cw * 0.45;
if (p.lit) {
const beam = g.createRadialGradient(cx, cy - ch * 0.6, 0, cx, cy - ch * 0.6, cw * 1.4);
beam.addColorStop(0, time === 'night' ? 'rgba(255,240,200,0.55)' : 'rgba(255,240,200,0.3)');
beam.addColorStop(1, 'rgba(255,240,200,0)');
g.fillStyle = beam;
g.beginPath();
g.moveTo(cx - cw * 0.4, cy - ch * 0.4);
g.lineTo(cx + cw * 0.4, cy - ch * 0.4);
g.lineTo(cx + cw * 0.9, cy - ch * 2.2);
g.lineTo(cx - cw * 0.9, cy - ch * 2.2);
g.fill();
}
g.fillStyle = 'rgba(0,0,0,0.35)';
g.fillRect(cx - cw * 0.55, cy - ch * 0.08, cw * 1.1, ch * 0.18); // shadow
g.fillStyle = '#141416';
g.fillRect(cx - cw * 0.48, cy - ch * 0.25, cw * 0.16, ch * 0.3); // tyres
g.fillRect(cx + cw * 0.32, cy - ch * 0.25, cw * 0.16, ch * 0.3);
g.fillStyle = p.car;
g.fillRect(cx - cw / 2, cy - ch * 0.75, cw, ch * 0.6); // body
g.fillStyle = time === 'night' ? '#0b0e16' : '#2a3340';
g.beginPath(); // rear window and roof
g.moveTo(cx - cw * 0.36, cy - ch * 0.75);
g.lineTo(cx - cw * 0.28, cy - ch * 1.12);
g.lineTo(cx + cw * 0.28, cy - ch * 1.12);
g.lineTo(cx + cw * 0.36, cy - ch * 0.75);
g.fill();
for (const side of [-1, 1]) {
const tx = cx + side * cw * 0.36;
if (p.lit) {
const glow = g.createRadialGradient(tx, cy - ch * 0.55, 0, tx, cy - ch * 0.55, cw * 0.22);
glow.addColorStop(0, 'rgba(255,40,30,0.75)');
glow.addColorStop(1, 'rgba(255,40,30,0)');
g.fillStyle = glow;
g.fillRect(tx - cw * 0.22, cy - ch * 0.55 - cw * 0.22, cw * 0.44, cw * 0.44);
}
g.fillStyle = p.lit ? '#ff3a2c' : '#a3161c';
g.fillRect(tx - cw * 0.1, cy - ch * 0.62, cw * 0.2, ch * 0.14);
}
g.fillStyle = '#e6e1cc';
g.fillRect(cx - cw * 0.1, cy - ch * 0.42, cw * 0.2, ch * 0.12); // plate
}
