Upload a dog photo. PawCSS samples it pixel by pixel in the browser and reconstructs it as thousands of real HTML elements styled with CSS — then lets you compare, zoom, inspect and export the result.
Hackathon brief: build something that is visually delightful and technically honest. Most "photo → art" toys are a CSS filter over an <img>. PawCSS does the opposite: the original image never appears in the artwork. What you see is generated DOM.
- The uploaded photo is drawn into an offscreen
<canvas>at a tiny resolution (40², 70² or 100²). getImageData()reads back every pixel; each pixel becomes an artwork cell with its own hex color, luminance, ASCII glyph and scale.- Each cell is rendered as its own DOM node (
<i>or<span>) inside a CSS Grid — the artwork contains 0<img>elements, and the stats panel reports that number from the actual render. - The export writes standalone HTML/CSS that still renders your dog with the photo deleted.
File → validate → decode → downscale (≤900px, JPEG q0.86, for the Before panel)
→ canvas Nx N draw → getImageData → per-cell {rgb, luma}
→ optional median-cut quantization (Pixel/Neon modes)
→ Artwork model {cols, rows, cells[]}
→ React renders one DOM element per cell (CSS Grid)
→ exporter maps unique colors to palette classes → standalone HTML+CSS
| Mode | Technique |
|---|---|
| CSS Mosaic (showcase) | One div per sampled pixel, full color fidelity |
| Pixel Pup | Larger cells + 24-color median-cut palette |
| Neon Pup | 16-color palette, saturation boost, per-cell box-shadow glow on a dark stage |
| Halftone Pup | Circular cells (border-radius: 50%) scaled by sampled brightness |
| Terminal Pup | ASCII ramp glyphs colored per cell on a terminal background |
- Drag-and-drop / file-picker upload, fully client-side (nothing is uploaded for the art pipeline)
- Draggable Before/After comparison slider (mouse, touch, and a keyboard-accessible range input)
- Zoom control (1× → 6×) with a live overlay of CSS pixels / colors / rendered elements
- Inspect My Dog: a devtools-style panel; click any cell to see its generated rule (
.dog-pixel-482 { … }) and copy it - Real statistics — element counts and unique-color counts are computed from the generated artwork, never hardcoded
- Export: Download HTML + CSS (standalone file) and Copy CSS
- Resolution presets Low 40² / Medium 70² / High 100² to keep the DOM sane on mobile
src/lib/imageProcessing.ts validation, decode, compression, sampling, median-cut quantization
src/lib/artwork.ts Artwork model + per-cell CSS rule generation
src/lib/exportArtwork.ts palette-class HTML/CSS exporter + download helper
src/lib/demoArtwork.ts hand-authored hero sample (still real DOM cells)
src/lib/dogProfile.functions.ts server function calling Gemini through the AI gateway
src/components/pawcss/
LandingHero, ImageUploader, ArtStyleSelector, CSSArtwork, FittedArtwork,
ComparisonSlider, ArtworkStats, CSSInspector, DogPersonality,
ExportPanel, GenerationProgress
src/routes/index.tsx app shell / orchestration
Image logic is pure TypeScript with no React imports, so it is testable and reusable.
After upload, the downscaled photo is sent to a server function which calls Gemini (vision) through the Lovable AI gateway and returns a playful profile: invented name, likely breed mix, energy 1–5, cuddle level 1–5, vibe, one-line description and a "dog thought". The key lives only in LOVABLE_API_KEY on the server — it is never exposed to the browser. Rate limits, missing keys, malformed JSON and network failures all fall back to friendly placeholder content, and the CSS art pipeline never depends on it. Breed output is clearly labelled a playful estimate, not a veterinary or genetic identification.
Speaks a line built from the AI personality. It currently uses the browser's built-in SpeechSynthesis API so it works with zero keys and zero cost; swapping in ElevenLabs only requires a server function that returns audio for the same sentence. The button is hidden when speech synthesis is unavailable and can never break the core app.
bun install
bun run dev # http://localhost:8080| Variable | Required | Purpose |
|---|---|---|
LOVABLE_API_KEY |
optional | Server-side Gemini access for "Meet Your Dog". Without it the app shows fallback profile content. |
Download HTML + CSS produces a self-contained document: a .pawcss-art grid, one CSS class per unique color, and one element per cell. Because colors are deduplicated into palette classes, a 10,000-cell artwork stays a reasonable file size while remaining pixel-identical to what you see on screen. Copy CSS copies just the stylesheet.
TanStack Start/Router/Query (framework), Tailwind CSS v4 + shadcn/ui primitives (Radix) for the design system, lucide-react (icons), sonner (toasts), zod (server-function input validation). All image processing, quantization, artwork generation and export code is original and written for this hackathon.
- DOM weight — 100² = 10,000 nodes. Solved with resolution presets, a lightweight
<i>element per cell, and a scale transform (FittedArtwork) instead of re-rendering at different sizes. - Export size — per-cell rules would be megabytes; palette classes cut this dramatically.
- Faithful color — naive nearest-neighbour sampling looked muddy; high-quality canvas smoothing plus alpha compositing onto the cream background fixed it.
- Honest stats — every number is derived from the artwork model at render time.
Dithering for the pixel mode, SVG/PNG export, shareable permalinks, CSS-variable-driven animated artwork, worker-based sampling for very large grids, and true ElevenLabs voices per personality.