From ec8ded0313c48aff23a1d403a2813142a62ecb5c Mon Sep 17 00:00:00 2001 From: Jonathan Peris Date: Tue, 6 Oct 2026 22:30:05 -0300 Subject: [PATCH] feat(lab): pixel-art RPG x dev aesthetic design prototype Self-contained /lab/rpg/ prototype for the redesign direction: PICO-8 palette, notched pixel windows with editor-style tabs, Pixelify Sans + JetBrains Mono, a hand-drawn pixel sprite, JRPG dialogue and command menu, a character sheet and progress counters derived from data.ts/GitHub, and skill groups as equipment slots. Pixel art renders to build-time SVG (PixelArt.astro). Excluded from the sitemap and marked noindex; the live portfolio is untouched. Co-Authored-By: Claude Opus 5.5 (1M context) --- astro.config.ts | 12 +- src/components/rpg/PixelArt.astro | 46 ++ src/components/rpg/PixelPanel.astro | 48 +++ src/components/rpg/sprites.ts | 180 ++++++++ src/pages/lab/rpg.astro | 624 ++++++++++++++++++++++++++++ 5 files changed, 909 insertions(+), 1 deletion(-) create mode 100644 src/components/rpg/PixelArt.astro create mode 100644 src/components/rpg/PixelPanel.astro create mode 100644 src/components/rpg/sprites.ts create mode 100644 src/pages/lab/rpg.astro diff --git a/astro.config.ts b/astro.config.ts index 314169a..e54f8ea 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -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: [ { @@ -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], diff --git a/src/components/rpg/PixelArt.astro b/src/components/rpg/PixelArt.astro new file mode 100644 index 0000000..0a248b8 --- /dev/null +++ b/src/components/rpg/PixelArt.astro @@ -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; + /** 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; + } +}); +--- + + + {rects.map(({ x, y, w, fill }) => )} + diff --git a/src/components/rpg/PixelPanel.astro b/src/components/rpg/PixelPanel.astro new file mode 100644 index 0000000..cf7243a --- /dev/null +++ b/src/components/rpg/PixelPanel.astro @@ -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; +--- + + + {tab && } + + + + diff --git a/src/components/rpg/sprites.ts b/src/components/rpg/sprites.ts new file mode 100644 index 0000000..94912ce --- /dev/null +++ b/src/components/rpg/sprites.ts @@ -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 }; + +/** 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..", + ], +}; diff --git a/src/pages/lab/rpg.astro b/src/pages/lab/rpg.astro new file mode 100644 index 0000000..0917f8f --- /dev/null +++ b/src/pages/lab/rpg.astro @@ -0,0 +1,624 @@ +--- +/** + * Design prototype: pixel-art RPG x dev aesthetics. + * Self-contained (own shell and styles) so the live portfolio is untouched; excluded from the sitemap and indexing. + */ +import { Font } from "astro:assets"; +import PixelArt from "../../components/rpg/PixelArt.astro"; +import PixelPanel from "../../components/rpg/PixelPanel.astro"; +import { CHEST, DATABASE, HEART, HERO, MONITOR, SCROLL, SHIELD, SPELLBOOK, SWORD, type Sprite } from "../../components/rpg/sprites"; +import { AVAILABILITY, CURRENT_ROLE, EXPERIENCES, PROFILE, SKILLS, SKILL_GROUPS, YEARS_OF_EXPERIENCE } from "../../lib/data"; +import { fetchRepos } from "../../lib/github"; + +const repos = await fetchRepos(); + +const level = Number.parseInt(YEARS_OF_EXPERIENCE, 10); +const questCount = EXPERIENCES.length; +const guildCount = new Set(EXPERIENCES.map((exp) => exp.company)).size; +const MAX_ICONS = 16; + +const counters: Array<{ label: string; value: string; count: number; sprite: Sprite }> = [ + { label: "Years in production", value: YEARS_OF_EXPERIENCE, count: level, sprite: HEART }, + { label: "Quests completed", value: `${questCount} roles`, count: questCount, sprite: SCROLL }, + { label: "Public builds", value: `${repos.length} repos`, count: repos.length, sprite: CHEST }, +]; + +const sheet = [ + { key: "NAME", value: PROFILE.name }, + { key: "CLASS", value: "Backend Architect" }, + { key: "LV", value: `${level} (${YEARS_OF_EXPERIENCE} yrs XP)` }, + { key: "GUILD", value: CURRENT_ROLE ? `${CURRENT_ROLE.company} — ${CURRENT_ROLE.title}` : "Freelance" }, + { key: "HOME", value: `${PROFILE.address.locality}, ${PROFILE.address.countryCode} · remote` }, + { key: "PARTY", value: `${guildCount} guilds joined` }, +]; + +const SLOTS: Record<(typeof SKILL_GROUPS)[number]["key"], { slot: string; sprite: Sprite }> = { + backend: { slot: "Main hand", sprite: SWORD }, + architecture: { slot: "Spellbook", sprite: SPELLBOOK }, + cloud: { slot: "Armor", sprite: SHIELD }, + databases: { slot: "Relic", sprite: DATABASE }, + languages: { slot: "Tongues", sprite: SCROLL }, + frontend: { slot: "Off-hand", sprite: MONITOR }, +}; + +const commands = [ + { label: "View resume", href: "/resume/", hint: "open the full stat sheet" }, + { label: "Talk", href: "https://www.linkedin.com/in/jonathan-peris/", hint: "LinkedIn", external: true }, + { label: "Equipment", href: "#equipment", hint: "skills & gear" }, + { label: "Quest log", href: "/#trace", hint: "experience" }, +]; +--- + + + + + + + + + Prototype · Jonathan Peris RPG + + + + + + + +
+ + +

SAVE 01 LV {level} · {YEARS_OF_EXPERIENCE} yrs

+
+ +
+
+
+ +
+ +
+ +
+

{PROFILE.name}

+

Lv.{level} Backend Architect

+
+
+ +
+ +

JONATHAN

+

+ I build backend systems that can be understood, operated, and changed after they meet production traffic. +

+

.NET · Azure · CQRS · DDD — {YEARS_OF_EXPERIENCE} years of production code.

+ +
+ +

STATUS: AVAILABLE {AVAILABILITY.short}

+ + + + +
+
+ +
+ +

Character sheet

+
+ {sheet.map(({ key, value }) => ( +
+
{key}
+
{value}
+
+ ))} +
+
+ + +

Progress

+
    + {counters.map(({ label, value, count, sprite }) => ( +
  • +

    {label}{value}

    + +
  • + ))} +
+
+
+ +
+ +

Equipment

+
    + {SKILL_GROUPS.map(({ key, label }) => ( +
  • + +
    +

    {SLOTS[key].slot} // {label}

    +

    {SKILLS[key].join(" · ")}

    +
    +
  • + ))} +
+
+
+
+ + + + + + + +