Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export default defineConfig({
site: 'https://jonathanperis.github.io',
outDir: 'out',
trailingSlash: 'always',
integrations: [sitemap()],
// /lab/ holds design prototypes: never listed in the sitemap.
integrations: [sitemap({ filter: (page) => !page.includes('/lab/') })],
// Fonts are downloaded at build time and served from this origin (no runtime Google Fonts request).
fonts: [
{
Expand All @@ -27,6 +28,15 @@ export default defineConfig({
subsets: ['latin', 'latin-ext'],
fallbacks: ['monospace'],
},
{
provider: fontProviders.google(),
name: 'Pixelify Sans',
cssVariable: '--font-pixelify',
weights: ['400 700'],
styles: ['normal'],
subsets: ['latin', 'latin-ext'],
fallbacks: ['monospace'],
},
],
vite: {
plugins: [tailwindcss() as never],
Expand Down
46 changes: 46 additions & 0 deletions src/components/rpg/PixelArt.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
/**
* Renders pixel art from a character grid as a crisp SVG at build time.
* Each character in `rows` is one pixel; `.` (or any key missing from `palette`) is transparent.
* Horizontal runs of the same color merge into one rect to keep the markup small.
*/
interface Props {
rows: readonly string[];
palette: Record<string, string>;
/** CSS size of one art pixel. */
scale?: number;
label?: string;
class?: string;
}

const { rows, palette, scale = 4, label, class: className } = Astro.props;

const width = Math.max(...rows.map((row) => row.length));
const height = rows.length;

const rects: Array<{ x: number; y: number; w: number; fill: string }> = [];
rows.forEach((row, y) => {
let x = 0;
while (x < row.length) {
const key = row[x];
let run = 1;
while (row[x + run] === key) run++;
if (Object.hasOwn(palette, key)) rects.push({ x, y, w: run, fill: palette[key] });
x += run;
}
});
---

<svg
class:list={["pixel-art", className]}
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${width} ${height}`}
width={width * scale}
height={height * scale}
shape-rendering="crispEdges"
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : "true"}
>
{rects.map(({ x, y, w, fill }) => <rect x={x} y={y} width={w} height="1" fill={fill} />)}
</svg>
48 changes: 48 additions & 0 deletions src/components/rpg/PixelPanel.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
/** JRPG-style window: notched pixel border, hard drop shadow, and an editor-tab style title. */
interface Props {
tab?: string;
class?: string;
as?: "section" | "div" | "aside";
labelledby?: string;
}

const { tab, class: className, as: Tag = "div", labelledby } = Astro.props;
---

<Tag class:list={["pixel-panel", className]} aria-labelledby={labelledby}>
{tab && <span class="pixel-panel-tab" aria-hidden="true">{tab}</span>}
<slot />
</Tag>

<style>
.pixel-panel {
position: relative;
margin: 4px;
padding: 1.6rem 1.4rem 1.4rem;
color: var(--rpg-paper);
background: linear-gradient(180deg, #24346a 0%, var(--rpg-navy) 55%, #141d3d 100%);
/* Four edge shadows without corners give the notched pixel border; the inset ring is the inner bevel. */
box-shadow:
0 -4px 0 0 var(--rpg-paper),
0 4px 0 0 var(--rpg-paper),
-4px 0 0 0 var(--rpg-paper),
4px 0 0 0 var(--rpg-paper),
inset 0 0 0 4px #10172e,
inset 0 4px 0 4px rgb(255 255 255 / 0.06);
filter: drop-shadow(8px 8px 0 var(--rpg-ink));
}

.pixel-panel-tab {
position: absolute;
top: -14px;
left: 18px;
padding: 2px 10px;
background: var(--rpg-ink);
color: var(--rpg-yellow);
font-family: var(--font-jetbrains-mono);
font-size: 0.72rem;
letter-spacing: 0.04em;
box-shadow: 0 0 0 2px var(--rpg-paper);
}
</style>
180 changes: 180 additions & 0 deletions src/components/rpg/sprites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Pixel-art sprites for the RPG prototype, drawn as character grids for PixelArt.astro.
* Colors follow the PICO-8 16-color palette, plus a few character tones.
*/

export const PICO = {
ink: "#0b0d1a",
navy: "#1d2b53",
plum: "#7e2553",
leaf: "#008751",
rust: "#ab5236",
stone: "#5f574f",
mist: "#c2c3c7",
paper: "#fff1e8",
red: "#ff004d",
orange: "#ffa300",
yellow: "#ffec27",
green: "#00e436",
sky: "#29adff",
lavender: "#83769c",
pink: "#ff77a8",
peach: "#ffccaa",
} as const;

export type Sprite = { rows: readonly string[]; palette: Record<string, string> };

/** Jonathan: dark hair, beard, green hoodie with drawstrings, holding an open laptop (lid and logo face the viewer). */
export const HERO: Sprite = {
palette: {
K: PICO.ink,
H: "#2a1a14",
h: "#4a3226",
S: "#d08b5b",
s: "#a8653a",
B: "#3b2418",
G: "#00b543",
g: PICO.leaf,
L: PICO.mist,
l: PICO.stone,
W: PICO.sky,
w: PICO.paper,
P: PICO.navy,
O: PICO.stone,
},
rows: [
"......KKKKKKKK......",
".....KHHHHHHHHK.....",
"....KHHhHHHHhHHK....",
"....KHHHHHHHHHHK....",
"....KHSSSSSSSSHK....",
"....KSSSSSSSSSSK....",
"....KSKKSSSSKKSK....",
"...KsSSSSSSSSSSsK...",
"....KSSSSssSSSSK....",
"....KBSSSSSSSSBK....",
"....KBBBSKKSBBBK....",
".....KBBBBBBBBK.....",
"......KKBBBBKK......",
".....KGGKSSKGGK.....",
"...KKGGGGKKGGGGKK...",
"..KGGGGGwGGwGGGGGK..",
"..KGgGGGwGGwGGGgGK..",
"..KGKKKKKKKKKKKKGK..",
"..KGKLLLLLLLLLLKGK..",
"..KSKLLLLWWLLLLKSK..",
"..KSKLLLLWWLLLLKSK..",
"...KKllllllllllKK...",
"....KPPPPPPPPPPK....",
"....KPPPPKKPPPPK....",
"....KPPPK..KPPPK....",
"....KOOOK..KOOOK....",
"....KKKKK..KKKKK....",
],
};

export const HEART: Sprite = {
palette: { R: PICO.red, r: PICO.peach },
rows: [
".RR.RR.",
"RrRRRRR",
"RRRRRRR",
".RRRRR.",
"..RRR..",
"...R...",
],
};

export const SCROLL: Sprite = {
palette: { T: PICO.peach, t: PICO.rust, n: PICO.stone },
rows: [
".tTTTTt",
"..TnnT.",
"..TTTT.",
"..TnnT.",
"..TTTT.",
".tTTTTt",
],
};

export const CHEST: Sprite = {
palette: { o: PICO.rust, d: "#6b3a1f", Y: PICO.yellow },
rows: [
".dddddd.",
"dooooood",
"dddYYddd",
"dooYYood",
"dooooood",
"dddddddd",
],
};

export const SWORD: Sprite = {
palette: { W: PICO.mist, w: PICO.paper, Y: PICO.yellow, b: PICO.rust },
rows: [
"......WW",
".....WwW",
"....WwW.",
"...WwW..",
"Y.WwW...",
".YwW....",
".bY.....",
"b..Y....",
],
};

export const SPELLBOOK: Sprite = {
palette: { V: PICO.plum, v: PICO.pink, Y: PICO.yellow, T: PICO.paper },
rows: [
"VVVVVVV.",
"VvvvvvVT",
"VvYYvvVT",
"VvYYvvVT",
"VvvvvvVT",
"VvvvvvVT",
"VVVVVVVT",
".TTTTTTT",
],
};

export const SHIELD: Sprite = {
palette: { U: PICO.sky, u: PICO.navy, w: PICO.paper, Y: PICO.yellow },
rows: [
"UUUUUUUU",
"UwwUUuuU",
"UwUUUUuU",
"UUUYYUUU",
"UUUYYUUU",
".UUUUUu.",
"..UUUu..",
"...UU...",
],
};

export const DATABASE: Sprite = {
palette: { A: PICO.mist, a: PICO.stone, G: PICO.green },
rows: [
".AAAAAA.",
"AaaaaaaA",
".AAAAAA.",
"AAAAAAAA",
"AGAAAAAA",
"AAAAAAAA",
"AGAAAAAA",
".AAAAAA.",
],
};

export const MONITOR: Sprite = {
palette: { M: PICO.stone, m: PICO.navy, G: PICO.green },
rows: [
"MMMMMMMM",
"MmmmmmmM",
"MmGGmmmM",
"MmmGGmmM",
"MmGGmmmM",
"MmmmmmmM",
"MMMMMMMM",
"..MMMM..",
],
};
Loading
Loading