EasyBeadPattern.com: turn an image, a word, or a pixel art from the shared library into a bead pattern for fuse beads (Perler, Hama, Artkal S, Nabbi, MARD) or beadwork (kandi, bead loom, peyote, brick stitch, alpha friendship bracelets), with counts, PNG and a printable PDF.
bun install
bun run dev # http://localhost:3010 (already in the backend CORS allowlist)
bun run test # bun:test, pure helpers only (tests/)
bun run typecheck # nuxi typecheck (vue-tsc, TypeScript 5.x; TS 7 breaks vue-tsc)
bun run build
bun run palettes # rebuild app/data/beads.json from data/beadcolors/*.csv
bun run deploy # manual build + ship to the VPS (CI normally does it)Repo git@github.com:comficker/easybeadpattern_frontend.git, pushed via the github.com-comficker
SSH alias. Push to main → .github/workflows/deploy.yml builds on GitHub (never on the 2 GB VPS),
rsyncs .output/ to /home/frontend/easybeadpattern, swaps it in and restarts pm2
easybeadpattern (:4015, NUXT_PUBLIC_SITE_URL=https://easybeadpattern.com), then smoke-tests.
Needs repo secret SSH_PRIVATE_KEY. nginx: /etc/nginx/sites-available/easybeadpattern_web
(apex canonical, www/http → 301, /_nuxt from disk with gzip_static, gzip for HTML/JSON).
TLS: Let's Encrypt via certbot (auto-renew). DNS on Cloudflare, currently DNS-only (grey cloud).
- No new backend model. Library patterns are
SharedPagerows fromninosaur_backend/coloring/shared-pages/(the same data simplepixelart uses), read-only.map_numberskeys are"x_y"→ index intocolors. - Catalog (
server/utils/catalog.ts): Nitro fetches every public original (full_schema) plus all tags once an hour and derives tag counts, co-occurring tags, sizes, colour counts and a difficulty level (levelOf). Listing pages and filters read/api/patternsand/api/facets, not the backend. The catalog is also the gate: a pattern page 404s unless the art is in it. - SEO pages:
/patterns/tag(A–Z),/patterns/tag/[tag],/patterns/size/[WxH],/patterns/level/[easy|medium|hard]. Empty → 404; fewer thanMIN_INDEX(3) patterns →noindex, followand left out of the sitemap. Intros come from real stats (utils/listing.ts). - Everything is converted to a brand-agnostic
SourceGrid(RGB or null per cell), thenmatchToBeads()maps it to one brand. Stored data never holds a brand; switching brand re-matches. - Matching (
app/helper/beads/pattern.ts): CIEDE2000 on a CIE76 shortlist; colour reduction drops the bead with the least√cells × distance²so small distinct details (eyes) survive; optional Floyd–Steinberg. Clear/glitter/glow/pearl/neon beads (special) are excluded by default. - Image → cells reuses
app/helper/pixel/reconstruct.ts, copied from simplepixelart (no shared package in this monorepo). Keep it in sync by hand. - Brush / eraser (
PatternWorkbench+BeadCanvas): hand edits areEdits(cell → hex or null), applied after matching and swaps byapplyEdits(). Stored as colours so they survive a brand switch and a colour-limit change; reset when the grid size changes. Undo keeps one snapshot per stroke (50 max). Keys: V select, B brush, E eraser, Alt-click picks, Ctrl+Z / Ctrl+Shift+Z. - Canvas draws each bead from a cached sprite (
beadSpriteindraw.ts) and redraws at most once per frame; per-bead gradients were too slow for brush strokes on 87 × 87. - Boards are 29 × 29 (
BOARDinbrands.ts); labels A1, A2 … B1. - Crafts (
app/helper/beads/crafts.ts): each craft is config, not code: palettes, layout (square|peyoteoffset columns |brickoffset rows), bead shape, cell size in mm (sets the drawn aspect and cm size;null= 1:1, no cm), width presets, boards (fuse only), written-chart order (wordChart()inpattern.ts) and landing copy/FAQ./[slug]serves both fuse brand and craft landings. Geometry for offsets lives indraw.ts(canvasSize,cellOrigin,cellAt); offset layouts sample images by box average (sampleBoxesinsource.ts). - Palettes (
brands.ts, built bybun run palettes): fuse brands from beadcolors (MIT);dmcexported from the backend'sapps/coloring/dmc.py;ponyandseedare our own generic palettes with no brand codes. There is no licensed Miyuki Delica / Toho dataset: two GitHub repos have Delica data but no licence, so don't copy them. - Bead letters (
font.ts,/bead-letters): our own 5 × 7 font; text → SourceGrid → any craft. - Pattern pages take
?craft=to open as that craft; canonical stays the plain URL. - Editor: select / brush / eraser / fill (
floodRegion), zoom 1–8× (Ctrl/⌘-wheel, +/−/0, space- or middle-drag pans). The board (.bench-stage) is always square and the side panels arecontain: sizeso they never stretch it./designeris a blank board (?art=<id_string>opens a saved one). - Build mode (
BuildMode.vue): tap beads as placed; fuse works board by board, beadwork row by row fromwordChart().cells. Progress is a bitset in localStorage keyed bypatternKey(). - My colours (
useInventory): owned codes per palette in localStorage;matchToBeads({only}). - Accounts: Google sign-in through the backend (
/auth/google?state=<origin>/auth/callback), tokens inbap_token/bap_refreshcookies (useAuth). Save = POST/PATCH/coloring/shared-pages/withmeta.origin = "easybeadpattern"; sharing PATCHesstatus: public, which the backend turns intopendingreview for non-staff./my-patternslists the user's arts. - SEO (
composables/useSeo.ts): every page callsuseSeowith title, description, path, image and JSON-LD. It drops the " · EasyBeadPattern" suffix when a title would pass 60 chars and clips descriptions to 158. Helpers:ldTool,ldFaq,ldBreadcrumbs,ldItemList; WebSite + Organization come fromapp.vue. Listing pages put the page number in title and description, 404 past the last page, and use the first pattern's image. Share cards inpublic/og/are rendered bybun run og(scripts/og.mjs, needs local Chrome); re-run after changing titles, crafts, brands or guides. Sitemap includes image entries for patterns. Rubik is self-hosted. - Performance (Lighthouse mobile 96–99, desktop 99–100, a11y/BP/SEO 100): all CSS is inlined
(
features.inlineStyles), so keep styles inmain.css, not in<style scoped>of client-only components (those can't be inlined and block render). Rubik has a metric-matched fallback (Rubik Fallback) so the font swap causes no layout shift. Don't wrap small inline elements in<ClientOnly>inside grids: its SSR placeholder takes a grid cell and shifts layout. Keep the SSR payload small:/api/facetshas no per-tag related lists (theme pages use/api/tags/:id). HTML is not compressed by Nitro; nginx must gzip it. Accent is#D6336A(AA with white text). - Guides (
app/helper/guides.ts): brand facts are computed from palette data; craft advice stays general. - Design system: tokens at the top of
main.css(type 12–22 + h1, 4-px spacing, controls 28/32/40, radii 8/12/pill). No one-off sizes and no inline styles. Icons are Iconsax "linear" SVGs inpublic/icons/via<Icon name>(CSS mask,currentColor); add new ones withscripts/render-icons.mjs. - All conversion, PNG and PDF (jsPDF) run in the browser. The converter autosaves to
localStorage (
bap:maker).
app/helper/pure TS (relative imports sobun testworks without Nuxt aliases)app/components/PatternWorkbench.vuethe settings | pegboard | bead list tool, used by the maker (image mode) and pattern pages (sourceprop)app/pages/:/,/[brand]-bead-pattern-maker(via[slug].vue),/patternsand the listing pages above,/patterns/[id],/colors,/colors/[brand]server/api/catalog endpoints,server/routes/sitemap.xml.ts
- Design tokens live on
:rootinapp/assets/css/main.css(light + dark). Pills for actions, panels flush with 1px dividers; the only decoration is the bead rendering itself. - Bead data: github.com/maxcleme/beadcolors (MIT), credited in the footer.