Removed the auto generated sprites

This commit is contained in:
pricewowen
2026-05-23 23:54:36 -06:00
parent 47303bb4eb
commit 25e9960d01
7 changed files with 110 additions and 582 deletions

View File

@@ -38,32 +38,26 @@ On the **Pet** tab, users rename their pet and switch between **Overview** and *
npm install
```
2. Generate placeholder character/shop sprites (if missing):
```bash
npm run generate:sprites
```
3. Create local environment file:
2. Create local environment file:
```bash
cp .env.example .env.local
```
4. Add Supabase values to `.env.local`:
3. Add Supabase values to `.env.local`:
```env
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
```
5. Apply migrations locally (Supabase CLI):
4. Apply migrations locally (Supabase CLI):
```bash
supabase db reset
```
6. Start the app:
5. Start the app:
```bash
npm run dev
@@ -87,7 +81,7 @@ Stored in `avatar_state`:
Style columns are validated against allowed variant ids (`none`, `pants-1`, …). Shop accessories overlay equipped variants at render time without overwriting saved customization.
Sprite assets: `public/character/{layer}/` (regenerate via `npm run generate:sprites`).
Sprite assets are added manually under `public/character/{layer}/` (e.g. `public/character/head/head-1.png`). Paths must match the variant ids in `lib/character/presets.ts` and `supabase/seed.sql`.
## Shop

View File

@@ -15,6 +15,8 @@ import {
import type { LayerColorState, LayerVariantState } from "@/lib/character/types";
import { cn } from "@/lib/utils";
const EMPTY_EQUIPPED_ITEMS: string[] = [];
type CharacterLayerPreviewProps = {
colors: LayerColorState;
variants: LayerVariantState;
@@ -27,16 +29,21 @@ type CharacterLayerPreviewProps = {
export function CharacterLayerPreview({
colors,
variants,
equippedItems = [],
equippedItems,
scale = 10,
className,
compact = false,
}: CharacterLayerPreviewProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [displaySize, setDisplaySize] = useState({ width: 0, height: 0 });
const resolvedEquippedItems = equippedItems ?? EMPTY_EQUIPPED_ITEMS;
const equippedKey = resolvedEquippedItems.join("\0");
const equippedItemsRef = useRef(resolvedEquippedItems);
equippedItemsRef.current = resolvedEquippedItems;
useEffect(() => {
let cancelled = false;
const items = equippedItemsRef.current;
async function render() {
const canvas = canvasRef.current;
@@ -45,62 +52,74 @@ export function CharacterLayerPreview({
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) return;
const displayVariants = applyEquippedItemsToVariants(
variants,
equippedItems,
);
const activeLayers = CHARACTER_LAYERS.flatMap((layer) => {
const selected = getSelectedVariant(layer.id, displayVariants[layer.id]);
if (!selected) return [];
return [{ layer, variant: selected }];
});
const images = await Promise.all(
activeLayers.map(async ({ layer, variant }) => ({
layer,
image: await getCachedImage(variant.src),
})),
);
if (cancelled || images.length === 0) return;
const width = images[0]?.image.width ?? 0;
const height = images[0]?.image.height ?? 0;
canvas.width = width;
canvas.height = height;
setDisplaySize({ width: width * scale, height: height * scale });
ctx.clearRect(0, 0, width, height);
for (const { layer, image } of images) {
const layerColor = colors[layer.id];
const rgb = hslToRgb(layerColor.h, layerColor.s, layerColor.l);
drawTintedSprite(
ctx,
image,
rgb,
layer.id === "eyes"
? {
mode: "eyes",
skinColor: hslToRgb(
colors.skin.h,
colors.skin.s,
colors.skin.l,
),
}
: undefined,
try {
const displayVariants = applyEquippedItemsToVariants(
variants,
items,
);
const activeLayers = CHARACTER_LAYERS.flatMap((layer) => {
const selected = getSelectedVariant(layer.id, displayVariants[layer.id]);
if (!selected) return [];
return [{ layer, variant: selected }];
});
const images = await Promise.all(
activeLayers.map(async ({ layer, variant }) => ({
layer,
image: await getCachedImage(variant.src),
})),
);
if (cancelled || images.length === 0) return;
const width = images[0]?.image.width ?? 0;
const height = images[0]?.image.height ?? 0;
canvas.width = width;
canvas.height = height;
const nextWidth = width * scale;
const nextHeight = height * scale;
setDisplaySize((current) =>
current.width === nextWidth && current.height === nextHeight
? current
: { width: nextWidth, height: nextHeight },
);
ctx.clearRect(0, 0, width, height);
for (const { layer, image } of images) {
const layerColor = colors[layer.id];
const rgb = hslToRgb(layerColor.h, layerColor.s, layerColor.l);
drawTintedSprite(
ctx,
image,
rgb,
layer.id === "eyes"
? {
mode: "eyes",
skinColor: hslToRgb(
colors.skin.h,
colors.skin.s,
colors.skin.l,
),
}
: undefined,
);
}
} catch {
// Ignore missing or broken sprite assets so the UI stays responsive.
}
}
render();
void render();
return () => {
cancelled = true;
};
}, [colors, variants, equippedItems, scale]);
}, [colors, variants, equippedKey, scale]);
return (
<div

View File

@@ -37,35 +37,39 @@ export function TintedSpriteIcon({
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) return;
const image = await getCachedImage(src);
if (cancelled) return;
try {
const image = await getCachedImage(src);
if (cancelled) return;
canvas.width = image.width;
canvas.height = image.height;
ctx.clearRect(0, 0, image.width, image.height);
canvas.width = image.width;
canvas.height = image.height;
ctx.clearRect(0, 0, image.width, image.height);
const rgb = hslToRgb(color.h, color.s, color.l);
const isEyesLayer = src.includes("/character/eyes/");
const rgb = hslToRgb(color.h, color.s, color.l);
const isEyesLayer = src.includes("/character/eyes/");
drawTintedSprite(
ctx,
image,
rgb,
isEyesLayer && skinColor
? {
mode: "eyes",
skinColor: hslToRgb(skinColor.h, skinColor.s, skinColor.l),
}
: undefined,
);
drawTintedSprite(
ctx,
image,
rgb,
isEyesLayer && skinColor
? {
mode: "eyes",
skinColor: hslToRgb(skinColor.h, skinColor.s, skinColor.l),
}
: undefined,
);
} catch {
// Ignore missing or broken sprite assets so the UI stays responsive.
}
}
render();
void render();
return () => {
cancelled = true;
};
}, [src, color, skinColor]);
}, [src, color.h, color.s, color.l, skinColor?.h, skinColor?.s, skinColor?.l]);
return (
<canvas

View File

@@ -3,7 +3,6 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
import { CharacterLayerPreview } from "@/components/character/character-layer-preview";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -13,7 +12,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { defaultAvatarCustomization } from "@/lib/avatar-customization-storage";
import {
focusTopicOptions,
saveOnboardingPreferences,
@@ -52,25 +50,16 @@ export function OnboardingQuizForm() {
return (
<div className="min-h-svh bg-background">
<main className="mx-auto flex min-h-svh max-w-lg flex-col justify-center px-5 py-10">
<div className="mb-8 flex flex-col items-center gap-4 text-center">
<CharacterLayerPreview
colors={defaultAvatarCustomization.colors}
variants={defaultAvatarCustomization.variants}
scale={6}
compact
className="max-w-[12rem]"
/>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
Step 1 of 2
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
Welcome to Habit Pet
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Choose a focus area, then design your pet in the next step.
</p>
</div>
<div className="mb-8 text-center">
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
Step 1 of 2
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
Welcome to Habit Pet
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Choose a focus area, then design your pet in the next step.
</p>
</div>
<Card>

321
package-lock.json generated
View File

@@ -10,8 +10,8 @@
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-slot": "^1.2.2",
"@supabase/ssr": "^0.10.3",
"@supabase/supabase-js": "^2.106.1",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.511.0",
@@ -29,10 +29,7 @@
"autoprefixer": "^10.4.20",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"pg": "^8.16.0",
"pngjs": "^7.0.0",
"postcss": "^8",
"supabase": "^2.101.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5",
@@ -2955,130 +2952,6 @@
"node": ">=20.0.0"
}
},
"node_modules/@supabase/cli-darwin-arm64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.101.0.tgz",
"integrity": "sha512-SoWYzu2CIE+rADWsZH6H0YZYF8nV2YMelZLbjAqzahb5RRgFOTMWGhKCUj6g4KlNvocUnU19JObUJ7oCx/9aHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@supabase/cli-darwin-x64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.101.0.tgz",
"integrity": "sha512-rPES03BF9KYkirq3X1Yll+T+eORueRvC858Yp2+2aQMvB4qxi6k+WI+ilCQ7NGzOJ3G0jl/ZG/KsSjZIa4AJ6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@supabase/cli-linux-arm64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.101.0.tgz",
"integrity": "sha512-E+hN1GRIFEi+U1Jf4Omw/gjwpSRTpJ5jai58tQana15AaXt3GCoG7z2YzWVnzD/mvAubTqA7SLHsV+A2lcdTGQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-arm64-musl": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.101.0.tgz",
"integrity": "sha512-B5JFFKKpJLLMwn8vWUB7j5euZPytiDOLo9WYXp0mshg45OaFKjRcDImSzBzx4k0W8MK2x75rFEURca90Ic60Ug==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-x64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.101.0.tgz",
"integrity": "sha512-7iU9uA+8pbmW5uz+yGxU0fM83a+8SH/gtKmSSRZrOu9vIGcugg9gLbN9GlWkG5P7I7wEjnoVKb7Jzea6onSeQA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-x64-musl": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.101.0.tgz",
"integrity": "sha512-pr9Ar1/aVaQVqjNSfwRgXHWJznQTIaxblGO+hKIl9k55Ajahiq8GConCwx6DjX1Wa9VW//5Dji+O0pkmFSfcHw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-windows-arm64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.101.0.tgz",
"integrity": "sha512-mSnFDhTIAe9cr1iifv+DTQzpY/0WphywED2YT3yMGeu94jjxrXrreQkywxEaeSXTnc4ys1umtnWOB1CaBlYKag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@supabase/cli-windows-x64": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.101.0.tgz",
"integrity": "sha512-xzZpybVWq62VF102abZr1EP3BbA2bYecwYm3G2dJk+6ua7nAIodu1fiGHAvBSR0BLGQmesNb6KCneawNL3XBsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@supabase/functions-js": {
"version": "2.106.1",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.1.tgz",
@@ -9024,103 +8897,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/pg": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.13.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.14.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -9160,16 +8936,6 @@
"node": ">= 6"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -9365,49 +9131,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -10237,16 +9960,6 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/srvx": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.8.9.tgz",
@@ -10547,26 +10260,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supabase": {
"version": "2.101.0",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.101.0.tgz",
"integrity": "sha512-9wqlvXGeI+VjOMUKOGEpcZzTJGLia6IOv8LWN3W2u8F6Xiv4ZqAuTuXCp3MWfkhV5x9KQaBtN5d6IiLEMWzkJA==",
"dev": true,
"license": "MIT",
"bin": {
"supabase": "dist/supabase.js"
},
"optionalDependencies": {
"@supabase/cli-darwin-arm64": "2.101.0",
"@supabase/cli-darwin-x64": "2.101.0",
"@supabase/cli-linux-arm64": "2.101.0",
"@supabase/cli-linux-arm64-musl": "2.101.0",
"@supabase/cli-linux-x64": "2.101.0",
"@supabase/cli-linux-x64-musl": "2.101.0",
"@supabase/cli-windows-arm64": "2.101.0",
"@supabase/cli-windows-x64": "2.101.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11517,16 +11210,6 @@
"node": ">= 6.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View File

@@ -5,8 +5,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"generate:sprites": "node scripts/generate-character-sprites.mjs"
"lint": "eslint ."
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.1",
@@ -32,7 +31,6 @@
"autoprefixer": "^10.4.20",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"pngjs": "^7.0.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",

View File

@@ -1,159 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { PNG } from "pngjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const publicDir = path.join(root, "public");
const W = 32;
const H = 48;
const RED = [255, 0, 0];
const DARK_RED = [51, 0, 0];
const WHITE = [255, 255, 255];
const MAGENTA = [255, 0, 255];
function createCanvas() {
return new PNG({ width: W, height: H });
}
function setPixel(png, x, y, [r, g, b], alpha = 255) {
if (x < 0 || y < 0 || x >= W || y >= H) {
return;
}
const index = (W * y + x) << 2;
png.data[index] = r;
png.data[index + 1] = g;
png.data[index + 2] = b;
png.data[index + 3] = alpha;
}
function fillRect(png, x, y, width, height, color, alpha = 255) {
for (let row = 0; row < height; row += 1) {
for (let col = 0; col < width; col += 1) {
setPixel(png, x + col, y + row, color, alpha);
}
}
}
function writeSprite(relativePath, png) {
const filePath = path.join(publicDir, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, PNG.sync.write(png));
console.log(`wrote ${relativePath}`);
}
function drawSkin() {
const png = createCanvas();
fillRect(png, 11, 6, 10, 10, RED);
fillRect(png, 10, 16, 12, 14, RED);
fillRect(png, 8, 30, 6, 12, RED);
fillRect(png, 18, 30, 6, 12, RED);
fillRect(png, 6, 18, 4, 10, RED);
fillRect(png, 22, 18, 4, 10, RED);
fillRect(png, 12, 8, 8, 2, DARK_RED);
writeSprite("character/skin/skin-1.png", png);
}
function drawPantsVariant(index, widthOffset) {
const png = createCanvas();
fillRect(png, 8 + widthOffset, 28, 7 - widthOffset, 14, RED);
fillRect(png, 17, 28, 7 - widthOffset, 14, RED);
if (index === 2) {
fillRect(png, 8, 26, 16, 4, RED);
}
if (index === 3) {
fillRect(png, 7, 24, 18, 6, RED);
}
writeSprite(`character/pants/pants-${index}.png`, png);
}
function drawShoesVariant(index) {
const png = createCanvas();
fillRect(png, 6, 40, 8, 4, RED);
fillRect(png, 18, 40, 8, 4, RED);
if (index >= 2) {
fillRect(png, 5, 42, 10, 2, DARK_RED);
fillRect(png, 17, 42, 10, 2, DARK_RED);
}
if (index === 3) {
fillRect(png, 4, 38, 12, 3, RED);
fillRect(png, 16, 38, 12, 3, RED);
}
writeSprite(`character/shoes/shoes-${index}.png`, png);
}
function drawTorsoVariant(index) {
const png = createCanvas();
fillRect(png, 9, 16, 14, 14, RED);
if (index >= 2) {
fillRect(png, 7, 18, 3, 10, RED);
fillRect(png, 22, 18, 3, 10, RED);
}
if (index >= 3) {
fillRect(png, 11, 18, 10, 8, DARK_RED);
}
if (index === 4) {
fillRect(png, 10, 14, 12, 4, RED);
}
writeSprite(`character/torso/torso-${index}.png`, png);
}
function drawEyesVariant(index) {
const png = createCanvas();
fillRect(png, 12, 10, 3, 3, index === 1 ? RED : WHITE);
fillRect(png, 17, 10, 3, 3, index === 1 ? RED : WHITE);
fillRect(png, 13, 11, 1, 1, MAGENTA);
fillRect(png, 18, 11, 1, 1, MAGENTA);
if (index === 2) {
fillRect(png, 11, 9, 5, 5, WHITE);
fillRect(png, 16, 9, 5, 5, WHITE);
fillRect(png, 13, 11, 2, 2, RED);
fillRect(png, 18, 11, 2, 2, RED);
}
writeSprite(`character/eyes/eyes-${index}.png`, png);
}
function drawHeadVariant(index) {
const png = createCanvas();
const y = index <= 3 ? 4 : 2;
fillRect(png, 10, y, 12, index >= 5 ? 12 : 10, RED);
if (index === 2) {
fillRect(png, 8, y + 2, 3, 3, RED);
fillRect(png, 21, y + 2, 3, 3, RED);
}
if (index === 3) {
fillRect(png, 9, y - 1, 14, 3, RED);
}
if (index >= 4) {
fillRect(png, 11, y + 1, 10, 4, DARK_RED);
}
if (index >= 6) {
fillRect(png, 9, y - 2, 14, 4, RED);
}
if (index === 7) {
fillRect(png, 7, y, 4, 8, RED);
fillRect(png, 21, y, 4, 8, RED);
}
writeSprite(`character/head/head-${index}.png`, png);
}
drawSkin();
for (let index = 1; index <= 3; index += 1) {
drawPantsVariant(index, index - 1);
drawShoesVariant(index);
}
for (let index = 1; index <= 4; index += 1) {
drawTorsoVariant(index);
}
for (let index = 1; index <= 2; index += 1) {
drawEyesVariant(index);
}
for (let index = 1; index <= 7; index += 1) {
drawHeadVariant(index);
}
console.log("Character sprites generated.");