init
This commit is contained in:
commit
6250d23297
53 changed files with 6381 additions and 0 deletions
73
components/PixelArt.tsx
Normal file
73
components/PixelArt.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue