From 7ca0de03d560d333af0ddd6aa31b7e74cb4052a3 Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Mon, 12 Oct 2026 14:13:21 +0530 Subject: [PATCH 1/4] feat(brand): add Pixel Phase A2 vector artwork library --- apps/web/public/brand/pixel/README.md | 32 ++++ .../brand/pixel/expressions/pixel-awake.svg | 52 ++++++ .../pixel/expressions/pixel-celebrating.svg | 40 ++++ .../pixel/expressions/pixel-concerned.svg | 52 ++++++ .../brand/pixel/expressions/pixel-curious.svg | 52 ++++++ .../pixel/expressions/pixel-eye-roll.svg | 52 ++++++ .../brand/pixel/expressions/pixel-focused.svg | 52 ++++++ .../brand/pixel/expressions/pixel-happy.svg | 40 ++++ .../pixel/expressions/pixel-mischievous.svg | 46 +++++ .../brand/pixel/expressions/pixel-neutral.svg | 52 ++++++ .../pixel/expressions/pixel-thinking.svg | 52 ++++++ apps/web/public/brand/pixel/manifest.json | 35 ++++ .../web/public/brand/pixel/pixel-face-rig.svg | 24 +++ .../public/brand/pixel/pixel-hero-outline.svg | 27 +++ .../brand/pixel/pixel-laptop-companion.svg | 46 +++++ apps/web/public/brand/pixel/pixel-master.svg | 40 ++++ .../brand/pixel/pixel-static-fallback.svg | 40 ++++ .../pixel/poses/pixel-anticipation-crouch.svg | 52 ++++++ .../brand/pixel/poses/pixel-bump-up-jump.svg | 40 ++++ .../brand/pixel/poses/pixel-fully-drawn.svg | 40 ++++ .../brand/pixel/poses/pixel-hover-wave.svg | 41 ++++ .../brand/pixel/poses/pixel-idle-resting.svg | 40 ++++ .../pixel/poses/pixel-landing-squash.svg | 40 ++++ .../pixel/poses/pixel-surprised-awake.svg | 52 ++++++ .../brand/pixel/poses/pixel-thinking.svg | 52 ++++++ apps/web/scripts/build-pixel-phase-a2.mjs | 175 ++++++++++++++++++ apps/web/src/lib/pixel-artwork.test.ts | 113 +++++++++++ docs/pixel-phase-a2.md | 26 +++ 28 files changed, 1405 insertions(+) create mode 100644 apps/web/public/brand/pixel/README.md create mode 100644 apps/web/public/brand/pixel/expressions/pixel-awake.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-celebrating.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-concerned.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-curious.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-eye-roll.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-focused.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-happy.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-mischievous.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-neutral.svg create mode 100644 apps/web/public/brand/pixel/expressions/pixel-thinking.svg create mode 100644 apps/web/public/brand/pixel/manifest.json create mode 100644 apps/web/public/brand/pixel/pixel-face-rig.svg create mode 100644 apps/web/public/brand/pixel/pixel-hero-outline.svg create mode 100644 apps/web/public/brand/pixel/pixel-laptop-companion.svg create mode 100644 apps/web/public/brand/pixel/pixel-master.svg create mode 100644 apps/web/public/brand/pixel/pixel-static-fallback.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-anticipation-crouch.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-bump-up-jump.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-hover-wave.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-idle-resting.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-landing-squash.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-surprised-awake.svg create mode 100644 apps/web/public/brand/pixel/poses/pixel-thinking.svg create mode 100644 apps/web/scripts/build-pixel-phase-a2.mjs create mode 100644 apps/web/src/lib/pixel-artwork.test.ts create mode 100644 docs/pixel-phase-a2.md diff --git a/apps/web/public/brand/pixel/README.md b/apps/web/public/brand/pixel/README.md new file mode 100644 index 000000000..4bc9b562a --- /dev/null +++ b/apps/web/public/brand/pixel/README.md @@ -0,0 +1,32 @@ +# Pixel — Phase A2 artwork + +This is an **artwork-only**, opt-in vector library. Nothing here changes the existing Creative Cursor logo, Pixel assistant, process illustrations, homepage, PWA, or product interactions. + +## Identity reference + +The original `/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg` and the user's approved Phase A character sheet are anatomical/art-direction references. Pixel has a plush rounded seated body, extremely wide upward-angled ears with graphite tips, small hair tuft, a large rounded face and glossy eyes, tiny smiling muzzle, short forepaws, whiskers, and a curled dark-tipped tail. This collection is a **manual vector adaptation**, not a claim that the original raster-containing SVG has become a lossless vector. + +## Library + +- `pixel-master.svg`: canonical layered static character +- `pixel-face-rig.svg`: isolated eyes/pupils/brows/nose/mouth/whiskers +- `pixel-hero-outline.svg`: ordered, real stroke-only paths with `pathLength="1"` and `data-pencil-order` for Phase B +- `pixel-static-fallback.svg`: motion-free illustration +- `pixel-laptop-companion.svg`: optional laptop prop based on the original reference (the prop is not part of the character rig) +- `expressions/`: ten standalone expression variants +- `poses/`: eight standalone motion-key illustrations +- `manifest.json`: inventory and approved artwork-only scope + +All exports are standalone local SVGs with internal defs only. No embedded PNG, external URL, script, active animation, CSS injection, or customer media. Existing files and paths remain unchanged. + +## Anatomy/animation contract + +`#pixel-character` includes `#pixel-head`, `#pixel-ear-left`, `#pixel-ear-right`, `#pixel-tail`, `#pixel-paws`, `#pixel-feet`, and `#pixel-details`. Facial groups include `#pixel-eyes`, `#pixel-eye-left`, `#pixel-eye-right`, `#pixel-pupil-left`, `#pixel-pupil-right`, `#pixel-brows`, `#pixel-whiskers`, `#pixel-nose`, and `#pixel-mouth`. The pose files are static key artwork, **not** animated timelines. The draw-on outline consists of individually addressable contours; render a fully drawn fallback before or without JS. + +All individual assets use a `0 0 720 520` viewBox. Preserve the whole aspect ratio; don't crop ear/tail tips or invert the artwork in dark mode. Display on `illustration-paper`. Any subsequent animation must respect reduced motion, pause off-screen, and cap pupil movement inside the eye. Apply violet/coral accents sparingly around Pixel. + +## Deterministic maintenance + +`apps/web/scripts/build-pixel-phase-a2.mjs` is the single source for these generated assets. Do not hand-edit exports. Modify the builder, run `node apps/web/scripts/build-pixel-phase-a2.mjs --write`, inspect the artwork, and run `node apps/web/scripts/build-pixel-phase-a2.mjs --check`. No third-party build-time dependencies are required. + +Phase B animation and Phase C/D homepage integration require a separate PR and artwork sign-off. diff --git a/apps/web/public/brand/pixel/expressions/pixel-awake.svg b/apps/web/public/brand/pixel/expressions/pixel-awake.svg new file mode 100644 index 000000000..7e2015a9f --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-awake.svg @@ -0,0 +1,52 @@ + +Pixel expression: awakeOfficial Pixel character expression study: awake. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-celebrating.svg b/apps/web/public/brand/pixel/expressions/pixel-celebrating.svg new file mode 100644 index 000000000..f83fdfe17 --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-celebrating.svg @@ -0,0 +1,40 @@ + +Pixel expression: celebratingOfficial Pixel character expression study: celebrating. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-concerned.svg b/apps/web/public/brand/pixel/expressions/pixel-concerned.svg new file mode 100644 index 000000000..7eb06dc2e --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-concerned.svg @@ -0,0 +1,52 @@ + +Pixel expression: concernedOfficial Pixel character expression study: concerned. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-curious.svg b/apps/web/public/brand/pixel/expressions/pixel-curious.svg new file mode 100644 index 000000000..b0924c587 --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-curious.svg @@ -0,0 +1,52 @@ + +Pixel expression: curiousOfficial Pixel character expression study: curious. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-eye-roll.svg b/apps/web/public/brand/pixel/expressions/pixel-eye-roll.svg new file mode 100644 index 000000000..0e55e9cfb --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-eye-roll.svg @@ -0,0 +1,52 @@ + +Pixel expression: eye-rollOfficial Pixel character expression study: eye-roll. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-focused.svg b/apps/web/public/brand/pixel/expressions/pixel-focused.svg new file mode 100644 index 000000000..e81f7e16f --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-focused.svg @@ -0,0 +1,52 @@ + +Pixel expression: focusedOfficial Pixel character expression study: focused. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-happy.svg b/apps/web/public/brand/pixel/expressions/pixel-happy.svg new file mode 100644 index 000000000..ff20ac734 --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-happy.svg @@ -0,0 +1,40 @@ + +Pixel expression: happyOfficial Pixel character expression study: happy. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-mischievous.svg b/apps/web/public/brand/pixel/expressions/pixel-mischievous.svg new file mode 100644 index 000000000..20593dd5f --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-mischievous.svg @@ -0,0 +1,46 @@ + +Pixel expression: mischievousOfficial Pixel character expression study: mischievous. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-neutral.svg b/apps/web/public/brand/pixel/expressions/pixel-neutral.svg new file mode 100644 index 000000000..d335f571f --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-neutral.svg @@ -0,0 +1,52 @@ + +Pixel expression: neutralOfficial Pixel character expression study: neutral. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/expressions/pixel-thinking.svg b/apps/web/public/brand/pixel/expressions/pixel-thinking.svg new file mode 100644 index 000000000..f8e202cb1 --- /dev/null +++ b/apps/web/public/brand/pixel/expressions/pixel-thinking.svg @@ -0,0 +1,52 @@ + +Pixel expression: thinkingOfficial Pixel character expression study: thinking. Same canonical body and ears with independent facial vector groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/manifest.json b/apps/web/public/brand/pixel/manifest.json new file mode 100644 index 000000000..7cbeec0b4 --- /dev/null +++ b/apps/web/public/brand/pixel/manifest.json @@ -0,0 +1,35 @@ +{ + "version": "phase-a2", + "status": "artwork-only", + "canonicalReference": "/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg", + "sourceOfTruth": "/brand/pixel/pixel-master.svg", + "license": "Creators first-party brand media", + "assetType": "vector-only SVG, no embedded raster, no external resources", + "master": "pixel-master.svg", + "staticFallback": "pixel-static-fallback.svg", + "heroOutline": "pixel-hero-outline.svg", + "facialRig": "pixel-face-rig.svg", + "expressionIds": [ + "neutral", + "happy", + "curious", + "thinking", + "mischievous", + "awake", + "eye-roll", + "celebrating", + "focused", + "concerned" + ], + "poseIds": [ + "fully-drawn", + "anticipation-crouch", + "bump-up-jump", + "landing-squash", + "idle-resting", + "hover-wave", + "thinking", + "surprised-awake" + ], + "notes": "Artwork preparation only; do not activate character animation or replace current logo/mascots until separately approved." +} diff --git a/apps/web/public/brand/pixel/pixel-face-rig.svg b/apps/web/public/brand/pixel/pixel-face-rig.svg new file mode 100644 index 000000000..ffca0b07a --- /dev/null +++ b/apps/web/public/brand/pixel/pixel-face-rig.svg @@ -0,0 +1,24 @@ + +Pixel facial rig referenceIndividual vector elements named for both eyes, separate pupils, mouth, brows, whiskers and nose. This is a source illustration, not a functional animation. + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-hero-outline.svg b/apps/web/public/brand/pixel/pixel-hero-outline.svg new file mode 100644 index 000000000..f498d78e7 --- /dev/null +++ b/apps/web/public/brand/pixel/pixel-hero-outline.svg @@ -0,0 +1,27 @@ + +Pixel hero pencil outlineStroke-only canonical contours with stable pencil ordering and path lengths for the planned progressive hero drawing. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-laptop-companion.svg b/apps/web/public/brand/pixel/pixel-laptop-companion.svg new file mode 100644 index 000000000..dc1408cf1 --- /dev/null +++ b/apps/web/public/brand/pixel/pixel-laptop-companion.svg @@ -0,0 +1,46 @@ + +Pixel at a laptopOptional original-reference-inspired prop scene; the underlying character body remains independently editable. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-master.svg b/apps/web/public/brand/pixel/pixel-master.svg new file mode 100644 index 000000000..28c32f199 --- /dev/null +++ b/apps/web/public/brand/pixel/pixel-master.svg @@ -0,0 +1,40 @@ + +Pixel canonical vector masterOriginal chubby Pixel mascot with extended black-tipped ears, expressive eyes, graphite outlines, individual facial and body rig groups. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-static-fallback.svg b/apps/web/public/brand/pixel/pixel-static-fallback.svg new file mode 100644 index 000000000..2de25688c --- /dev/null +++ b/apps/web/public/brand/pixel/pixel-static-fallback.svg @@ -0,0 +1,40 @@ + +Pixel resting poseFully illustrated, motion-free mascot fallback for reduced motion, unsupported animation, and editorial usage. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-anticipation-crouch.svg b/apps/web/public/brand/pixel/poses/pixel-anticipation-crouch.svg new file mode 100644 index 000000000..ca97cd411 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-anticipation-crouch.svg @@ -0,0 +1,52 @@ + +Pixel pose: anticipation crouchMotion key art for Pixel: anticipation crouch. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-bump-up-jump.svg b/apps/web/public/brand/pixel/poses/pixel-bump-up-jump.svg new file mode 100644 index 000000000..81e8502f2 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-bump-up-jump.svg @@ -0,0 +1,40 @@ + +Pixel pose: bump up jumpMotion key art for Pixel: bump up jump. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg b/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg new file mode 100644 index 000000000..23e561e94 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg @@ -0,0 +1,40 @@ + +Pixel pose: fully drawnMotion key art for Pixel: fully drawn. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg b/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg new file mode 100644 index 000000000..0c9b4c29a --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg @@ -0,0 +1,41 @@ + +Pixel pose: hover waveMotion key art for Pixel: hover wave. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-idle-resting.svg b/apps/web/public/brand/pixel/poses/pixel-idle-resting.svg new file mode 100644 index 000000000..14fd6e943 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-idle-resting.svg @@ -0,0 +1,40 @@ + +Pixel pose: idle restingMotion key art for Pixel: idle resting. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-landing-squash.svg b/apps/web/public/brand/pixel/poses/pixel-landing-squash.svg new file mode 100644 index 000000000..7c8d8788d --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-landing-squash.svg @@ -0,0 +1,40 @@ + +Pixel pose: landing squashMotion key art for Pixel: landing squash. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-surprised-awake.svg b/apps/web/public/brand/pixel/poses/pixel-surprised-awake.svg new file mode 100644 index 000000000..05c383d73 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-surprised-awake.svg @@ -0,0 +1,52 @@ + +Pixel pose: surprised awakeMotion key art for Pixel: surprised awake. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-thinking.svg b/apps/web/public/brand/pixel/poses/pixel-thinking.svg new file mode 100644 index 000000000..9cd3f48f5 --- /dev/null +++ b/apps/web/public/brand/pixel/poses/pixel-thinking.svg @@ -0,0 +1,52 @@ + +Pixel pose: thinkingMotion key art for Pixel: thinking. Static vector illustration, no active animation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/scripts/build-pixel-phase-a2.mjs b/apps/web/scripts/build-pixel-phase-a2.mjs new file mode 100644 index 000000000..5ea453885 --- /dev/null +++ b/apps/web/scripts/build-pixel-phase-a2.mjs @@ -0,0 +1,175 @@ +// The single deterministic source of the Pixel Phase A2 vector exports. +// Run: node apps/web/scripts/build-pixel-phase-a2.mjs --check | --write +// Character identity is based on the existing Pixel laptop mascot; no existing media is modified. +import { readFile, mkdir, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { resolve, dirname } from "node:path"; +const repoRoot=resolve(dirname(fileURLToPath(import.meta.url)),"../../.."); +const buildPixelAssets=function buildPixelAssets() { + const root="apps/web/public/brand/pixel"; + const P={ + leftEar:"M323 177 C283 149 231 135 181 140 C132 144 104 161 101 185 C96 208 126 225 161 229 C214 236 269 211 311 189 Z", + rightEar:"M399 165 C437 117 503 57 566 39 C607 27 638 31 644 51 C652 77 633 107 602 127 C552 160 471 174 414 179 Z", + tail:"M304 395 C280 398 276 373 268 348 C256 310 234 283 200 281 C173 279 149 293 146 318 C143 339 158 355 179 364 C175 339 184 330 201 331 C224 333 235 365 254 394 C269 418 293 427 313 411 Z", + body:"M323 296 C302 307 291 329 288 363 C284 397 306 425 349 430 C389 435 437 433 463 408 C481 389 477 351 460 320 C444 291 365 279 323 296 Z", + head:"M301 248 C297 211 312 178 341 160 C358 149 377 145 395 148 C402 140 407 133 411 126 C423 129 429 139 426 152 C458 156 481 177 495 203 C511 233 512 266 499 291 C484 326 453 346 404 346 C351 348 315 327 305 292 C301 278 299 265 301 248 Z", + tuft:"M385 153 C394 142 399 136 399 132 C404 137 404 142 403 147 C414 136 418 128 418 121 C425 134 422 147 414 157", + leftPaw:"M328 347 C341 337 358 348 374 362 C387 373 391 388 384 398 C375 408 353 403 336 393 C323 383 318 359 328 347 Z", + rightPaw:"M443 343 C427 339 416 353 404 367 C395 377 391 390 399 399 C409 409 431 404 446 394 C457 382 457 355 443 343 Z", + leftFoot:"M320 409 C329 402 343 404 351 413 C360 423 354 433 336 435 C316 436 309 423 320 409 Z", + rightFoot:"M434 410 C442 401 455 403 465 413 C475 425 466 435 448 435 C429 434 424 423 434 410 Z" + }; + const ink="#29272a", ivory="#fbfaf6", shadow="#dedbd5", tip="#333235", violet="#8760c6", coral="#e8867a"; + const xml=(s)=>s.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',"""); + const basicAttrs=`fill="${ivory}" stroke="${ink}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"`; + const path=(d,id,attrs="")=>``; + const outline=(d,id,order)=>``; + const defs=` + + + + + + `; + function eyes(expression) { + if(["happy","celebrating","landing","idle"].includes(expression)){ + return ` + + `; + } + if(expression==="mischievous") { + return ` + ${openEye(460,256,"right",1,0)}`; + } + const look=expression==="curious"?[4,-2]:expression==="thinking"?[-3,-5]:expression==="eye-roll"?[-2,-9]:expression==="focused"?[0,2]:expression==="concerned"?[-2,1]:[0,0]; + return `${openEye(367,262,"left",1,look[0],look[1])}${openEye(461,256,"right",.93,look[0],look[1])}`; + } + function openEye(cx,cy,side,scale=1,dx=0,dy=0){ + return ` + + + + + + + `; + } + const mouths={ + neutral:``, + happy:``, + curious:``, + thinking:``, + mischievous:``, + awake:``, + "eye-roll":``, + celebrating:``, + focused:``, + concerned:``, + landing:``, + idle:`` + }; + function face(expression="happy"){ + return ` + + + + + ${eyes(expression)} + + + + + ${mouths[expression]||mouths.happy} + `; + } + function wrapper(inner,title,desc,role="img"){ + return ` +${xml(title)}${xml(desc)} +${defs} +${inner} +\n`; + } + function rig(expression="happy",pose="neutral",opts={}){ + const outlineOnly=!!opts.outlineOnly; + const draw=outlineOnly; + const bodyScale=pose==="crouch"?"translate(0 73) translate(396 0) scale(1.10 .79) translate(-396 0)":pose==="jump"?"translate(0 -46) rotate(-7 396 343)":pose==="landing"?"translate(0 92) translate(396 0) scale(1.15 .72) translate(-396 0)":pose==="wave"?"rotate(-4 396 345) translate(0 -10)":pose==="thinking"?"rotate(3 396 345)":""; + const earLeftPose=pose==="crouch"?"rotate(-12 312 177)":pose==="jump"?"rotate(-18 312 177)":pose==="wave"?"rotate(-5 312 177)":""; + const earRightPose=pose==="crouch"?"rotate(8 414 179)":pose==="jump"?"rotate(18 414 179)":pose==="wave"?"rotate(7 414 179)":""; + const drawP=(p,id,n,extra="")=>draw?outline(p,id,n):path(p,id,extra); + const tail=`${drawP(P.tail,"pixel-tail-contour",1)} + ${draw?"":``}`; + const ears=`${drawP(P.leftEar,"pixel-ear-left-contour",2)} + ${draw?"":``} + ${drawP(P.rightEar,"pixel-ear-right-contour",3)} + ${draw?"":``}`; + const feet=`${drawP(P.leftFoot,"pixel-foot-left",9,`fill="${tip}"`)}${drawP(P.rightFoot,"pixel-foot-right",10,`fill="${tip}"`)}`; + const paws=pose==="wave"?`${drawP(P.leftPaw,"pixel-paw-left",11)} + ${drawP(P.rightPaw,"pixel-paw-right",12)}`:`${drawP(P.leftPaw,"pixel-paw-left",11)}${drawP(P.rightPaw,"pixel-paw-right",12)}`; + const tuft=draw?outline(P.tuft,"pixel-hair-tuft",7):``; + return ` + ${tail} + ${drawP(P.body,"pixel-body",4,`fill="url(#pixel-soft-shade)"`)} + ${ears} + + ${drawP(P.head,"pixel-head-contour",5,`fill="url(#pixel-soft-shade)"`)} + ${tuft} + ${draw?"":face(expression)} + + ${feet} + ${paws} + ${draw?"":``} + `; + } + function backdrop(pose="neutral"){ + const y=pose==="jump"?457:pose==="crouch"||pose==="landing"?451:450; + return ``; + } + const output={}; + const assign=(name,content)=>{output[`${root}/${name}`]=content;}; + assign("pixel-master.svg",wrapper(`${backdrop()}${rig("happy")}`,"Pixel canonical vector master","Original chubby Pixel mascot with extended black-tipped ears, expressive eyes, graphite outlines, individual facial and body rig groups.")); + assign("pixel-static-fallback.svg",wrapper(`${backdrop()}${rig("happy")}`,"Pixel resting pose","Fully illustrated, motion-free mascot fallback for reduced motion, unsupported animation, and editorial usage.")); + assign("pixel-hero-outline.svg",wrapper(`${backdrop()}${rig("happy","neutral",{outlineOnly:true})}`,"Pixel hero pencil outline","Stroke-only canonical contours with stable pencil ordering and path lengths for the planned progressive hero drawing.")); + for(const e of ["neutral","happy","curious","thinking","mischievous","awake","eye-roll","celebrating","focused","concerned"]) { + assign(`expressions/pixel-${e}.svg`,wrapper(`${backdrop()}${rig(e,e==="thinking"?"thinking":"neutral")}`,`Pixel expression: ${e}`,`Official Pixel character expression study: ${e}. Same canonical body and ears with independent facial vector groups.`)); + } + const poses=[ + ["fully-drawn","happy","neutral"], + ["anticipation-crouch","focused","crouch"], + ["bump-up-jump","celebrating","jump"], + ["landing-squash","landing","landing"], + ["idle-resting","idle","neutral"], + ["hover-wave","happy","wave"], + ["thinking","thinking","thinking"], + ["surprised-awake","awake","neutral"] + ]; + for(const [name,expression,pose] of poses) { + assign(`poses/pixel-${name}.svg`,wrapper(`${backdrop(pose)}${rig(expression,pose)}`,`Pixel pose: ${name.replaceAll("-"," ")}`,`Motion key art for Pixel: ${name.replaceAll("-"," ")}. Static vector illustration, no active animation.`)); + } + assign("pixel-face-rig.svg",wrapper(`${path(P.head,"pixel-head-contour",`fill="url(#pixel-soft-shade)"`)}${face("happy")}`, + "Pixel facial rig reference","Individual vector elements named for both eyes, separate pupils, mouth, brows, whiskers and nose. This is a source illustration, not a functional animation.")); + // The canonical reference's laptop is an optional prop, never baked into the character rig. + const laptop=` + + + + + + `; + assign("pixel-laptop-companion.svg",wrapper(`${backdrop()}${rig("happy")}${laptop}`,"Pixel at a laptop","Optional original-reference-inspired prop scene; the underlying character body remains independently editable.")); + const manifest={version:"phase-a2",status:"artwork-only",canonicalReference:"/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg",sourceOfTruth:"/brand/pixel/pixel-master.svg",license:"Creators first-party brand media",assetType:"vector-only SVG, no embedded raster, no external resources",master:"pixel-master.svg",staticFallback:"pixel-static-fallback.svg",heroOutline:"pixel-hero-outline.svg",facialRig:"pixel-face-rig.svg",expressionIds:["neutral","happy","curious","thinking","mischievous","awake","eye-roll","celebrating","focused","concerned"],poseIds:poses.map(x=>x[0]),notes:"Artwork preparation only; do not activate character animation or replace current logo/mascots until separately approved."}; + assign("manifest.json",JSON.stringify(manifest,null,2)+"\n"); + return output; +}; +const assets=buildPixelAssets(); +const command=process.argv[2]; +if(command!=="--write" && command!=="--check") { console.error("Usage: node apps/web/scripts/build-pixel-phase-a2.mjs --check|--write"); process.exitCode=2; } +else { + let problems=0; + for(const [path,expected] of Object.entries(assets)){ + const absolute=resolve(repoRoot,path); + if(command==="--write"){ await mkdir(dirname(absolute),{recursive:true}); await writeFile(absolute,expected,"utf8"); } + else { const actual=await readFile(absolute,"utf8").catch(()=>null); if(actual!==expected) { console.error("Out of date: "+path); problems++; } } + } + if(problems) process.exitCode=1; + else console.log((command==="--write"?"Wrote ":"Verified ")+Object.keys(assets).length+" Pixel Phase A2 artwork files"); +} diff --git a/apps/web/src/lib/pixel-artwork.test.ts b/apps/web/src/lib/pixel-artwork.test.ts new file mode 100644 index 000000000..f53eea239 --- /dev/null +++ b/apps/web/src/lib/pixel-artwork.test.ts @@ -0,0 +1,113 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import sharp from "sharp"; +import { describe, expect, it } from "vitest"; + +const assetUrl = new URL("../../public/brand/pixel/", import.meta.url); +const content = (file: string) => + readFileSync(new URL(file, assetUrl), "utf8"); + +const manifest = JSON.parse(content("manifest.json")) as { + version: string; + master: string; + staticFallback: string; + heroOutline: string; + expressionIds: string[]; + poseIds: string[]; +}; + +const variants = [ + manifest.master, + manifest.staticFallback, + manifest.heroOutline, + "pixel-face-rig.svg", + "pixel-laptop-companion.svg", + ...manifest.expressionIds.map((id) => `expressions/pixel-${id}.svg`), + ...manifest.poseIds.map((id) => `poses/pixel-${id}.svg`), +]; + +describe("Pixel Phase A2 artwork exports", () => { + it("keeps checked-in SVG exports deterministic", () => { + const script = fileURLToPath( + new URL("../../scripts/build-pixel-phase-a2.mjs", import.meta.url), + ); + const run = spawnSync(process.execPath, [script, "--check"], { + encoding: "utf8", + }); + expect(run.status, run.stderr || run.stdout).toBe(0); + }); + + it("declares a complete canonical static asset set", () => { + expect(manifest.version).toBe("phase-a2"); + expect(manifest.expressionIds).toHaveLength(10); + expect(manifest.poseIds).toHaveLength(8); + expect(new Set(variants).size).toBe(23); + }); + + it.each(variants)( + "%s is a self-contained, labelled, vector-only SVG", + (file) => { + const svg = content(file); + expect(svg).toMatch(/^'); + expect(svg).toContain(''); + expect(svg).toContain(""); + expect(svg).toContain("]*\/>/g)) { + expect([...match[0].matchAll(/\sfill=/g)].length).toBeLessThanOrEqual(1); + } + }, + ); + + it("preserves independently addressable anatomy and facial features", () => { + const master = content(manifest.master); + for (const id of [ + "pixel-character", + "pixel-head", + "pixel-ear-left", + "pixel-ear-right", + "pixel-tail", + "pixel-paws", + "pixel-feet", + "pixel-eyes", + "pixel-eye-left", + "pixel-eye-right", + "pixel-pupil-left", + "pixel-pupil-right", + "pixel-mouth", + "pixel-whiskers", + ]) { + expect(master).toContain(`id="${id}"`); + } + const outline = content(manifest.heroOutline); + expect(outline).toContain('pathLength="1"'); + expect(outline).toContain('data-pencil-order="1"'); + expect(outline).toContain('data-pencil-order="12"'); + expect(outline).not.toContain(" { + for (const file of [ + manifest.master, + manifest.staticFallback, + manifest.heroOutline, + "expressions/pixel-eye-roll.svg", + "poses/pixel-bump-up-jump.svg", + "poses/pixel-hover-wave.svg", + ]) { + const image = sharp(Buffer.from(content(file)), { density: 96 }); + const metadata = await image.metadata(); + expect(metadata.width).toBe(720); + expect(metadata.height).toBe(520); + const rendered = await image.png().toBuffer(); + expect(rendered.length).toBeGreaterThan(1000); + } + }); +}); diff --git a/docs/pixel-phase-a2.md b/docs/pixel-phase-a2.md new file mode 100644 index 000000000..49a8590e1 --- /dev/null +++ b/docs/pixel-phase-a2.md @@ -0,0 +1,26 @@ +# Pixel Phase A2 — Artwork acceptance + +## Scope and approval + +This PR contributes first-party static vector artwork **only**. The existing homepage, Pixel assistant, process illustrations, logo, manifest, site navigation, and billing remain untouched. The original existing chubby Pixel reference and revised Phase A character sheet are the user's art-direction references. The newly drawn vector is a faithful **adaptation for rigging**, not a lossless vectorization of the embedded PNG in the legacy SVG. + +## Visual contract + +- Legacy anatomy reference: `apps/web/public/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg` +- Vector master: `apps/web/public/brand/pixel/pixel-master.svg` +- Identity traits: broad sideways graphite-tipped ears, round face, small flick of hair, glossy eyes, whiskers, tiny smiling muzzle, chubby short-limbed body, curved dark-tipped tail, dark feet +- Ivory/graphite artwork with restrained pencil texture; violet/coral are environment and annotation accents, not a replacement character palette +- Shared geometry across the entire expression/pose export library; props remain distinct from the master + +## Reviewer checklist + +1. Visually compare master, fallback and optional laptop scene with the original reference. +2. Check every expression, especially eye-roll, happy, awake and concerned. +3. Examine crouch, bump/jump, landing squash, idle and wave as separate motion key illustrations. +4. Verify master and face rig contain independently addressable head/ears/tail/paws/eyes/pupils/mouth layers. +5. Inspect real stroke-only contours and ordering metadata in `pixel-hero-outline.svg`; it must not be a bitmap wipe. +6. Render representative artwork on illustration-paper in both light and dark surrounding themes, at hero and smaller preview sizes. +7. Verify assets contain no embedded raster, active animation, foreign/external refs or scripts. +8. Verify deterministic export synchronization with `node apps/web/scripts/build-pixel-phase-a2.mjs --check`; then run `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build`. + +Art review and final-head CI are separate acceptance requirements. Do not merge until both are complete. Phase B implements motion/pointer/scroll, and subsequent phases implement the homepage; neither belongs in this PR. From a84426cd605ac45b0e005ba284b8fb9397a97d40 Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Mon, 12 Oct 2026 14:15:58 +0530 Subject: [PATCH 2/4] fix(brand): validate SVG fills, open-eye master rig and format artwork tooling --- .../brand/pixel/expressions/pixel-happy.svg | 18 +- .../web/public/brand/pixel/pixel-face-rig.svg | 18 +- .../brand/pixel/pixel-laptop-companion.svg | 18 +- apps/web/public/brand/pixel/pixel-master.svg | 18 +- .../brand/pixel/pixel-static-fallback.svg | 18 +- .../brand/pixel/poses/pixel-fully-drawn.svg | 18 +- .../brand/pixel/poses/pixel-hover-wave.svg | 18 +- apps/web/scripts/build-pixel-phase-a2.mjs | 378 +++++++++++++----- apps/web/src/lib/pixel-artwork.test.ts | 7 +- docs/pixel-phase-a2.md | 47 ++- 10 files changed, 417 insertions(+), 141 deletions(-) diff --git a/apps/web/public/brand/pixel/expressions/pixel-happy.svg b/apps/web/public/brand/pixel/expressions/pixel-happy.svg index ff20ac734..2bf85d75b 100644 --- a/apps/web/public/brand/pixel/expressions/pixel-happy.svg +++ b/apps/web/public/brand/pixel/expressions/pixel-happy.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-face-rig.svg b/apps/web/public/brand/pixel/pixel-face-rig.svg index ffca0b07a..f4189fd34 100644 --- a/apps/web/public/brand/pixel/pixel-face-rig.svg +++ b/apps/web/public/brand/pixel/pixel-face-rig.svg @@ -12,9 +12,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-laptop-companion.svg b/apps/web/public/brand/pixel/pixel-laptop-companion.svg index dc1408cf1..aa2c6fe13 100644 --- a/apps/web/public/brand/pixel/pixel-laptop-companion.svg +++ b/apps/web/public/brand/pixel/pixel-laptop-companion.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-master.svg b/apps/web/public/brand/pixel/pixel-master.svg index 28c32f199..7d0a74621 100644 --- a/apps/web/public/brand/pixel/pixel-master.svg +++ b/apps/web/public/brand/pixel/pixel-master.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/pixel-static-fallback.svg b/apps/web/public/brand/pixel/pixel-static-fallback.svg index 2de25688c..979514e85 100644 --- a/apps/web/public/brand/pixel/pixel-static-fallback.svg +++ b/apps/web/public/brand/pixel/pixel-static-fallback.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg b/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg index 23e561e94..340d2e42b 100644 --- a/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg +++ b/apps/web/public/brand/pixel/poses/pixel-fully-drawn.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg b/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg index 0c9b4c29a..19b8fbf02 100644 --- a/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg +++ b/apps/web/public/brand/pixel/poses/pixel-hover-wave.svg @@ -23,9 +23,21 @@ - - - + + + + + + + + + + + + + + + diff --git a/apps/web/scripts/build-pixel-phase-a2.mjs b/apps/web/scripts/build-pixel-phase-a2.mjs index 5ea453885..398a0af92 100644 --- a/apps/web/scripts/build-pixel-phase-a2.mjs +++ b/apps/web/scripts/build-pixel-phase-a2.mjs @@ -4,27 +4,45 @@ import { readFile, mkdir, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { resolve, dirname } from "node:path"; -const repoRoot=resolve(dirname(fileURLToPath(import.meta.url)),"../../.."); -const buildPixelAssets=function buildPixelAssets() { - const root="apps/web/public/brand/pixel"; - const P={ - leftEar:"M323 177 C283 149 231 135 181 140 C132 144 104 161 101 185 C96 208 126 225 161 229 C214 236 269 211 311 189 Z", - rightEar:"M399 165 C437 117 503 57 566 39 C607 27 638 31 644 51 C652 77 633 107 602 127 C552 160 471 174 414 179 Z", - tail:"M304 395 C280 398 276 373 268 348 C256 310 234 283 200 281 C173 279 149 293 146 318 C143 339 158 355 179 364 C175 339 184 330 201 331 C224 333 235 365 254 394 C269 418 293 427 313 411 Z", - body:"M323 296 C302 307 291 329 288 363 C284 397 306 425 349 430 C389 435 437 433 463 408 C481 389 477 351 460 320 C444 291 365 279 323 296 Z", - head:"M301 248 C297 211 312 178 341 160 C358 149 377 145 395 148 C402 140 407 133 411 126 C423 129 429 139 426 152 C458 156 481 177 495 203 C511 233 512 266 499 291 C484 326 453 346 404 346 C351 348 315 327 305 292 C301 278 299 265 301 248 Z", - tuft:"M385 153 C394 142 399 136 399 132 C404 137 404 142 403 147 C414 136 418 128 418 121 C425 134 422 147 414 157", - leftPaw:"M328 347 C341 337 358 348 374 362 C387 373 391 388 384 398 C375 408 353 403 336 393 C323 383 318 359 328 347 Z", - rightPaw:"M443 343 C427 339 416 353 404 367 C395 377 391 390 399 399 C409 409 431 404 446 394 C457 382 457 355 443 343 Z", - leftFoot:"M320 409 C329 402 343 404 351 413 C360 423 354 433 336 435 C316 436 309 423 320 409 Z", - rightFoot:"M434 410 C442 401 455 403 465 413 C475 425 466 435 448 435 C429 434 424 423 434 410 Z" +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const buildPixelAssets = function buildPixelAssets() { + const root = "apps/web/public/brand/pixel"; + const P = { + leftEar: + "M323 177 C283 149 231 135 181 140 C132 144 104 161 101 185 C96 208 126 225 161 229 C214 236 269 211 311 189 Z", + rightEar: + "M399 165 C437 117 503 57 566 39 C607 27 638 31 644 51 C652 77 633 107 602 127 C552 160 471 174 414 179 Z", + tail: "M304 395 C280 398 276 373 268 348 C256 310 234 283 200 281 C173 279 149 293 146 318 C143 339 158 355 179 364 C175 339 184 330 201 331 C224 333 235 365 254 394 C269 418 293 427 313 411 Z", + body: "M323 296 C302 307 291 329 288 363 C284 397 306 425 349 430 C389 435 437 433 463 408 C481 389 477 351 460 320 C444 291 365 279 323 296 Z", + head: "M301 248 C297 211 312 178 341 160 C358 149 377 145 395 148 C402 140 407 133 411 126 C423 129 429 139 426 152 C458 156 481 177 495 203 C511 233 512 266 499 291 C484 326 453 346 404 346 C351 348 315 327 305 292 C301 278 299 265 301 248 Z", + tuft: "M385 153 C394 142 399 136 399 132 C404 137 404 142 403 147 C414 136 418 128 418 121 C425 134 422 147 414 157", + leftPaw: + "M328 347 C341 337 358 348 374 362 C387 373 391 388 384 398 C375 408 353 403 336 393 C323 383 318 359 328 347 Z", + rightPaw: + "M443 343 C427 339 416 353 404 367 C395 377 391 390 399 399 C409 409 431 404 446 394 C457 382 457 355 443 343 Z", + leftFoot: + "M320 409 C329 402 343 404 351 413 C360 423 354 433 336 435 C316 436 309 423 320 409 Z", + rightFoot: + "M434 410 C442 401 455 403 465 413 C475 425 466 435 448 435 C429 434 424 423 434 410 Z", }; - const ink="#29272a", ivory="#fbfaf6", shadow="#dedbd5", tip="#333235", violet="#8760c6", coral="#e8867a"; - const xml=(s)=>s.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',"""); - const basicAttrs=`fill="${ivory}" stroke="${ink}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"`; - const path=(d,id,attrs="")=>``; - const outline=(d,id,order)=>``; - const defs=` + const ink = "#29272a", + ivory = "#fbfaf6", + shadow = "#dedbd5", + tip = "#333235", + violet = "#8760c6", + coral = "#e8867a"; + const xml = (s) => + s + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); + const basicAttrs = `fill="${ivory}" stroke="${ink}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"`; + const path = (d, id, attrs = "") => + ``; + const outline = (d, id, order) => + ``; + const defs = ` @@ -32,19 +50,30 @@ const buildPixelAssets=function buildPixelAssets() { `; function eyes(expression) { - if(["happy","celebrating","landing","idle"].includes(expression)){ + if (["celebrating", "landing", "idle"].includes(expression)) { return ` `; } - if(expression==="mischievous") { + if (expression === "mischievous") { return ` - ${openEye(460,256,"right",1,0)}`; + ${openEye(460, 256, "right", 1, 0)}`; } - const look=expression==="curious"?[4,-2]:expression==="thinking"?[-3,-5]:expression==="eye-roll"?[-2,-9]:expression==="focused"?[0,2]:expression==="concerned"?[-2,1]:[0,0]; - return `${openEye(367,262,"left",1,look[0],look[1])}${openEye(461,256,"right",.93,look[0],look[1])}`; + const look = + expression === "curious" + ? [4, -2] + : expression === "thinking" + ? [-3, -5] + : expression === "eye-roll" + ? [-2, -9] + : expression === "focused" + ? [0, 2] + : expression === "concerned" + ? [-2, 1] + : [0, 0]; + return `${openEye(367, 262, "left", 1, look[0], look[1])}${openEye(461, 256, "right", 0.93, look[0], look[1])}`; } - function openEye(cx,cy,side,scale=1,dx=0,dy=0){ + function openEye(cx, cy, side, scale = 1, dx = 0, dy = 0) { return ` @@ -54,122 +83,263 @@ const buildPixelAssets=function buildPixelAssets() { `; } - const mouths={ - neutral:``, - happy:``, - curious:``, - thinking:``, - mischievous:``, - awake:``, - "eye-roll":``, - celebrating:``, - focused:``, - concerned:``, - landing:``, - idle:`` + const mouths = { + neutral: ``, + happy: ``, + curious: ``, + thinking: ``, + mischievous: ``, + awake: ``, + "eye-roll": ``, + celebrating: ``, + focused: ``, + concerned: ``, + landing: ``, + idle: ``, }; - function face(expression="happy"){ + function face(expression = "happy") { return ` - - + + ${eyes(expression)} - ${mouths[expression]||mouths.happy} + ${mouths[expression] || mouths.happy} `; } - function wrapper(inner,title,desc,role="img"){ + function wrapper(inner, title, desc, role = "img") { return ` ${xml(title)}${xml(desc)} ${defs} ${inner} \n`; } - function rig(expression="happy",pose="neutral",opts={}){ - const outlineOnly=!!opts.outlineOnly; - const draw=outlineOnly; - const bodyScale=pose==="crouch"?"translate(0 73) translate(396 0) scale(1.10 .79) translate(-396 0)":pose==="jump"?"translate(0 -46) rotate(-7 396 343)":pose==="landing"?"translate(0 92) translate(396 0) scale(1.15 .72) translate(-396 0)":pose==="wave"?"rotate(-4 396 345) translate(0 -10)":pose==="thinking"?"rotate(3 396 345)":""; - const earLeftPose=pose==="crouch"?"rotate(-12 312 177)":pose==="jump"?"rotate(-18 312 177)":pose==="wave"?"rotate(-5 312 177)":""; - const earRightPose=pose==="crouch"?"rotate(8 414 179)":pose==="jump"?"rotate(18 414 179)":pose==="wave"?"rotate(7 414 179)":""; - const drawP=(p,id,n,extra="")=>draw?outline(p,id,n):path(p,id,extra); - const tail=`${drawP(P.tail,"pixel-tail-contour",1)} - ${draw?"":``}`; - const ears=`${drawP(P.leftEar,"pixel-ear-left-contour",2)} - ${draw?"":``} - ${drawP(P.rightEar,"pixel-ear-right-contour",3)} - ${draw?"":``}`; - const feet=`${drawP(P.leftFoot,"pixel-foot-left",9,`fill="${tip}"`)}${drawP(P.rightFoot,"pixel-foot-right",10,`fill="${tip}"`)}`; - const paws=pose==="wave"?`${drawP(P.leftPaw,"pixel-paw-left",11)} - ${drawP(P.rightPaw,"pixel-paw-right",12)}`:`${drawP(P.leftPaw,"pixel-paw-left",11)}${drawP(P.rightPaw,"pixel-paw-right",12)}`; - const tuft=draw?outline(P.tuft,"pixel-hair-tuft",7):``; + function rig(expression = "happy", pose = "neutral", opts = {}) { + const outlineOnly = !!opts.outlineOnly; + const draw = outlineOnly; + const bodyScale = + pose === "crouch" + ? "translate(0 73) translate(396 0) scale(1.10 .79) translate(-396 0)" + : pose === "jump" + ? "translate(0 -46) rotate(-7 396 343)" + : pose === "landing" + ? "translate(0 92) translate(396 0) scale(1.15 .72) translate(-396 0)" + : pose === "wave" + ? "rotate(-4 396 345) translate(0 -10)" + : pose === "thinking" + ? "rotate(3 396 345)" + : ""; + const earLeftPose = + pose === "crouch" + ? "rotate(-12 312 177)" + : pose === "jump" + ? "rotate(-18 312 177)" + : pose === "wave" + ? "rotate(-5 312 177)" + : ""; + const earRightPose = + pose === "crouch" + ? "rotate(8 414 179)" + : pose === "jump" + ? "rotate(18 414 179)" + : pose === "wave" + ? "rotate(7 414 179)" + : ""; + const drawP = (p, id, n, extra = "") => + draw ? outline(p, id, n) : path(p, id, extra); + const tail = `${drawP(P.tail, "pixel-tail-contour", 1)} + ${draw ? "" : ``}`; + const ears = `${drawP(P.leftEar, "pixel-ear-left-contour", 2)} + ${draw ? "" : ``} + ${drawP(P.rightEar, "pixel-ear-right-contour", 3)} + ${draw ? "" : ``}`; + const feet = `${drawP(P.leftFoot, "pixel-foot-left", 9, `fill="${tip}"`)}${drawP(P.rightFoot, "pixel-foot-right", 10, `fill="${tip}"`)}`; + const paws = + pose === "wave" + ? `${drawP(P.leftPaw, "pixel-paw-left", 11)} + ${drawP(P.rightPaw, "pixel-paw-right", 12)}` + : `${drawP(P.leftPaw, "pixel-paw-left", 11)}${drawP(P.rightPaw, "pixel-paw-right", 12)}`; + const tuft = draw + ? outline(P.tuft, "pixel-hair-tuft", 7) + : ``; return ` ${tail} - ${drawP(P.body,"pixel-body",4,`fill="url(#pixel-soft-shade)"`)} + ${drawP(P.body, "pixel-body", 4, `fill="url(#pixel-soft-shade)"`)} ${ears} - ${drawP(P.head,"pixel-head-contour",5,`fill="url(#pixel-soft-shade)"`)} + ${drawP(P.head, "pixel-head-contour", 5, `fill="url(#pixel-soft-shade)"`)} ${tuft} - ${draw?"":face(expression)} + ${draw ? "" : face(expression)} ${feet} ${paws} - ${draw?"":``} + ${draw ? "" : ``} `; } - function backdrop(pose="neutral"){ - const y=pose==="jump"?457:pose==="crouch"||pose==="landing"?451:450; - return ``; + function backdrop(pose = "neutral") { + const y = + pose === "jump" + ? 457 + : pose === "crouch" || pose === "landing" + ? 451 + : 450; + return ``; } - const output={}; - const assign=(name,content)=>{output[`${root}/${name}`]=content;}; - assign("pixel-master.svg",wrapper(`${backdrop()}${rig("happy")}`,"Pixel canonical vector master","Original chubby Pixel mascot with extended black-tipped ears, expressive eyes, graphite outlines, individual facial and body rig groups.")); - assign("pixel-static-fallback.svg",wrapper(`${backdrop()}${rig("happy")}`,"Pixel resting pose","Fully illustrated, motion-free mascot fallback for reduced motion, unsupported animation, and editorial usage.")); - assign("pixel-hero-outline.svg",wrapper(`${backdrop()}${rig("happy","neutral",{outlineOnly:true})}`,"Pixel hero pencil outline","Stroke-only canonical contours with stable pencil ordering and path lengths for the planned progressive hero drawing.")); - for(const e of ["neutral","happy","curious","thinking","mischievous","awake","eye-roll","celebrating","focused","concerned"]) { - assign(`expressions/pixel-${e}.svg`,wrapper(`${backdrop()}${rig(e,e==="thinking"?"thinking":"neutral")}`,`Pixel expression: ${e}`,`Official Pixel character expression study: ${e}. Same canonical body and ears with independent facial vector groups.`)); + const output = {}; + const assign = (name, content) => { + output[`${root}/${name}`] = content; + }; + assign( + "pixel-master.svg", + wrapper( + `${backdrop()}${rig("happy")}`, + "Pixel canonical vector master", + "Original chubby Pixel mascot with extended black-tipped ears, expressive eyes, graphite outlines, individual facial and body rig groups.", + ), + ); + assign( + "pixel-static-fallback.svg", + wrapper( + `${backdrop()}${rig("happy")}`, + "Pixel resting pose", + "Fully illustrated, motion-free mascot fallback for reduced motion, unsupported animation, and editorial usage.", + ), + ); + assign( + "pixel-hero-outline.svg", + wrapper( + `${backdrop()}${rig("happy", "neutral", { outlineOnly: true })}`, + "Pixel hero pencil outline", + "Stroke-only canonical contours with stable pencil ordering and path lengths for the planned progressive hero drawing.", + ), + ); + for (const e of [ + "neutral", + "happy", + "curious", + "thinking", + "mischievous", + "awake", + "eye-roll", + "celebrating", + "focused", + "concerned", + ]) { + assign( + `expressions/pixel-${e}.svg`, + wrapper( + `${backdrop()}${rig(e, e === "thinking" ? "thinking" : "neutral")}`, + `Pixel expression: ${e}`, + `Official Pixel character expression study: ${e}. Same canonical body and ears with independent facial vector groups.`, + ), + ); } - const poses=[ - ["fully-drawn","happy","neutral"], - ["anticipation-crouch","focused","crouch"], - ["bump-up-jump","celebrating","jump"], - ["landing-squash","landing","landing"], - ["idle-resting","idle","neutral"], - ["hover-wave","happy","wave"], - ["thinking","thinking","thinking"], - ["surprised-awake","awake","neutral"] + const poses = [ + ["fully-drawn", "happy", "neutral"], + ["anticipation-crouch", "focused", "crouch"], + ["bump-up-jump", "celebrating", "jump"], + ["landing-squash", "landing", "landing"], + ["idle-resting", "idle", "neutral"], + ["hover-wave", "happy", "wave"], + ["thinking", "thinking", "thinking"], + ["surprised-awake", "awake", "neutral"], ]; - for(const [name,expression,pose] of poses) { - assign(`poses/pixel-${name}.svg`,wrapper(`${backdrop(pose)}${rig(expression,pose)}`,`Pixel pose: ${name.replaceAll("-"," ")}`,`Motion key art for Pixel: ${name.replaceAll("-"," ")}. Static vector illustration, no active animation.`)); + for (const [name, expression, pose] of poses) { + assign( + `poses/pixel-${name}.svg`, + wrapper( + `${backdrop(pose)}${rig(expression, pose)}`, + `Pixel pose: ${name.replaceAll("-", " ")}`, + `Motion key art for Pixel: ${name.replaceAll("-", " ")}. Static vector illustration, no active animation.`, + ), + ); } - assign("pixel-face-rig.svg",wrapper(`${path(P.head,"pixel-head-contour",`fill="url(#pixel-soft-shade)"`)}${face("happy")}`, - "Pixel facial rig reference","Individual vector elements named for both eyes, separate pupils, mouth, brows, whiskers and nose. This is a source illustration, not a functional animation.")); + assign( + "pixel-face-rig.svg", + wrapper( + `${path(P.head, "pixel-head-contour", `fill="url(#pixel-soft-shade)"`)}${face("happy")}`, + "Pixel facial rig reference", + "Individual vector elements named for both eyes, separate pupils, mouth, brows, whiskers and nose. This is a source illustration, not a functional animation.", + ), + ); // The canonical reference's laptop is an optional prop, never baked into the character rig. - const laptop=` + const laptop = ` `; - assign("pixel-laptop-companion.svg",wrapper(`${backdrop()}${rig("happy")}${laptop}`,"Pixel at a laptop","Optional original-reference-inspired prop scene; the underlying character body remains independently editable.")); - const manifest={version:"phase-a2",status:"artwork-only",canonicalReference:"/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg",sourceOfTruth:"/brand/pixel/pixel-master.svg",license:"Creators first-party brand media",assetType:"vector-only SVG, no embedded raster, no external resources",master:"pixel-master.svg",staticFallback:"pixel-static-fallback.svg",heroOutline:"pixel-hero-outline.svg",facialRig:"pixel-face-rig.svg",expressionIds:["neutral","happy","curious","thinking","mischievous","awake","eye-roll","celebrating","focused","concerned"],poseIds:poses.map(x=>x[0]),notes:"Artwork preparation only; do not activate character animation or replace current logo/mascots until separately approved."}; - assign("manifest.json",JSON.stringify(manifest,null,2)+"\n"); + assign( + "pixel-laptop-companion.svg", + wrapper( + `${backdrop()}${rig("happy")}${laptop}`, + "Pixel at a laptop", + "Optional original-reference-inspired prop scene; the underlying character body remains independently editable.", + ), + ); + const manifest = { + version: "phase-a2", + status: "artwork-only", + canonicalReference: + "/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg", + sourceOfTruth: "/brand/pixel/pixel-master.svg", + license: "Creators first-party brand media", + assetType: "vector-only SVG, no embedded raster, no external resources", + master: "pixel-master.svg", + staticFallback: "pixel-static-fallback.svg", + heroOutline: "pixel-hero-outline.svg", + facialRig: "pixel-face-rig.svg", + expressionIds: [ + "neutral", + "happy", + "curious", + "thinking", + "mischievous", + "awake", + "eye-roll", + "celebrating", + "focused", + "concerned", + ], + poseIds: poses.map((x) => x[0]), + notes: + "Artwork preparation only; do not activate character animation or replace current logo/mascots until separately approved.", + }; + assign("manifest.json", JSON.stringify(manifest, null, 2) + "\n"); return output; }; -const assets=buildPixelAssets(); -const command=process.argv[2]; -if(command!=="--write" && command!=="--check") { console.error("Usage: node apps/web/scripts/build-pixel-phase-a2.mjs --check|--write"); process.exitCode=2; } -else { - let problems=0; - for(const [path,expected] of Object.entries(assets)){ - const absolute=resolve(repoRoot,path); - if(command==="--write"){ await mkdir(dirname(absolute),{recursive:true}); await writeFile(absolute,expected,"utf8"); } - else { const actual=await readFile(absolute,"utf8").catch(()=>null); if(actual!==expected) { console.error("Out of date: "+path); problems++; } } - } - if(problems) process.exitCode=1; - else console.log((command==="--write"?"Wrote ":"Verified ")+Object.keys(assets).length+" Pixel Phase A2 artwork files"); +const assets = buildPixelAssets(); +const command = process.argv[2]; +if (command !== "--write" && command !== "--check") { + console.error( + "Usage: node apps/web/scripts/build-pixel-phase-a2.mjs --check|--write", + ); + process.exitCode = 2; +} else { + let problems = 0; + for (const [path, expected] of Object.entries(assets)) { + const absolute = resolve(repoRoot, path); + if (command === "--write") { + await mkdir(dirname(absolute), { recursive: true }); + await writeFile(absolute, expected, "utf8"); + } else { + const actual = await readFile(absolute, "utf8").catch(() => null); + if (actual !== expected) { + console.error("Out of date: " + path); + problems++; + } + } + } + if (problems) process.exitCode = 1; + else + console.log( + (command === "--write" ? "Wrote " : "Verified ") + + Object.keys(assets).length + + " Pixel Phase A2 artwork files", + ); } diff --git a/apps/web/src/lib/pixel-artwork.test.ts b/apps/web/src/lib/pixel-artwork.test.ts index f53eea239..92f23c31d 100644 --- a/apps/web/src/lib/pixel-artwork.test.ts +++ b/apps/web/src/lib/pixel-artwork.test.ts @@ -6,8 +6,7 @@ import sharp from "sharp"; import { describe, expect, it } from "vitest"; const assetUrl = new URL("../../public/brand/pixel/", import.meta.url); -const content = (file: string) => - readFileSync(new URL(file, assetUrl), "utf8"); +const content = (file: string) => readFileSync(new URL(file, assetUrl), "utf8"); const manifest = JSON.parse(content("manifest.json")) as { version: string; @@ -61,7 +60,9 @@ describe("Pixel Phase A2 artwork exports", () => { ); expect(svg).not.toMatch(/(?:href|src)=["'](?:https?:)?\/\//i); for (const match of svg.matchAll(/]*\/>/g)) { - expect([...match[0].matchAll(/\sfill=/g)].length).toBeLessThanOrEqual(1); + expect([...match[0].matchAll(/\sfill=/g)].length).toBeLessThanOrEqual( + 1, + ); } }, ); diff --git a/docs/pixel-phase-a2.md b/docs/pixel-phase-a2.md index 49a8590e1..292a29a97 100644 --- a/docs/pixel-phase-a2.md +++ b/docs/pixel-phase-a2.md @@ -2,25 +2,46 @@ ## Scope and approval -This PR contributes first-party static vector artwork **only**. The existing homepage, Pixel assistant, process illustrations, logo, manifest, site navigation, and billing remain untouched. The original existing chubby Pixel reference and revised Phase A character sheet are the user's art-direction references. The newly drawn vector is a faithful **adaptation for rigging**, not a lossless vectorization of the embedded PNG in the legacy SVG. +This PR contributes first-party static vector artwork **only**. The existing +homepage, Pixel assistant, process illustrations, logo, manifest, site +navigation, and billing remain untouched. The original existing chubby Pixel +reference and revised Phase A character sheet are the user's art-direction +references. The newly drawn vector is a faithful **adaptation for rigging**, not +a lossless vectorization of the embedded PNG in the legacy SVG. ## Visual contract -- Legacy anatomy reference: `apps/web/public/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg` +- Legacy anatomy reference: + `apps/web/public/brand/mascots/creators-mascot-working-laptop-queued-laptop-float.svg` - Vector master: `apps/web/public/brand/pixel/pixel-master.svg` -- Identity traits: broad sideways graphite-tipped ears, round face, small flick of hair, glossy eyes, whiskers, tiny smiling muzzle, chubby short-limbed body, curved dark-tipped tail, dark feet -- Ivory/graphite artwork with restrained pencil texture; violet/coral are environment and annotation accents, not a replacement character palette -- Shared geometry across the entire expression/pose export library; props remain distinct from the master +- Identity traits: broad sideways graphite-tipped ears, round face, small flick + of hair, glossy eyes, whiskers, tiny smiling muzzle, chubby short-limbed body, + curved dark-tipped tail, dark feet +- Ivory/graphite artwork with restrained pencil texture; violet/coral are + environment and annotation accents, not a replacement character palette +- Shared geometry across the entire expression/pose export library; props remain + distinct from the master ## Reviewer checklist -1. Visually compare master, fallback and optional laptop scene with the original reference. +1. Visually compare master, fallback and optional laptop scene with the original + reference. 2. Check every expression, especially eye-roll, happy, awake and concerned. -3. Examine crouch, bump/jump, landing squash, idle and wave as separate motion key illustrations. -4. Verify master and face rig contain independently addressable head/ears/tail/paws/eyes/pupils/mouth layers. -5. Inspect real stroke-only contours and ordering metadata in `pixel-hero-outline.svg`; it must not be a bitmap wipe. -6. Render representative artwork on illustration-paper in both light and dark surrounding themes, at hero and smaller preview sizes. -7. Verify assets contain no embedded raster, active animation, foreign/external refs or scripts. -8. Verify deterministic export synchronization with `node apps/web/scripts/build-pixel-phase-a2.mjs --check`; then run `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build`. +3. Examine crouch, bump/jump, landing squash, idle and wave as separate motion + key illustrations. +4. Verify master and face rig contain independently addressable + head/ears/tail/paws/eyes/pupils/mouth layers. +5. Inspect real stroke-only contours and ordering metadata in + `pixel-hero-outline.svg`; it must not be a bitmap wipe. +6. Render representative artwork on illustration-paper in both light and dark + surrounding themes, at hero and smaller preview sizes. +7. Verify assets contain no embedded raster, active animation, foreign/external + refs or scripts. +8. Verify deterministic export synchronization with + `node apps/web/scripts/build-pixel-phase-a2.mjs --check`; then run + `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, `pnpm test`, and + `pnpm build`. -Art review and final-head CI are separate acceptance requirements. Do not merge until both are complete. Phase B implements motion/pointer/scroll, and subsequent phases implement the homepage; neither belongs in this PR. +Art review and final-head CI are separate acceptance requirements. Do not merge +until both are complete. Phase B implements motion/pointer/scroll, and +subsequent phases implement the homepage; neither belongs in this PR. From 81408afd3fd1ea011f981f8acb1566ea655a315f Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Mon, 12 Oct 2026 14:18:42 +0530 Subject: [PATCH 3/4] fix(brand): remove unused artwork generator palette constant --- apps/web/scripts/build-pixel-phase-a2.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/scripts/build-pixel-phase-a2.mjs b/apps/web/scripts/build-pixel-phase-a2.mjs index 398a0af92..f3ab8d777 100644 --- a/apps/web/scripts/build-pixel-phase-a2.mjs +++ b/apps/web/scripts/build-pixel-phase-a2.mjs @@ -29,7 +29,6 @@ const buildPixelAssets = function buildPixelAssets() { ivory = "#fbfaf6", shadow = "#dedbd5", tip = "#333235", - violet = "#8760c6", coral = "#e8867a"; const xml = (s) => s From 31f8054377bf391507ba7d3978c954a3504ba0c1 Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Mon, 12 Oct 2026 14:23:51 +0530 Subject: [PATCH 4/4] test(brand): render Pixel SVG assets at native density --- apps/web/src/lib/pixel-artwork.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/lib/pixel-artwork.test.ts b/apps/web/src/lib/pixel-artwork.test.ts index 92f23c31d..cd098d143 100644 --- a/apps/web/src/lib/pixel-artwork.test.ts +++ b/apps/web/src/lib/pixel-artwork.test.ts @@ -103,7 +103,7 @@ describe("Pixel Phase A2 artwork exports", () => { "poses/pixel-bump-up-jump.svg", "poses/pixel-hover-wave.svg", ]) { - const image = sharp(Buffer.from(content(file)), { density: 96 }); + const image = sharp(Buffer.from(content(file))); const metadata = await image.metadata(); expect(metadata.width).toBe(720); expect(metadata.height).toBe(520);