perf(sprites): ship 192px sprites and a manifest of every sprite
Sprites were shipped at 512x512 but render at 44-64px in the box grid. Resizing to 192px halves each file (median 15.5KB -> 7.6KB); a full dex of artwork drops from 16.6MB to 7.6MB. The sprite build now also writes static/sprites-small/manifest.json, listing every sprite (all forms, shiny and female variants) with its size, so the offline worker can save the complete set and report what is missing. A unit test keeps it in step with the files on disk.
@@ -9,6 +9,7 @@ src/lib/helpers/pokeapi-pokemon.json
|
|||||||
static/sprites/pokemon.json
|
static/sprites/pokemon.json
|
||||||
src/lib/helpers/pokedex.json
|
src/lib/helpers/pokedex.json
|
||||||
src/lib/helpers/sprites.json
|
src/lib/helpers/sprites.json
|
||||||
|
static/sprites-small/manifest.json
|
||||||
|
|
||||||
# Machine-local editor and tool settings.
|
# Machine-local editor and tool settings.
|
||||||
**/*.local.json
|
**/*.local.json
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"dev-generate": "GENERATE_SW=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
"dev-generate": "GENERATE_SW=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||||
"dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
"dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||||
"sprites:build": "node scripts/optimize-sprites.mjs",
|
"sprites:build": "node scripts/optimize-sprites.mjs",
|
||||||
|
"sprites:manifest": "node scripts/sprite-manifest.mjs",
|
||||||
"build-generate-sw": "npm run tailwind && GENERATE_SW=true vite build",
|
"build-generate-sw": "npm run tailwind && GENERATE_SW=true vite build",
|
||||||
"build-generate-sw-node": "npm run tailwind && NODE_ADAPTER=true GENERATE_SW=true vite build",
|
"build-generate-sw-node": "npm run tailwind && NODE_ADAPTER=true GENERATE_SW=true vite build",
|
||||||
"build": "npm run tailwind && vite build",
|
"build": "npm run tailwind && vite build",
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import path from 'node:path';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { mkdir, readdir, rename } from 'node:fs/promises';
|
import { mkdir, readdir, rename } from 'node:fs/promises';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { writeSpriteManifest } from './sprite-manifest.mjs';
|
||||||
|
|
||||||
const inputDir = process.env.SPRITE_INPUT_DIR ?? path.join(process.cwd(), 'static', 'sprites');
|
const inputDir = process.env.SPRITE_INPUT_DIR ?? path.join(process.cwd(), 'static', 'sprites');
|
||||||
const outputDir =
|
const outputDir =
|
||||||
process.env.SPRITE_OUTPUT_DIR ?? path.join(process.cwd(), 'static', 'sprites-small');
|
process.env.SPRITE_OUTPUT_DIR ?? path.join(process.cwd(), 'static', 'sprites-small');
|
||||||
const format = (process.env.SPRITE_FORMAT ?? 'webp').toLowerCase();
|
const format = (process.env.SPRITE_FORMAT ?? 'webp').toLowerCase();
|
||||||
const quality = Number(process.env.SPRITE_QUALITY ?? 80);
|
const quality = Number(process.env.SPRITE_QUALITY ?? 80);
|
||||||
const maxSize = Number(process.env.SPRITE_MAX_SIZE ?? 0);
|
// Sprites render at 44-64px in the box grid and up to 192px in the detail view, and every byte is
|
||||||
|
// downloaded (and cached offline) per sprite, so larger sources only cost users data.
|
||||||
|
const maxSize = Number(process.env.SPRITE_MAX_SIZE ?? 192);
|
||||||
|
|
||||||
if (!['png', 'webp'].includes(format)) {
|
if (!['png', 'webp'].includes(format)) {
|
||||||
console.error(`Unsupported SPRITE_FORMAT "${format}". Use "png" or "webp".`);
|
console.error(`Unsupported SPRITE_FORMAT "${format}". Use "png" or "webp".`);
|
||||||
@@ -69,4 +72,5 @@ for (const [index, file] of files.entries()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Sprite optimization complete.');
|
const manifest = await writeSpriteManifest(outputDir);
|
||||||
|
console.log(`Sprite optimization complete. Manifest lists ${manifest.files.length} sprites.`);
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Lists every sprite (all forms, shiny and female variants) with its size, so the offline worker can
|
||||||
|
// save the complete set on request and tell whether anything is still missing.
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { readdir, stat, writeFile } from 'node:fs/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
export const SPRITE_MANIFEST_NAME = 'manifest.json';
|
||||||
|
|
||||||
|
async function walk(dir, files = []) {
|
||||||
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) await walk(fullPath, files);
|
||||||
|
else if (entry.isFile() && entry.name.endsWith('.webp')) files.push(fullPath);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the manifest for `<spritesDir>/home`, with paths relative to that folder. */
|
||||||
|
export async function buildSpriteManifest(spritesDir) {
|
||||||
|
const homeDir = path.join(spritesDir, 'home');
|
||||||
|
const files = await walk(homeDir);
|
||||||
|
const entries = await Promise.all(
|
||||||
|
files.map(async (file) => [
|
||||||
|
path.relative(homeDir, file).split(path.sep).join('/'),
|
||||||
|
(await stat(file)).size
|
||||||
|
])
|
||||||
|
);
|
||||||
|
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||||
|
return { version: 1, files: entries };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeSpriteManifest(spritesDir) {
|
||||||
|
const manifest = await buildSpriteManifest(spritesDir);
|
||||||
|
await writeFile(path.join(spritesDir, SPRITE_MANIFEST_NAME), `${JSON.stringify(manifest)}\n`);
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
|
const spritesDir =
|
||||||
|
process.env.SPRITE_OUTPUT_DIR ?? path.join(process.cwd(), 'static', 'sprites-small');
|
||||||
|
const manifest = await writeSpriteManifest(spritesDir);
|
||||||
|
const bytes = manifest.files.reduce((total, [, size]) => total + size, 0);
|
||||||
|
console.log(
|
||||||
|
`Wrote ${manifest.files.length} sprites (${(bytes / 1048576).toFixed(1)} MB) to ${SPRITE_MANIFEST_NAME}`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -263,4 +263,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sprite files are at most 192px, so cap the display size there to keep them crisp. */
|
||||||
|
.sprite-container :global(img) {
|
||||||
|
width: 192px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/** Folder holding every sprite; paths in static/sprites-small/manifest.json are relative to it. */
|
||||||
|
export function spriteRoot(useLocalSprites: boolean): string {
|
||||||
|
return useLocalSprites
|
||||||
|
? '/sprites-small/home'
|
||||||
|
: 'https://raw.githubusercontent.com/jcreek/LivingDexTracker/master/static/sprites-small/home';
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveSpriteUrl(
|
export function resolveSpriteUrl(
|
||||||
entry: { pokedexNumber: number; form?: string; spriteKey?: string },
|
entry: { pokedexNumber: number; form?: string; spriteKey?: string },
|
||||||
shiny: boolean,
|
shiny: boolean,
|
||||||
@@ -34,9 +41,7 @@ export function resolveSpriteUrl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = useLocalSprites
|
let root = spriteRoot(useLocalSprites);
|
||||||
? '/sprites-small/home'
|
|
||||||
: 'https://raw.githubusercontent.com/jcreek/LivingDexTracker/master/static/sprites-small/home';
|
|
||||||
if (shiny) root += '/shiny';
|
if (shiny) root += '/shiny';
|
||||||
if (/^female\b/i.test(form)) root += '/female';
|
if (/^female\b/i.test(form)) root += '/female';
|
||||||
return `${root}/${key}.webp`;
|
return `${root}/${key}.webp`;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |