74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
|
|
import { CLOUD, FOG, HEART_EMPTY, HEART_FULL, HEART_HALF, MOON, PALETTE, RAIN, SNOW, STORM, SUN } from "@/lib/pixel-art";
|
|||
|
|
import type { Condition } from "@/lib/weather/types";
|
|||
|
|
|
|||
|
|
interface PixelArtProps {
|
|||
|
|
rows: string[];
|
|||
|
|
size: number; // default rendered width in px (a className can override it)
|
|||
|
|
label?: string; // omit for decorative art
|
|||
|
|
className?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Renders a text-drawn sprite (see lib/pixel-art.ts) as crisp SVG squares. */
|
|||
|
|
export function PixelArt({ rows, size, label, className }: PixelArtProps) {
|
|||
|
|
const width = rows[0].length;
|
|||
|
|
const height = rows.length;
|
|||
|
|
|
|||
|
|
const pixels = rows.flatMap((row, y) =>
|
|||
|
|
[...row].map((char, x) =>
|
|||
|
|
char === "." ? null : <rect key={`${x}-${y}`} x={x} y={y} width={1} height={1} fill={PALETTE[char]} />,
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<svg
|
|||
|
|
className={className}
|
|||
|
|
width={size}
|
|||
|
|
height={(size / width) * height}
|
|||
|
|
viewBox={`0 0 ${width} ${height}`}
|
|||
|
|
shapeRendering="crispEdges"
|
|||
|
|
role={label ? "img" : undefined}
|
|||
|
|
aria-label={label}
|
|||
|
|
aria-hidden={label ? undefined : true}
|
|||
|
|
>
|
|||
|
|
{pixels}
|
|||
|
|
</svg>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const SKY_ART: Record<Condition, string[]> = {
|
|||
|
|
clear: SUN,
|
|||
|
|
clouds: CLOUD,
|
|||
|
|
rain: RAIN,
|
|||
|
|
storm: STORM,
|
|||
|
|
snow: SNOW,
|
|||
|
|
fog: FOG,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** Picks the sprite for a condition; clear nights get the moon. */
|
|||
|
|
export function SkyIcon({ condition, isDay = true, size, className }: {
|
|||
|
|
condition: Condition;
|
|||
|
|
isDay?: boolean;
|
|||
|
|
size: number;
|
|||
|
|
className?: string;
|
|||
|
|
}) {
|
|||
|
|
const rows = condition === "clear" && !isDay ? MOON : SKY_ART[condition];
|
|||
|
|
return <PixelArt rows={rows} size={size} className={className} />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** A row of five hearts showing a 0–5 score in half steps. */
|
|||
|
|
export function HeartMeter({ hearts }: { hearts: number }) {
|
|||
|
|
const sprites = [0, 1, 2, 3, 4].map((i) => {
|
|||
|
|
if (hearts >= i + 1) return HEART_FULL;
|
|||
|
|
if (hearts >= i + 0.5) return HEART_HALF;
|
|||
|
|
return HEART_EMPTY;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="flex gap-[5px]" role="img" aria-label={`Comfort: ${hearts} of 5 hearts`}>
|
|||
|
|
{sprites.map((rows, i) => (
|
|||
|
|
<PixelArt key={i} rows={rows} size={22} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|