Higgsfield website builder (CLI) — three product types, three flows You drive the whole lifecycle through the Higgsfield CLI ( ), then edit code on the local filesystem with + . You are building ONE per-website Cloudflare Worker: a React 19 + TanStack Start app, server-rendered (SSR) , deployed as a single Worker at the product's own subdomain. The project lives in — run every /build command from there. The three types — and the REQUIRED on create requires , and it is the USER'S choice — when the request doesn't make it obvious, ask the user before creating (one question, up front): - — a sta…

,\n provider: { '@id': 'https://acme-studio.higgsfield.app/#org' },\n },\n ],\n});\n```\n\n### Pitfalls\n\n1. **No relative URLs.** Every `url`, `logo`, `image` field must be an absolute `https://` URL. Schema validators reject relative paths silently.\n2. **Don't duplicate schemas.** Use `@graph` to bundle multiple types in one `\u003cscript>` tag. Multiple `\u003cscript type=\"application/ld+json\">` blocks are valid but harder to maintain.\n3. **Match visible content.** Schema `name`/`description` must match what the user sees on the page. Google penalizes mismatches.\n4. **Test with Google Rich Results Test** (https://search.google.com/test/rich-results) before shipping. Schema syntax errors are invisible to users but block rich results.\n5. **Keep schemas on the pages they describe.** Organization schema goes on the homepage. Product schema goes on the product page. Don't dump everything on every page.\n\n## Entity SEO\n\n### What It Is\n\nEntity optimization establishes the site's primary entity (business, person, product) as a distinct node in knowledge graphs used by Google, Bing, and AI search engines. The goal is unambiguous machine identification — when an AI engine mentions your entity, it should pull the correct name, description, and links.\n\n### Entity Data Model\n\nCollect these fields during the business intake (the \"Schema markup\" section above handles the base schema; this section enriches it):\n\n| Field | Example | Required |\n|----------------|----------------------------------------------|----------|\n| name | Acme Corp | Yes |\n| description | Automated invoicing for logistics companies | Yes |\n| url | https://acmecorp.com | Yes |\n| logo | https://acmecorp.com/logo.png | Yes |\n| sameAs[] | [LinkedIn URL, Instagram URL, ...] | Yes |\n| foundingDate | 2019-03-15 | If known |\n| industry | Financial Technology | If known |\n| areaServed | North America, Europe | If known |\n\n### sameAs Strategy\n\n`sameAs` tells search engines which external profiles belong to this entity. Include URLs from:\n\n- **LinkedIn** — company page URL (e.g. `https://linkedin.com/company/acmecorp`)\n- **Instagram** — `https://instagram.com/acmecorp`\n- **X (Twitter)** — `https://x.com/acmecorp`\n- **GitHub** — `https://github.com/acmecorp` (if applicable)\n- **Crunchbase** — `https://crunchbase.com/organization/acmecorp` (if listed)\n- **Wikidata** — `https://wikidata.org/wiki/Q12345` (if an entry exists)\n\nDuring intake, ask the client for all active social/professional profiles. Verify each URL resolves (don't include dead links). Order from highest authority to lowest. Only include profiles the entity actually controls.\n\n### Consistent NAP\n\nName, Address, and Phone must be identical across:\n\n1. **JSON-LD structured data** — the Organization schema on the page\n2. **Visible page content** — the footer or contact section\n3. **External listings** — Google Business Profile, Yelp, LinkedIn, etc.\n\nEven minor differences (\"St.\" vs \"Street\", \"+1 555\" vs \"555\") fragment the entity in knowledge graphs. Pick one canonical format and enforce it everywhere. If the business has no physical address, omit `address` entirely rather than using a fake or partial one.\n\n### Multi-Entity @graph Pattern\n\nWhen a page represents multiple entities (the company, its founder, and its product), use the `@graph` array to define them in a single JSON-LD block with cross-references:\n\n```json\n{\n \"@context\": \"https://schema.org\",\n \"@graph\": [\n {\n \"@type\": \"Organization\",\n \"@id\": \"https://acmecorp.com/#org\",\n \"name\": \"Acme Corp\",\n \"url\": \"https://acmecorp.com\",\n \"logo\": \"https://acmecorp.com/logo.png\",\n \"founder\": { \"@id\": \"https://acmecorp.com/#founder\" },\n \"sameAs\": [\n \"https://linkedin.com/company/acmecorp\",\n \"https://instagram.com/acmecorp\"\n ]\n },\n {\n \"@type\": \"Person\",\n \"@id\": \"https://acmecorp.com/#founder\",\n \"name\": \"Jane Smith\",\n \"jobTitle\": \"CEO & Founder\",\n \"worksFor\": { \"@id\": \"https://acmecorp.com/#org\" },\n \"sameAs\": [\"https://linkedin.com/in/janesmith\"]\n },\n {\n \"@type\": \"SoftwareApplication\",\n \"@id\": \"https://acmecorp.com/#product\",\n \"name\": \"Acme Invoicing\",\n \"applicationCategory\": \"BusinessApplication\",\n \"offers\": {\n \"@type\": \"Offer\",\n \"price\": \"99\",\n \"priceCurrency\": \"USD\"\n },\n \"provider\": { \"@id\": \"https://acmecorp.com/#org\" }\n }\n ]\n}\n```\n\nKey rules: each entity gets a unique `@id` (use URL fragments like `/#org`, `/#founder`). Cross-reference via `{ \"@id\": \"...\" }` rather than nesting the full object. This lets search engines build a connected graph rather than treating each entity as isolated.\n\n### Implementation\n\nUse the `StructuredData` component from the \"Schema markup\" section above. Build the JSON-LD string at module level with all entity fields populated, then pass it as the `json` prop:\n\n```tsx\nimport { StructuredData } from '~/components/StructuredData';\n\nconst entityJson = JSON.stringify({\n \"@context\": \"https://schema.org\",\n \"@graph\": [ /* entities as above */ ]\n});\n\nexport default function Page() {\n return (\n \u003c>\n \u003cStructuredData json={entityJson} />\n {/* visible page content */}\n \u003c/>\n );\n}\n```\n\nThe visible page content must reflect every claim in the schema — name, description, address, founding date. Don't put data in JSON-LD that visitors can't see.\n\n### Pitfalls\n\n1. **Dead sameAs links** — Including social URLs that 404 or redirect to a login wall. Verify every URL before adding it to the schema.\n2. **NAP fragmentation** — Using \"Acme Corp\" in schema but \"Acme Corporation\" in the footer. Pick one canonical name.\n3. **Missing @id cross-references** — Defining Organization and Person in the same @graph but not linking them via `founder`/`worksFor`. Without links, search engines treat them as unrelated.\n4. **Over-claiming sameAs** — Listing profiles the entity doesn't own (e.g. a Wikipedia article about a different \"Acme\"). Every sameAs URL must be a profile controlled by or specifically about this entity.\n5. **Invisible schema data** — Putting industry, areaServed, or founding date in JSON-LD without showing it anywhere on the page. Search engines increasingly penalize schema that has no visible counterpart.\n\n## GEO / content\n\n### What is GEO\n\nGenerative Engine Optimization (GEO) is the practice of structuring website content so AI-powered search engines (ChatGPT, Perplexity, Gemini, Copilot) can extract, cite, and surface it in generated answers. Traditional SEO gets you ranked; GEO gets you quoted.\n\n### 7 Principles\n\n#### 1. Direct Answer Structure\n\nLead every section with the answer, not a buildup. AI engines extract the first sentence that resolves the query — if your answer is buried in paragraph three, it won't be selected. Write the topic sentence as a standalone factual statement, then add supporting detail below. Pattern: \"X is Y. It works by Z. This matters because W.\"\n\n#### 2. Entity Clarity\n\nName and type the primary entity within the first 100 words of the page. \"Acme Corp is a B2B SaaS company that provides automated invoicing for mid-market logistics firms.\" This gives AI engines the subject, category, and scope immediately. Avoid opening with generic statements like \"Welcome to our website\" or \"In today's fast-paced world.\"\n\n#### 3. Factual Specificity\n\nReplace vague claims with concrete, verifiable data points. AI engines prefer citable facts over marketing language. \"Trusted by many clients\" → \"Used by 200+ logistics companies across 14 countries since 2019.\" \"Industry-leading uptime\" → \"99.97% uptime over the trailing 12 months, verified by StatusPage.\" Every stat should be something you can back up if challenged.\n\n#### 4. Schema-Content Alignment\n\nThe JSON-LD structured data must reflect what's visible on the page — not aspirational content, not a different description, not extra services not mentioned in the copy. If the Organization schema says `\"description\": \"AI-powered invoicing platform\"`, those words must appear in the visible hero or about section. AI engines cross-reference schema against page text; mismatches reduce trust signals.\n\n#### 5. FAQ Sections\n\nAdd a FAQ section with direct question-and-answer pairs. Each answer should be 1-3 sentences — long enough to be useful, short enough to be extractable. Wrap in FAQPage schema.\n\nComponent pattern:\n\n```tsx\nfunction FAQ({ items }: { items: { q: string; a: string }[] }) {\n return (\n \u003csection>\n \u003ch2>Frequently Asked Questions\u003c/h2>\n {items.map((item, i) => (\n \u003cdetails key={i}>\n \u003csummary>{item.q}\u003c/summary>\n \u003cp>{item.a}\u003c/p>\n \u003c/details>\n ))}\n \u003c/section>\n );\n}\n```\n\nMatching FAQPage schema (add to the page's JSON-LD):\n\n```json\n{\n \"@type\": \"FAQPage\",\n \"mainEntity\": [\n {\n \"@type\": \"Question\",\n \"name\": \"What does Acme Corp do?\",\n \"acceptedAnswer\": {\n \"@type\": \"Answer\",\n \"text\": \"Acme Corp provides automated invoicing software for mid-market logistics companies, handling billing, compliance, and payment reconciliation.\"\n }\n }\n ]\n}\n```\n\n#### 6. Citation-Friendly Headings\n\nWrite H2 and H3 headings that match the queries people (and AI) actually ask. Not \"Our Approach\" but \"How Acme Corp Automates Invoice Processing.\" Not \"Features\" but \"Key Features of Acme Invoicing Software.\" The heading should be a valid search query on its own. AI engines use headings as section identifiers when constructing citations — a descriptive heading increases the chance your section gets attributed.\n\n#### 7. Topical Authority\n\nDemonstrate depth through internal linking, consistent entity naming, and expertise signals. Link related sections to each other (e.g. the pricing page links to the features page with descriptive anchor text). Use the exact same entity name everywhere — don't alternate between \"Acme\", \"Acme Corp\", \"ACME Corporation\", and \"our company.\" Include an expertise section (team credentials, years in operation, certifications) to strengthen E-E-A-T signals that AI engines evaluate.\n\n---\n\n### Before / After Example\n\n**Before (vague, buried answer):**\n```\nWelcome to Acme Corp. We've been in business for years and pride\nourselves on excellent service. Our innovative platform helps\ncompanies manage their finances. Many organizations trust us.\nContact us to learn more about what we can do for you.\n```\n\n**After (GEO-optimized):**\n```\nAcme Corp is an automated invoicing platform for mid-market\nlogistics companies, processing over 2M invoices annually across\n14 countries. Founded in 2019, the platform reduces manual billing\ntime by 73% through AI-powered line-item matching and compliance\nchecks. Acme serves 200+ customers including DHL Freight and Kuehne+Nagel.\n```\n\n---\n\n### Pitfalls\n\n1. **Keyword stuffing for AI** — Repeating the same phrase unnaturally hoping AI will pick it up. AI engines detect and penalize this the same way traditional search does.\n2. **Schema without matching content** — Adding FAQPage schema for questions never shown on the page. This is structured-data spam and triggers trust penalties.\n3. **Walls of text with no structure** — AI engines rely on headings, lists, and paragraphs to segment content. A 2000-word block with no subheadings is effectively invisible to extraction.\n4. **Answering questions indirectly** — \"Contact us for pricing\" instead of showing actual pricing. AI engines skip sections that don't contain an answer.\n5. **Inconsistent entity naming** — Switching between \"Acme Corp\", \"Acme\", \"the company\", and \"we\" makes it harder for AI to build a coherent entity profile. Pick one primary name and use it consistently, especially in headings and first sentences.\n\n## Audit\n\n### When to Run\n\nRun this audit after building any website and before running `higgsfield website deploy`. It is the SEO quality gate. Do not deploy until every FAIL is resolved. The audit is self-contained — read the project source files directly, no external tools or browser needed.\n\n### Audit Procedure\n\nRead every route/page component in `app/src/routes/`. For each file, evaluate the 10 checks below. Collect results, print the summary table, fix any FAILs, then re-run until clean.\n\n---\n\n#### 1. Heading Hierarchy\n\nVerify exactly one `\u003ch1>` per page. Sections use `\u003ch2>`, subsections `\u003ch3>`, etc. No skipped levels (e.g. `\u003ch1>` followed by `\u003ch3>` with no `\u003ch2>`). The `\u003ch1>` must contain or closely match the page's primary keyword.\n\nFAIL if: multiple `\u003ch1>` tags, zero `\u003ch1>` tags, or any skipped heading level.\n\n#### 2. Image Alt Text\n\nEvery `\u003cimg>` element must have a non-empty `alt` attribute. For AI-generated images, derive `alt` from the generation prompt (e.g. prompt \"modern office interior\" → `alt=\"Modern office interior\"`). Decorative images use `alt=\"\"` with `role=\"presentation\"`.\n\nFAIL if: any `\u003cimg>` lacks `alt`. WARN if: `alt` is generic like \"image\" or \"photo\".\n\n#### 3. Link Text Quality\n\nAnchor text must describe the destination. Flag any `\u003ca>` whose visible text is \"click here\", \"read more\", \"learn more\", \"here\", or \"link\". Replace with descriptive text that makes sense out of context.\n\nFAIL if: any non-descriptive anchor text found.\n\n#### 4. Content-to-Code Ratio\n\nScan each page component for visible text content vs. JS/markup overhead. A page should have at least 200 words of visible text (excluding nav/footer boilerplate). Flag pages that are mostly animations, images, or interactive JS with minimal readable text.\n\nWARN if: visible text is under 200 words. FAIL if: under 50 words.\n\n#### 5. Keyword Alignment\n\nIdentify the page's primary keyword (from the business intake or page purpose). Verify it appears in: the `\u003ctitle>` tag, the `\u003ch1>`, the first paragraph of body text, and the `\u003cmeta name=\"description\">` content. Phrasing can vary but the core term must be present.\n\nFAIL if: keyword missing from title or H1. WARN if: missing from first paragraph or meta description.\n\n#### 6. Mobile Readability\n\nCheck CSS/Tailwind classes for: no `font-size` below 16px on body text (12px acceptable only for captions/labels), `line-height` at least 1.5 on paragraphs, sufficient color contrast (no light gray on white). Verify the viewport meta tag is present.\n\nFAIL if: viewport meta missing. WARN if: body text under 16px or line-height under 1.4.\n\n#### 7. Keyboard Navigation\n\nAll interactive elements (`\u003cbutton>`, `\u003ca>`, `\u003cinput>`, custom clickable `\u003cdiv>`s) must be keyboard-focusable. Any `\u003cdiv>` or `\u003cspan>` with an `onClick` must also have `role=\"button\"`, `tabIndex={0}`, and a keyboard handler. Check for visible focus indicators (no `outline-none` without a replacement).\n\nFAIL if: clickable element lacks keyboard support. WARN if: `outline-none` used without custom focus style.\n\n#### 8. Fragment Integrity\n\nFind all `href=\"#...\"` links in the page. For each fragment identifier, verify a matching `id` exists in the rendered DOM of the same page. Check both static IDs and dynamically generated ones (section slugs, etc.).\n\nFAIL if: any fragment link points to a non-existent ID.\n\n#### 9. Form Accessibility\n\nEvery `\u003cinput>`, `\u003cselect>`, and `\u003ctextarea>` must have an associated `\u003clabel>` (via `htmlFor`/`id` pairing) or an `aria-label`/`aria-labelledby`. Required fields must have the `required` attribute or `aria-required=\"true\"`. Error messages must be linked via `aria-describedby`.\n\nFAIL if: any input lacks a label. WARN if: required fields not marked.\n\n#### 10. Social Preview\n\nVerify `\u003cmeta property=\"og:title\">`, `\u003cmeta property=\"og:description\">`, and `\u003cmeta property=\"og:image\">` exist. The `og:image` must be an absolute URL (starts with `https://`), not a relative path. Also check for `\u003cmeta name=\"twitter:card\">`.\n\nFAIL if: `og:image` missing or relative. WARN if: `og:title` or `og:description` missing.\n\n---\n\n### Output Format\n\nAfter scanning, print this table to the build log:\n\n```\n┌─────────────────────────┬────────┬──────────────────────────────────┐\n│ Check │ Status │ Detail │\n├─────────────────────────┼────────┼──────────────────────────────────┤\n│ Heading hierarchy │ PASS │ │\n│ Image alt text │ FAIL │ 2 images missing alt in Hero.tsx │\n│ Link text quality │ PASS │ │\n│ Content-to-code ratio │ WARN │ 120 words on /pricing │\n│ Keyword alignment │ PASS │ │\n│ Mobile readability │ PASS │ │\n│ Keyboard navigation │ PASS │ │\n│ Fragment integrity │ FAIL │ #team target missing │\n│ Form accessibility │ PASS │ │\n│ Social preview │ PASS │ │\n├─────────────────────────┼────────┼──────────────────────────────────┤\n│ RESULT │ BLOCK │ 2 FAIL — fix before deploy │\n└─────────────────────────┴────────┴──────────────────────────────────┘\n```\n\nStatus values: **PASS** (good), **WARN** (acceptable, note for improvement), **FAIL** (must fix before deploy).\n\n### Fix-and-Recheck Loop\n\n1. For each FAIL, open the source file and apply the fix directly.\n2. After fixing all FAILs, re-run the full 10-item audit from the top.\n3. Repeat until the table shows zero FAILs.\n4. WARNs are acceptable for deploy but should be noted in the deploy summary.\n5. Only after a clean pass (zero FAILs), proceed to `higgsfield website deploy`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":36672,"content_sha256":"c088f089f365fae621dac90db77b56abdc47463becb2dbd182bd0cd04de98ee7"},{"filename":"references/website-flow.md","content":"# Website flow — `type: \"website\"` (independent brand, phased pipeline)\n\nYou are building ONE per-website Cloudflare Worker: a **React 19 + TanStack\nStart** app that is **server-rendered (SSR)** and deploys as a single Worker\nserved at the website's own subdomain. A `type: \"website\"` product is a\nSTANDALONE brand — no Higgsfield integration, no Quanta, no fnf SDK; the\nuser's brand is the only brand on the page. If mid-build the request turns\nout to need Higgsfield generation, sign-in, or credits, that is a\n`type: \"app\"` — switch to `references/app-flow.md`.\n\n**Higgsfield as the asset engine — EVERY build.** All visual assets on every\nwebsite are generated with the Higgsfield CLI generation commands per\n`references/asset-system.md`. (Internal use of Higgsfield generation is\ninvisible to visitors and always fine.)\n\n**Scope: build the real, full app.** This stack ships complete, production-grade\napplications: real frontend AND backend, database (D1), file storage (R2), auth,\nthird-party API integrations, background work. When the user describes an app,\ntool, site, or web product, BUILD IT with the website builder — it is the default\nfor any web target. Never ask the user to confirm the platform, never downgrade to\na \"demo\"/mockup as the safe option, and don't stall on scope questions beyond the\nsingle intake round defined below. Take the fullest reasonable interpretation\nand ship a working site.\n\n**Repo layout.** The website project lives in **`app/`** — its own `package.json`,\n`src/`, `packages/`, `migrations/`, build config, and the deploy inputs\n(`app.manifest.json`, `wrangler.jsonc`). Run every `bun`/build command from there.\n\n---\n\n## THE PIPELINE — phases in order, artifacts + gates, no skipping\n\nEvery NEW build runs this machine. Each phase produces a named artifact the next\nphase consumes. Do not reorder, merge, or skip phases — \"simple\" briefs are where\ngeneric output happens. (Follow-up edits to an existing site do NOT restart the\npipeline — see \"Small edits\" at the bottom.)\n\n### Before Phase 0 — intake (ONE batched round of questions, then never again)\n\nThe `type` is already resolved (the main skill also asks, in that first round,\nwhether to publish to the community feed when ready — remember that for Phase\n6). This single intake round carries the two website-specific questions worth\nasking; put both in the ONE batched round and never ask a second round.\n\n1. **Animation — MANDATORY, ALWAYS ask on every `--type website` build.** This\n question is NEVER skipped — ask it even when the request seems to imply a\n choice (\"an animated site\", \"a plain static page\"), even for a \"simple\" or\n \"quick\" site, even when you think you already know the answer. The user must\n make this call, not you. Offer exactly two options:\n - **Animated (recommended)** → sets `Animation mode: animated-website` — the\n scroll-scrub camera journey (the product default; mark it Recommended).\n - **Non-animated** → sets `Animation mode: non-animated` — a well-crafted site\n with lighter/optional motion, no mandatory camera journey.\n\n If the request already leans one way, still ask — just point the Recommended\n marker at the implied option. Record the picked value on the brief's\n `Animation mode` line (Phase 0). ONLY if the user is genuinely unreachable /\n never answers do you proceed on the default (**Animated**) and say so in one\n line — never as a shortcut to avoid asking.\n2. **Brand constraints** — an existing brand to honor (ask for\n colors/fonts/logo/photos/links) vs. free rein (\"design the brand for me\").\n Whatever they don't have, you generate: the full identity kit plus the\n personalization ladder in `references/asset-system.md` (logo family, icon set,\n patterns, illustrations, state artwork, product universe). Free rein is the\n richer path, not the degraded one.\n\nThe Animation question (1) is always asked — never skip the round to avoid it.\nYou may drop only question 2 when the brief already answers the brand question. If\nthe user skips or is unreachable, default to **Animated** and say so in one line.\nNever ask a second round.\n\n### Phase 0 — Concept (`app/design-brief.md`, committed, BEFORE any code)\n\nWrite the brief (~40 lines). Every section mandatory; a generic line (\"modern and\nclean\", \"Inter\", \"blue accent\") means the brief is not done:\n\n- **Design read** — one sentence: who is this for, what emotional register.\n- **Concept spine** — a nameable narrative idea threading the whole page (e.g.\n \"the site is a calibration instrument\", \"an archive dossier\", \"a stage\").\n Pick from `references/reference-boards.md`'s spine list or invent better.\n- **Delivery tier** — `cinema` (**default**: Lenis+GSAP, Tier-1 hero, scroll\n chapters — carries the animated website) · `spectacle` (briefs saying\n awwwards/webgl/3d/immersive: cinema + WebGL/3D/scrub + custom cursor + a\n second beat) · `editorial` (calm/minimal/B2B: typography + imagery + bespoke\n chrome, micro-motion only). The animated website is the default Tier-1\n experience regardless of tier, so cinema/spectacle is the normal home for it.\n `editorial` drops the scroll-scrub journey to micro-motion — treat choosing it\n as one of the \"user explicitly asked for something other than an animated\n website\" cases, not a default an ordinary B2B/minimal brief falls into on its\n own.\n- **Locked palette** — exact hexes + a one-line defense. Hard bans (mechanical,\n gate-checked): (1) graphite/near-black + orange/amber/ember accent, (2)\n near-black + neon cyan/blue/green accent, (3) beige/cream + brass/clay/\n oxblood, (4) AI purple/violet glow, (5) the palette family of your previous\n build in this chat. Overridable only by the user's explicit brand colors.\n See `references/reference-boards.md` for what to reach for instead.\n- **Locked type** — pairing from the recipe's tables; serif only with a written\n brand justification.\n- **`Animation mode` — MANDATORY explicit state, set from the intake answer.\n HARD STOP.** The brief MUST contain a literal line `Animation mode: \u003cvalue>`\n (the value the user picked at intake — see \"Before Phase 0\"; default Animated\n if they skipped) and you may not leave Phase 0 without it. Only two values are\n legal:\n - **`animated-website`** (the recommended default — from \"Animated\" at intake):\n the seam-locked scroll-scrub camera journey (**A4**), where the visitor's\n scroll flies the page through several connected generated scenes. This value\n OBLIGATES you to also write the journey block into the brief NOW, before\n boards or generation: read **`references/scroll-scrub.md`** and add its\n **Journey** (4–7 named scenes), **Camera architecture** (A or B), **seam\n direction**, and **mobile framing**, plus one sentence on how the journey\n enacts the concept spine. `Animation mode: animated-website` with no journey\n block is an INCOMPLETE brief — a hard stop, not a proceed-anyway. On this\n value, do NOT shop the wow-catalog — the animated website IS the technique;\n the anti-convergence \"no repeat\" rule does NOT force you off it (differ on the\n OTHER five axes instead: world/subject, journey shape, palette, type, CTA\n garments, corner language).\n - **`non-animated`** (from \"Non-animated\" at intake, or a request that clearly\n asked for it): a well-crafted site with lighter/optional motion and NO\n mandatory camera journey. Legal ONLY as the user's own choice — append the\n reason on the line, e.g. `non-animated — user picked Non-animated at intake`\n or `non-animated — \"\u003cverbatim request>\"`. Your own taste, \"calm/minimal/B2B\",\n or \"it's just a simple landing page\" is NOT a reason to pick this — when in\n doubt it is `animated-website`. On this value the site still clears the\n `wow-maker.md` craft floor (bespoke assets, motivated micro-motion, real\n typography); reach into **`references/wow-catalog.md`** for a lighter Tier-1\n technique (named with its catalog ID) or run the editorial tier. Never ship a\n dead flat page.\n\n Either way, a passive autoplay loop is never the Tier-1 mechanic.\n- **Section plan** — ordered, one layout family per section, no consecutive\n repeats, ≥4 families for 6+ sections, eyebrow budget ceil(sections/3).\n- **Asset plan** — the full kit per `references/asset-system.md` (hero visual,\n section plates, content imagery, custom icon set, logo/monogram, OG; + video\n loop for cinema, + GLB for spectacle).\n- **CTA inventory** — every CTA named with its OWN interaction identity (no\n shared button style — see bespoke-chrome in `references/image-to-code.md`).\n\nThe brief is a contract: later phases may not silently contradict it — edit the\nbrief first and say why.\n\n### Phase 1 — Reference boards (design the page as IMAGES)\n\nRead **`references/reference-boards.md`** and execute it: ONE horizontal design\nreference image PER SECTION via `higgsfield generate create` (image models\n`gpt_image_2` / `nano_banana_pro`), one committed\ncombinatorial pick (theme paradigm, background character, typography character,\nhero architecture, section system, 4 signature components, narrative spine,\nsecond-read moment), composition anchor VARYING per board, palette locked across\nall boards. **Look at every board** and re-roll any that reads template-y\n(budget 2 re-rolls). Boards land in `refs/` in the repo. The boards ARE the\ndesign — do not start Phase 3 with a generic board in the set.\n\n### Phase 2 — Asset system (submit everything, then build while it renders)\n\nRead **`references/asset-system.md`** and submit the ENTIRE kit as async jobs\nright after the boards are chosen: hero visual (2 candidates + interaction\npair), section plates, all content imagery, the custom generated icon set, the\nlogo/monogram + favicon, the OG card — plus video loop (cinema) / GLB\n(spectacle). Poll between build steps; download into `app/public/assets/`;\nverify kit coherence when it lands (re-generate anything whose grade fights the\nboards). Never idle waiting on renders; never fall back to stock/picsum/CSS-only.\nFor the animated website (A4, the default), follow `references/scroll-scrub.md`'s\nspecialized scene/clip chain: independent stills/dives may batch, but exact-frame\nforward legs are intentionally sequential. The normal \"submit everything up\nfront\" rule never overrides a real rendered-frame dependency.\n\n### Phase 3 — Build to the boards, section by section\n\nRead **`references/image-to-code.md`** and follow its discipline per section:\nre-read the board at build time, extract text/type-scale/spacing/color/\ncomponent logic, implement faithfully, anti-drift (when your habit disagrees\nwith the board, the board wins). The craft floor in\n**`references/design-recipe.md`** still applies everywhere (hero discipline,\nlayout bans, copy rules, zero em-dashes). Bespoke chrome: every CTA designed in\nits own component with its own interaction identity; no site-wide button\nutility classes. Registry components (`references/wow-maker.md` §5) remain\navailable as raw material — restyled to the boards, never default-skinned.\nBuild static-but-complete; motion is the next phase.\n\n**HARD STOP for `Animation mode: animated-website` — build the journey as the\nspine, not an afterthought.** The scroll-scrub scene media (Phase 2) and the\nscroll-scrub component (`app/src/components/scroll-scrub/scroll-scrub.tsx` +\n`.css`, per `references/scroll-scrub.md` Phase 3) ARE the page — the semantic\nchapters are the page structure, not decoration added later. Materialize the\nscroll-scrub component and wire the real scene data in THIS phase. Do NOT build a\ngeneric static page of sections and plan to \"add the camera journey later\" — that\nis the exact failure this flow guards against, and it is caught by the Phase 5\ngate. If the scene MP4 chain is still rendering, build the scroll-scrub component\nagainst its posters and swap the clips in when they land — never substitute a\nplain static layout for the animated website.\n\n### Phase 4 — Motion pass (tier-mandated, one focused pass)\n\n- **cinema/spectacle:** Lenis smooth scroll bridged to GSAP ScrollTrigger\n (`autoRaf: false` + `gsap.ticker` — without the bridge, scrub stutters).\n- The **Tier-1 hero mechanic** from the brief, fully executed — a half-wired\n version fails review. The hero is the wow carrier and it must respond to\n the USER'S INPUT (scroll plays the movie), never a passive autoplay loop.\n Passive motion the user can't influence does not count as the Tier-1 mechanic.\n- **Animated website (default):** the full-site seam-locked MP4 chain from\n `references/scroll-scrub.md` IS the hero mechanic — it replaces the ordinary\n single hero frame sequence. Let its controller own scroll-to-video time; keep\n the Lenis/GSAP bridge for surrounding motion, and never drive the same media\n with a second ScrollTrigger timeline. Only when the user explicitly asked for\n a non-animated-website treatment do you instead wire the chosen catalog\n technique — e.g. the single scroll-scrubbed hero film per `asset-system.md` §7.\n- Scroll-chapter reveals: staggered headline builds (`split-type` + GSAP or\n registry text components), per-section distinct timing; work rows / cards\n with hover reveals; magnetic nav/CTA physics via `useMotionValue`, never\n `useState`.\n- **Screenshot-safe reveals (hard rule):** nothing waits at `opacity: 0` for an\n IntersectionObserver. The safe recipe: headline/text builds fire ON MOUNT\n (not viewport-gated); scroll-linked effects animate transform/scale/clip\n ONLY, never opacity-to-zero; hover states may use opacity freely. Ignore\n any `whileInView` fade-in examples in the ingredient libraries — they fail\n this gate. A full-page headless screenshot must show every section.\n- **Pin-spacer trap:** a GSAP pinned hero injects a spacer that reads as a\n large blank band in full-page screenshots (guaranteed review failure).\n Use `pinSpacing: false` with the following content sliding over the pinned\n layer, or otherwise verify the full-page shot has no dead band after the\n hero.\n- EVERYTHING `prefers-reduced-motion`-gated with static fallbacks; `[C]`/`[W]`\n components behind the SSR pattern (wow-maker §6). A top-level `window`\n reference crashes SSR — the #1 recurring build failure.\n- spectacle only: custom cursor + WebGL/3D/scrub second beat.\n\n### Phase 5 — Mechanical gate (before first deploy; every item fixed)\n\nRun the grep checklist in\n**`references/review-rubric.md` §A**: placeholders; em/en-dashes; banned palette\nfamilies in tokens; eyebrow ration; unreferenced generated assets (every kit\nfile used); `h-screen`; SSR safety; reduced-motion coverage; **repeated CTA\nclasses** (bespoke-chrome violation); **opacity-0 + whileInView** combinations;\nsection plan honored; copy self-audit. This is a completion gate — do not\ndeploy with a failing item.\n\n### Phase 6 — Deploy\n\n0. `bun run typecheck` once, from `app/` — ~15s locally vs a failed deploy\n round-trip plus a fix-and-redeploy loop. Fix what it finds, then deploy.\n This is the ONE pre-deploy local check; do not also run `bun run build`\n unless typecheck passed and the deploy still failed.\n1. `higgsfield website deploy \u003cwebsite_id>` — this ships the live public site\n immediately; there is no preview stage.\n2. Report: live URL (from `higgsfield website status`) — \"Your site is live:\n \u003curl>\" — + one-line concept statement + anything honestly skipped. Speak in\n product terms — no repo/commit/deploy jargon (see the SKILL.md \"Talking to\n the user\" rule).\n\nDo NOT navigate to, screenshot, or run image analysis on the deployed site —\nthe mechanical gate (the grep checklist in `references/review-rubric.md` §A) is\nthe only verification.\n\n3. **Publish.** If the user opted in at intake, publish automatically now that\n the site is deployed with its cover + metadata filled — run\n `higgsfield website publish \u003cwebsite_id>` (don't wait to be asked) and share\n the community-feed listing URL it reports. Otherwise publish only when the\n user asks. (The $100k contest is for `--type app` products — don't pitch it\n for a plain website.)\n\n---\n\n## Design references — read order\n\n## Design references — read order\n\n1. **`references/design-recipe.md`** — craft floor (ALWAYS read; short).\n2. **`references/scroll-scrub.md`** — the **animated website**, which is the\n DEFAULT Tier-1 experience for every website: read it in Phase 0. It owns the\n specialized boards/assets/runtime sequence and bundled Markdown code\n references for that build. Only read **`references/wow-catalog.md`** (Tier-1\n technique menu + anti-convergence ledger + Phase 4 implementation contracts)\n when the user explicitly asked for something other than an animated website.\n3. **`references/reference-boards.md`** — Phase 1: per-section design boards.\n4. **`references/asset-system.md`** — Phase 2: the Higgsfield asset kit.\n5. **`references/image-to-code.md`** — Phase 3: faithful implementation +\n bespoke chrome + the CTA garment catalog.\n6. **`references/review-rubric.md`** — Phase 5: the mechanical gate.\n7. `references/wow-maker.md` — ingredient directory: motion/3D libs (§4),\n component registries (§5), signature effect patterns (§2), SSR pattern (§6).\n Only listed free/permissive sources may be used.\n8. `references/design-taste-frontend.md` — the full deep-dive playbook behind\n the recipe; consult for specific situations, not required start-to-end.\n9. `references/app-cover.md` — the branded 3:2 launch cover + OG image\n (stadium-capsule mask via the inlined compose script; hosted style refs).\n REQUIRED before every publish (`og_image_url` + `marketplace_cover_url` are\n mandatory feed-card fields — never run `higgsfield website publish` while\n they are empty), and whenever the user asks for a cover/OG image directly.\n\nDo NOT search the skill library for other design guidance — everything is here.\n\nThen route to the FUNCTIONAL reference for the task:\n\n| Task | Read |\n|---|---|\n| **Any website (the DEFAULT — animated website)** / scrollable world / continuous camera journey / diorama fly-through / browse-through-an-industry site | `references/scroll-scrub.md` — seam-locked media pipeline + React/CSS Markdown assets + mobile/QA contract |\n| TanStack Start routes, SSR, server functions, Cloudflare Worker runtime | `references/runtime-and-infra.md` |\n| Cover / OG image (\"cover\", \"обложка\", \"OG image\", publish prep) | `references/app-cover.md` — branded 3:2 cover + capsule OG mask |\n| SEO: meta tags, OG/Twitter cards, robots/sitemap, JSON-LD, entity, GEO, audit | `references/seo.md` |\n| Security: Worker hardening, OWASP audit, threat modeling | `references/security.md` |\n\n## Stack\n\n- **TanStack Start** (file-based routing under `app/src/routes/`, SSR via\n `app/src/server.ts` → a Worker `export default { fetch }`). No Next/Remix/Astro\n conventions, no `app/src/pages`.\n- **Vite 7 + bun**. Build emits `dist/server/server.js` (the Worker) +\n `dist/client` (hashed static assets). Tailwind v4 is wired in `app/src/styles.css`\n (it also imports Quanta's Tailwind entry for the template\n bundle — leave that wiring alone even though websites use neither). Legacy\n shadcn/ui files may exist from the scaffold. Websites use custom\n Tailwind/CSS only — never import `@higgsfield/quanta/*`.\n- **No separate Hono/Express backend.** Server logic is TanStack **server\n functions** (`createServerFn`) and **server routes**. App-local API routes are\n allowed when a platform contract requires them (for example a webhook\n receiver or a JSON endpoint the site's own client fetches).\n\n\n## Hard rules\n\n### 0a. Vendored packages and template modules\n\nThe `app/packages/` directory contains managed snapshots vendored from the\nupstream Higgsfield web app (`@higgsfield/fnf`, `@higgsfield/fnf-react`,\n`@higgsfield/quanta`). Websites never use them, but do NOT edit or delete\nthem. Template-owned infrastructure lives in `app/src/module/**`.\n\n### 0b. Supercomputer Design mode inspector\n\nGenerated websites support a Higgsfield design inspector bridge for editing in\nSupercomputer Design mode. The split is strict:\n\n- The Higgsfield editor (parent window) owns the iframe UI, hover overlay,\n edit popover, origin/session checks, and edit prompt submission.\n- This template owns the child iframe runtime through\n `app/src/module/design-inspector`.\n- Agents never manually implement inspector code, refs, source markers, or\n `data-hf-*` attributes.\n\nLocal scripts (LOCAL work only — the deploy build is owned by CI):\n\n- `bun run build` is inspector-free by default: no inspector runtime and no\n source metadata. Setting `HF_DESIGN_INSPECTOR=1` in the env turns the same\n build into the inspector-enabled one (this is what platform CI does on\n every deploy).\n- `bun run dev:design` is local dev with the inspector enabled.\n\nThe platform CI sets `HF_DESIGN_INSPECTOR=1` on every deploy build, so the live\ndeployed site always carries the inspector and IS the surface Supercomputer\nDesign mode opens. Exact source metadata is attached with inspector-only\ncallback refs and a `WeakMap`, not DOM attributes. Keep the guarded\ndynamic install in `app/src/routes/__root.tsx` and the Vite integration in\n`app/vite.config.ts` wired to `app/src/module/design-inspector/vite`.\n\nFor every Supercomputer website-builder task there is ONE deploy —\n`higgsfield website deploy \u003cwebsite_id>` — and it ships the live public site\nimmediately; there is no preview stage or environment choice. Publishing/listing on\nthe community feed is separate: do NOT run `higgsfield website publish` unless\nthe user explicitly asks to publish, list, or share the site. Never hard-code\n`HF_DESIGN_INSPECTOR=1` into the `build` script and never hand-edit the build\nscript to toggle it — the deploy build is CI-owned.\n### 1. SSR-safe rendering\nEvery route renders on the server per request. NEVER touch browser-only globals\n(`window`, `document`, `localStorage`, `navigator`) at module top level or during\nrender — only inside `useEffect`/event handlers, or guarded with\n`typeof window !== \"undefined\"`. A top-level `window` reference crashes SSR.\n\n### 2. Server-only code stays server-only\nPut server logic in `createServerFn(...).handler(...)` or a `*.server.ts` module\n(the `.server.ts` suffix keeps it out of the client bundle). Secrets and\nbindings are read **server-side, per request** — never shipped to the browser.\n\n### 3. No Higgsfield integration — but a REAL backend of the site's own\nA `type: \"website\"` product never calls `https://fnf.internal/*`, never shows\n\"Sign in with Higgsfield\", and never imports the fnf SDK. It still gets a real\nbackend wherever the product needs one: server functions (`createServerFn`),\napp-local API routes, sessions, business logic, and real persistence (D1) —\nnever in-memory arrays, `localStorage`-as-database, or fixture data. In-app\nauth for the site's OWN users (accounts, teams, dashboards) is built with the\nwebsite's own routes/storage. If the request needs generation or Higgsfield\naccounts, it is a `type: \"app\"` — switch to `references/app-flow.md`.\n\n### 4. Cloudflare bindings via `cloudflare:workers`\nAny infra you opt into (D1 `DB`, R2 `STORAGE`, KV `KV`) is read server-side\nthrough `app/src/lib/bindings.server.ts` (`import { env } from \"cloudflare:workers\"`).\nEach binding is present ONLY if declared in `app/app.manifest.json`, so the typed\naccessors are optional — guard before use. Do not thread `env` through React\nprops or read it at module top level.\n\n### 5. Opted-in storage is LIVE — one deploy, one database\nIf you opt into D1, R2, or KV, each is a SINGLE instance backing the ONE live\ndeploy. There is no staging copy: every migration and data change hits **live\nproduction data** directly.\n- `env.HF_ENV` is always `\"production\"` on deployed builds; there is no\n separate database/bucket to test against.\n- A destructive migration you run \"just to test\" destroys **production data**.\n Prefer additive migrations (`CREATE TABLE IF NOT EXISTS`, `ADD COLUMN`), and\n get explicit user approval before any destructive change.\n\n### 6. `app/app.manifest.json` declares infra — NOTHING is provisioned by default\nA new website gets **no D1, no R2, no KV, no Durable Object**. Opt in only when\nthe website actually needs it:\n- `\"db\": true` → a D1 database, bound `env.DB`\n- `\"r2\": true` → an R2 bucket, bound `env.STORAGE`\n- `\"kv\": true` → a KV namespace, bound `env.KV`\n- `\"durableObject\": \"ClassName\"` → a Durable Object, bound `env.ROOMS`\n- `\"container\": true` (or `{ \"instanceType\", \"port\", \"sleepAfter\" }`) → a Docker\n container for heavy/long-running work, bound `env.CONTAINER` — see\n `references/containers.md`\n\nCounts are capped (≤1 each) by the platform, which PROVISIONS the resource and\nbinds it at deploy. The committed `app/wrangler.jsonc` is build/dev input only;\nthe platform OVERWRITES its `name` + bindings at deploy — declare infra in\n`app/app.manifest.json`.\n\n**KV is eventually consistent** (NOT Redis): config, feature flags, cached reads\n— NOT counters, locks, or read-after-write. Use a Durable Object for strong\nconsistency.\n\nFor a **Durable Object** you must ALSO `export class ClassName extends\nDurableObject {…}` from `app/src/server.ts` (alongside the default `{ fetch }`\nexport).\n\nFor a **container** — heavy or long-running work a Worker can't do: set\n`\"container\"` in the manifest and follow **`references/containers.md`** (exact\nDockerfile, the platform-fixed `AppContainer` class, keep-alive + 3-hour-deadline\npattern, fnf via container token). Containers are **off by default**.\n\n## Editing map\n- Pages / routing → `app/src/routes/**` (file-based; `__root.tsx` is the shell).\n- Server logic → `createServerFn` (see `app/src/lib/api/example.functions.ts`) or\n `*.server.ts`.\n- Bindings access → `app/src/lib/bindings.server.ts`.\n- Infra declaration → `app/app.manifest.json`; `app/wrangler.jsonc` = build/dev input.\n- Durable Object class → exported from `app/src/server.ts`.\n- Components → custom components per the boards; app-local files in\n `app/src/components/**`. Do not start from `app/src/components/ui/*` unless\n migrating a legacy shadcn piece.\n- Styles / theme → `app/src/styles.css` wires Tailwind v4. Websites: a custom\n token layer from the design brief — no q-prefixed utilities, no site-wide\n CTA utility classes.\n- D1 schema → `app/migrations/000N_*.sql` (additive; see rule 5).\n- Pipeline artifacts → `app/design-brief.md` (Phase 0) + `refs/*.png` (Phase 1);\n commit both.\n\n## Verify + deploy\n\nThe trusted platform CI builds the website on **every deploy** (always with\n`HF_DESIGN_INSPECTOR=1` and `HF_ENV=\"production\"` — the live site carries the\ndesign inspector), so a deploy already gives you the authoritative type +\nbuild result. Do NOT reflexively `bun install` +\n`bun run build` just to check your work. The sandbox cannot deploy/migrate (no\nCloudflare token); the trusted platform CI does that.\n\n**Default: run the pipeline, pass the Phase 5 gate, deploy**\n(`higgsfield website deploy \u003cwebsite_id>` — this ships the live site\nimmediately). Never publish/list on the community feed unless the user\nexplicitly asked to publish.\n\n**Publishing (\"show in feed\").** When the user asks to publish / share / put the\nsite on the feed, run `higgsfield website publish \u003cwebsite_id>` — it lists the\nsite on the Higgsfield community feed. **Publishing no longer deploys**: it\nlists whatever is already live, so run `higgsfield website deploy \u003cwebsite_id>`\nFIRST — and after ANY later change, deploy again to ship it (re-publishing does\nnot re-deploy and won't pick up un-deployed changes).\n\n**HARD GATE — the cover is NOT optional. Running `higgsfield website publish` while\n`og_image_url` or `marketplace_cover_url` is empty is a BROKEN publish** (the\nfeed card renders ONLY from `app/src/app-meta.json`; an empty `og_title` makes\nthe listing INVISIBLE, an empty cover makes it a blank card). The publish\nsequence is: (a) READ `app/src/app-meta.json`; (b) if `og_image_url` or\n`marketplace_cover_url` is empty → STOP, read `references/app-cover.md` and\ngenerate + upload the cover NOW — do not skip this because the user \"only asked\nto publish\", the cover IS part of publishing; (c) fill ALL fields below with\nreal values (never placeholders); (d) commit + push; (e) run\n`higgsfield website deploy \u003cwebsite_id>` to ship the pushed changes (publish no\nlonger deploys — it lists what's already live); (f) only then run\n`higgsfield website publish`:\n\n1. `og_title` — the card's title (also the browser tab title).\n2. `og_description` — the card's one-liner.\n3. `og_image_url` — REQUIRED: the cover image, generated per\n `references/app-cover.md` (the branded 3:2 cover + stadium-capsule OG\n mask) if none exists yet; upload the OG file with `higgsfield upload create`\n and set the returned durable URL.\n4. `marketplace_cover_url` — REQUIRED: the plain (unmasked) cover, the same\n generation's `\u003cname>_cover.png` from `references/app-cover.md`, uploaded\n with `higgsfield upload create`. One generation fills both this and\n `og_image_url` — there is never a reason to have one without the other.\n5. `favicon_url` — the card's logo/icon (generate one if none exists yet).\n6. `og_video_url` — the **cover video**, OPTIONAL and permission-gated: OFFER\n it to the user (\"want a short cover video for the feed card?\") and ASK\n PERMISSION FIRST — generating a video costs credits; never generate it\n unprompted. If they say yes, follow \"Cover video\" in `references/seo.md`.\n\n(1–5 are generated without asking — they are part of the publish, not a\nseparate credit decision; only the cover VIDEO (6) needs permission.)\n\n`higgsfield website deploy \u003cwebsite_id>` remains the way to ship the live site\nWITHOUT a feed listing.\n\n**Run the local checks only when you actually need them** — from `app/`:\n```bash\ncd app\nbun install # only when you changed dependencies / package.json\nbun run typecheck # tsc --noEmit — only to chase a type error on deploy\nbun run build # local build — only to chase a build error on deploy\n```\nRun them when: you changed dependencies or build/runtime config, you're debugging\na build/type error, or a command genuinely needs `node_modules`.\n\n**Small edits to an existing site** (copy tweak, one component, styling fix): the\npipeline does not restart. Make the edit, deploy.\n\n**Before claiming a build done / deploying, no placeholders may remain** — no\n`\u003c...>`-style tokens, `lorem ipsum`, or scaffold blank-page markers\n(`REMOVE_THIS` / `blank-app-v1`). This is covered by the mechanical gate (the\ngrep checklist in `references/review-rubric.md` §A).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":30856,"content_sha256":"0158bf018aee2113f364d06ab9c8d25cbabe84141ba56815f7ab159fb7c57766"},{"filename":"references/wow-catalog.md","content":"# wow-catalog — Tier-1 experience techniques (pick in Phase 0, build in Phase 4)\n\nThe Tier-1 mechanic is the thing a visitor remembers. One per page, chosen in\nthe design brief, fully executed — and chosen from THIS catalog, not from the\nfirst idea that comes to mind. Every technique here leans on Higgsfield\ngeneration: the wow is bespoke media responding to input, not a CSS trick.\n\n**The default is the animated website (A4).** Every `--type website` build is\nan **animated website** — the seam-locked scroll-scrub camera journey (A4) — and\nyou do NOT open this menu unless the user EXPLICITLY asked for a different\ntreatment. Skip straight to `references/scroll-scrub.md`. The rules below apply\nonly on that explicit-alternative path.\n\n**Selection rules (mechanical):**\n\n1. Pick the technique that pays off the CONCEPT SPINE — the mechanic should\n enact the site's idea (a \"calibration\" spine wants focus/precision physics;\n an \"ascent\" spine wants climbing/parallax depth; an \"archive\" spine wants\n leafing/stacking). Write one sentence in the brief defending the pairing.\n2. **Never reuse the previous build's technique** (see the anti-convergence\n ledger below). If the spine genuinely demands a repeat, change its\n expression (different subject, different axis, different payoff).\n3. Cinema tier: one Tier-1 from the catalog + motivated reveals. Spectacle:\n one Tier-1 PLUS a second beat mid-page (different technique family) +\n custom cursor.\n4. Every technique must be screenshot-safe (initial state fully rendered) and\n reduced-motion-safe (static composed fallback).\n\n## The catalog\n\n### A. Film scrub family (scroll plays generated video)\n\n- **A1 — Single-shot hero scrub.** One ~5s seedance clip from the approved\n hero still (push-in, rack focus, subject turn, light sweep; start ≠ end).\n ffmpeg → ~100 frames → canvas bound to pin progress. The proven baseline.\n- **A2 — Long-form chaptered scrub.** The upgrade: 2-4 clips (same grade,\n different beats — e.g. wide establishing → detail macro → reveal) played\n across a LONG pin (300-500vh). Between chapters, pinned text cards, layered\n cutouts, or metric readouts hand off. Many moving parts choreographed on one\n timeline: frames scrub, headline stages swap per chapter, a progress rail /\n readout tracks the journey. This is the awwwards centerpiece — budget it\n only when the page has few other heavy beats.\n- **A3 — Product turntable.** Multi-angle generated shots of the SAME product\n (reference-driven so identity holds) or a short orbit clip → frame scrub =\n the user rotates the product by scrolling. For physical-product briefs.\n- **A4 — Seam-locked scroll scrub (the \"animated website\" — DEFAULT for every\n website).** A full-site camera journey through 4–7 generated scenes. Chain\n each leg from the previous rendered leg's ACTUAL boundary frame (grounded\n continuous-forward architecture), or join miniature/isometric dives with\n start/end-frame-locked aerial connectors. Scrub optimized MP4 segments\n directly while semantic chapter copy remains in normal document flow. This is\n what \"animated website\" means and it is the default experience — reach for\n another technique here ONLY when the user explicitly asked for one. This is a\n specialized pipeline: read and follow **`references/scroll-scrub.md`** before\n boards/assets/code.\n\n### B. Layered depth family (one image becomes a 3D-feeling scene)\n\n- **B1 — Cutout parallax rig.** Take the hero image, cut out the subject\n (`image_background_remover`), generate/outpaint a clean background plate\n behind it (+ optionally\n a mid layer: fog, foliage, particles as transparent PNGs). Stack 3-5 layers\n moving at different rates on scroll AND subtly on cursor. The hero feels\n volumetric. Cheap, robust, dramatic.\n- **B2 — Grade-shift interaction pair.** Two renders of the SAME composition\n (image-edit re-grade: dark/dormant vs. lit/alive). Crossfade by cursor\n spotlight (mask follows pointer) or by scroll. The \"the site notices you\"\n effect.\n- **B3 — 3D subject scene.** Approved hero image → `multi_image_to_3d` GLB →\n R3F scene with scroll-driven camera orbit + cursor tilt. Spectacle tier.\n\n### C. Canvas/pixel family (the image itself is alive)\n\n- **C1 — Displacement/liquid hover.** Hero image on a WebGL plane; cursor\n ripples/distorts it (three.js displacement or OGL). Editorial-compatible\n when subtle.\n- **C2 — Particle dissolve.** Hero image sampled into canvas particles that\n assemble on load and scatter/reform with scroll or cursor. Good for\n data/tech/AI spines.\n- **C3 — Scroll-driven mask reveal.** The page opens inside giant display\n type or a shape; scrolling expands the mask until the media goes full-bleed\n (clip-path or canvas). Strong opener for editorial+cinema hybrids.\n\n### D. Spatial layout family (the page itself moves unusually)\n\n- **D1 — Horizontal cinema rail.** A pinned section pans horizontally through\n a WIDE generated panorama (outpaint the hero sideways) or a sequence of\n scene plates; content cards ride the rail. The scroll axis rotation itself\n is the surprise.\n- **D2 — Sticky-stack chapters.** Full-bleed chapters stack/peel over each\n other, each carrying its own plate/loop; type pinned per chapter. Reliable,\n editorial-friendly.\n- **D3 — Kinetic type opener.** Massive display type choreographed on scroll\n (per-char stagger, weight/width axis animation on a variable font, lines\n sliding on different tracks) over a generated plate. When the brand voice\n IS typography.\n\n**Banned as Tier-1:** autoplay ambient loop alone (passive), generic fade-in\nreveals, particles-behind-text with no interaction, marquee strips, tilt-on-\nhover cards. These may exist as seasoning, never as the answer to \"what's the\nwow.\"\n\n## Implementation contracts (all families)\n\n- Initial paint is complete: frame 1 / layer stack / unmasked state renders\n before any JS-driven interaction (headless full-page shot shows a finished\n hero).\n- Input → response latency feels physical: scrub via ScrollTrigger progress\n (scrub: 0.5-1), cursor via `useMotionValue` + springs. Never `useState` per\n frame, never animating layout properties.\n- Reduced motion: the composed final state, static, no pin.\n- Mobile: the technique must degrade deliberately (shorter pin, cursor\n effects replaced by scroll equivalents, turntable becomes a swipeable\n sequence) — declare the degradation in the brief.\n\n## Anti-convergence ledger (mechanical, checked in the gate)\n\nThe skill must not produce siblings. Before Phase 0 locks, list what the\nPREVIOUS build in this chat used (if any) for the six identity axes — then\ndiffer on AT LEAST four:\n\n1. **Palette family** (already a hard ban on repeat)\n2. **Type pairing** (no repeat of the exact display face two builds running)\n3. **Hero architecture** (image-as-canvas / split / masked / rail…)\n4. **Tier-1 technique** (this catalog — no repeat, per selection rule 2).\n EXCEPTION: the animated website (A4) is the default and repeats build-to-\n build by design — it is exempt from this no-repeat axis. When two consecutive\n builds are both animated websites, differ instead on the world/subject,\n journey shape, and the remaining axes.\n5. **CTA garment set** (see garment catalog in `image-to-code.md` — zero\n garment overlap with the previous build)\n6. **Corner/border language** (sharp vs. soft vs. pill vs. hairline-ruled)\n\nFirst build in a chat: derive all six from the brief's material world (the\nnouns the business actually touches — steel, paper, steam, moss, vinyl) and\nsay so in the brief. When there is no previous build to differ from, the enemy\nis the model's own statistical default — if an axis choice would look at home\nin a generic template, re-roll that choice.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7826,"content_sha256":"8f74e5723cecdbde09fcd90b65813ef96a2d5d00337abeb798b2e88d85150e05"},{"filename":"references/wow-maker.md","content":"# wow-maker — everything you need to make a website wow\n\nThis is THE design source for every site you build. There are no fixed templates to\npick and fill — you **compose** a distinctive, award-tier site from the directory\nbelow: bespoke AI-generated assets, signature animation patterns, motion/3D\nlibraries, and copy-paste component/block registries.\n\n**The mandatory wow on a website is the ANIMATED WEBSITE.** On every\n`--type website` build the Tier-1 wow is fixed by the brief's `Animation mode`\n(`references/website-flow.md` Phase 0): the DEFAULT is the **animated website** —\nthe seam-locked scroll-scrub camera journey in `references/scroll-scrub.md` — and\nthat is what \"ships a real wow moment\" MEANS here. This file is the ingredient\ndirectory you build the SURROUNDING craft with (bespoke assets, section motion,\ncomponent registries); it is NOT a menu of alternatives to the animated website.\nUse `design-recipe.md` for HOW to execute with taste; this file is WHAT to build\nthe supporting layer with.\n\n## What counts as \"done\" (every site)\n\nWow is part of DONE, not optional polish. The bar is the `Animation mode` the\nbrief committed to — nothing lighter:\n\n1. **The Tier-1 wow matches `Animation mode`.** Default `animated-website` → the\n scroll-scrub camera journey is built (component + scene media), per\n `references/scroll-scrub.md` and enforced by the Phase 5 gate item 9f. The\n items below are the craft AROUND it, not a substitute: a ticker, hover motion,\n entrance transitions, animated numbers, a shader gradient, or any single §2\n effect DO NOT satisfy the animated-website requirement. Only when the brief\n records `Animation mode: non-animated` (the user picked Non-animated at intake)\n does a §2 signature effect / lighter technique become the Tier-1 wow.\n2. **≥1 bespoke generated asset**, downloaded into `app/public/` and actually\n referenced — never stock, picsum, an icon-font hero, or a CSS-only hero. (§1)\n3. **Motivated entrance motion** (scroll reveals / spring transitions via\n `motion/react`, GSAP, or a registry reveal), `prefers-reduced-motion`-gated. Not a\n dead static page.\n4. **Reach into the component/block directory (§5) instead of hand-rolling** generic\n sections — Tailark for full marketing sections; Magic UI / Cult UI / SmoothUI /\n motion-primitives for effects. Hand-built-from-scratch is slower and more generic.\n5. **Where they fit:** an animated headline (`split-type` + GSAP, Magic UI\n `morphing-text`/`aurora-text`) and animated numbers (`@number-flow/react`) instead\n of static text.\n\n**Order of work — the camera journey FIRST.** On the animated-website default,\ndecide the journey and generate the scene media BEFORE you build the page, so the\nscroll-scrub spine drives the design. Do not ship a generic static structure and\nplan to \"add the animation later\" — you won't, and the gate will fail it.\n\n**Restraint is NOT an escape hatch from the animated website.** \"Clean\",\n\"minimal\", \"trustworthy\", \"Linear-like\", \"Notion-like\" briefs are STILL\n`Animation mode: animated-website` — restraint means the journey is *calm and\ndeliberate* (slower camera, quieter scenes), not absent. A minimal or serious\nbrief does NOT make it `non-animated`; only the user's own Non-animated choice\ndoes. Never use \"it's minimal\" as cover to ship a plain page or downgrade\nthe animated website to a soft effect.\n\n## Rules (short)\n\n- **License (the only hard rule for us):** use ONLY what is listed here — every entry\n is free + permissively licensed (MIT / Apache-2.0 / BSD / Zlib / Unlicense). Never\n add a proprietary/paid source (Unicorn Studio, Cult Pro, React Bits, Spline, Rive,\n Origin UI, hover.dev, Aceternity Pro, Motion+). What the end user adds to their own\n site later is their responsibility.\n- **SSR tags (Cloudflare Worker):** `[S]` static = server-safe anywhere. `[C]`\n client-only = render under a mounted gate / client boundary, no `window` at module\n top. `[W]` webgl/canvas = `[C]` **plus** `React.lazy` + code-split so it never\n enters the SSR render path. See the SSR pattern at the bottom.\n- **Motion budget:** one signature/hero effect per page (don't stack two). Motion must\n be motivated (`design-recipe.md` §6). Honor `prefers-reduced-motion`:\n every animated effect needs a static fallback.\n\n---\n\n## 1. Generate bespoke AI assets — this is our biggest edge\n\nA site with real, generated art looks dramatically cooler than one built from stock,\nicon-fonts, CSS gradients, or empty placeholders — and generating it is **our\nproduct's superpower**. Treat bespoke asset generation as a **default step on every\nbuild**, not an afterthought. A hero with a generated image / video / 3D subject is\noften the single biggest wow upgrade available.\n\n**Commands** (`higgsfield generate create \u003cjob_type> --prompt \"…\" [flags]`).\nJobs are async — **submit (no `--wait` when batching; each prints a job id) →\npoll `higgsfield generate wait \u003cid>` / `higgsfield generate get \u003cid>` → use the\nresult** (a single job can pass `--wait` to block and print the result URL). If\na param/model is rejected, run `higgsfield model list` +\n`higgsfield model get \u003cjob_type>` and use what they report.\n\n- Images — `gpt_image_2` (general / art-directed), `nano_banana_pro`\n (photoreal + reference-image driven, e.g. try-ons / product normalization).\n- Video — `seedance_2_0` (seamless loops + short films).\n- `image_background_remover` — turn a product/subject shot into a transparent\n PNG cutout.\n- `multi_image_to_3d` — turn 1-4 approved images into a textured `.glb`\n (repeated `--image`, `--should_texture true`; no rigging).\n\n**Pipeline:** generate → poll → **download into `app/public/`** (e.g. `assets/`,\n`media/`, `frames/`) → reference **same-origin** (`/assets/...`). The Worker serves\n`public/` at the root. Never ship `\u003cimg src=\"\">` / `\u003cvideo src=\"\">` blanks or\nstock/picsum as the final asset.\n\n**What to generate, and where it lands:**\n- **Hero image / background** — the centerpiece visual (full-bleed or behind glass).\n- **Section textures / atmospheric plates** — backdrops that crossfade per section.\n- **Video loop / showreel** — a seamless `seedance_2_0` clip for a hero or band.\n- **Product cutouts** — generate the image → `image_background_remover` →\n transparent PNG.\n- **3D subject** — approved image → `multi_image_to_3d` → `.glb` for an R3F scene.\n- **OG image + favicon** — generate, wire into `\u003chead>` / `app-meta`.\n- **People / avatars / testimonial faces, icon & logo glyphs** — bespoke, not stock.\n\n**Rules:** prompt for \"no text, no logos, no watermark\" (IP-safe + lets you set type\nin HTML); match the palette/mood to the brief; downscale large outputs (hero ≤2k,\ncutouts ~800px); **verify an image before the expensive `multi_image_to_3d`\nstep**; for a monochrome look force grayscale in the prompt AND on export.\n\n---\n\n## 2. Signature effects you can build (patterns)\n\nHigh-impact, award-tier mechanics — build any of these from scratch with the libs\nnamed, adapt them to the brief, combine, or invent your own. These are ideas, not\nmandates. Pair every one with generated assets (§1) and the SSR pattern (bottom).\n\n- **Particle-morph hero** `[W]` — ~4-6k Three.js instanced points that morph through\n the scroll (sphere → the product's silhouette → disperse), recolored/relit per\n scroll stage. *Build:* `three` instanced points + a scroll-driven lerp between\n precomputed target positions. *Fits:* premium product/brand (perfume, spirits,\n jewelry, tech). Tune shape/finish/`envColor` to the niche so it never reads generic.\n- **Fluid + smooth-scroll studio** `[W]` — a fixed WebGL fluid/gradient backdrop with\n a rotating point cloud, weighty Lenis scroll, GSAP ScrollTrigger reveals, and a\n **staggered 2-column video grid** (not a carousel). *Build:* `three` + `gsap` +\n `lenis` (or `@shadergradient/react` for the backdrop). *Fits:* studios, agencies.\n- **Camera-dolly product switcher** `[W]` — the active product dollies toward camera\n while the next approaches from depth and the bg color lerps; drag/wheel/tabs +\n spring snap + autoplay. *Build:* `@react-three/fiber` + `gsap`, scenes as data.\n *Fits:* multi-variant product / flavor showcases.\n- **Hold-to-spin 3D showroom** `[W]` — a clay/toy-look turntable: a 3D subject on a\n stepped platform, hold to spin with inertia, click hotspots for detail. *Build:*\n `three` + `@react-three/fiber` + `@react-three/drei` (+ `image_to_3d` assets).\n *Fits:* apparel, physical products, collectibles.\n- **Cursor X-ray reveal** `[C]` — a full-screen photo with an aligned \"X-ray\"\n substrate beneath; a soft feathered lens follows the cursor and dissolves the cover\n into the truth underneath. *Build:* pure React + a CSS `mask-image` at the cursor,\n no 3D/GSAP. *Fits:* editorial deep-dives, \"look inside\" product stories. (Strictly\n B&W + one breathing accent reads most premium.)\n- **Scroll-scrub film** `[C]` — an AI-generated ~15s film extracted to frames and\n drawn on a `\u003ccanvas>`, scrubbed forward/back by scroll, caption cards fading in.\n *Build:* generate the film (§1) → ffmpeg frames into `public/frames/` → canvas +\n rAF scrub. *Fits:* high-impact brand reveals / launches (dark, cinematic).\n- **Frosted-card scrubber** `[C]` — the warm, light variant: a lifestyle film scrubs\n behind translucent frosted serif cards, then resolves into content panels. *Fits:*\n boutiques, cafés, florists, ateliers.\n- **Magnetic gallery wall** `[C]` — an infinite draggable wall of tiles with magnetic\n hover, 3D tilt, a burst intro, and click-to-zoom into a circular gallery. *Build:*\n one shared pointer-physics handler + inertia + modulo wrap. *Fits:* portfolios,\n galleries, visual merch.\n- **Kinetic portfolio** `[C]` — the full motion suite: Lenis smooth scroll, GSAP\n reveals + scroll-velocity skew, magnetic elements, custom cursor + hero spotlight,\n one marquee, a Swiper vinyl carousel, live clock. *Build:* `gsap` + `lenis` +\n `swiper`. *Fits:* personal sites, résumés, creative showcases.\n\n---\n\n## 3. Quick \"I need X\" index\n\n- **Animated hero backdrop** → `@shadergradient/react`, `three`+R3F, Magic UI\n `light-rays`/`warp-background`/`retro-grid`, Cult UI `hero-*`, Kokonut\n `beams-background`, `@tsparticles/slim`, `cobe` (globe) — or generate a hero image/video (§1).\n- **Animated headline / text** → Magic UI `aurora-text`/`morphing-text`/`hyper-text`,\n motion-primitives `text-*`, Cult UI `text-animate`, chanhdai `apple-hello-effect`,\n Eldora `*text`, or `split-type` + GSAP.\n- **Scroll reveal** → motion-primitives `in-view`/`animated-group`, Magic UI\n `blur-fade`/`text-reveal`, `motion/react` `useInView`, GSAP ScrollTrigger.\n- **Marquee / logo strip** → Magic UI `marquee`/`scroll-based-velocity`,\n motion-primitives `infinite-slider`, Animata/Eldora `marquee`.\n- **Card with flair** → Magic UI `magic-card`/`neon-gradient-card`, motion-primitives\n `tilt`/`border-trail`, Cult UI `shift-card`, SmoothUI `glow-hover-card`.\n- **Nav / dock / command** → shadcn `navigation-menu`/`command`, Cult UI\n `dock`/`floating-panel`, motion-primitives `dock`.\n- **Animated numbers** → `@number-flow/react`, motion-primitives `sliding-number`,\n Magic UI `number-ticker`.\n- **Custom cursor / magnetic** → motion-primitives `cursor`/`magnetic`, Magic UI\n `smooth-cursor`, SmoothUI `magnetic-button`.\n- **Full marketing section** (hero/features/pricing/testimonials/CTA/footer) → **Tailark**.\n- **App shell / dashboard / auth / table / chart** → **shadcn blocks**.\n- **Confetti / celebration** → `@tsparticles/confetti`, Magic UI `confetti`.\n- **3D scene / particle field** → `three`+R3F+`drei` (+`@react-three/postprocessing`, `maath`).\n\n---\n\n## 4. Framework toolkit (npm deps — add to `app/package.json` when used)\n\n| Library (npm) | License | Wow it adds | SSR |\n|---|---|---|---|\n| `motion` (`motion/react`) | MIT | enter/exit (`AnimatePresence`), layout/FLIP, springs, `useScroll`/`useInView`, gestures | `[C]` |\n| `gsap` + `@gsap/react` | GSAP Standard License (free, all plugins) | ScrollTrigger pinned/scrubbed sequences, timelines, `SplitText`; use `useGSAP()` | `[C]` |\n| `lenis` (`lenis/react`) | MIT | weighted buttery smooth scroll; `\u003cReactLenis root>` | `[C]` |\n| `three` + `@react-three/fiber` + `@react-three/drei` | MIT | real-time 3D, GLTF/PBR, glass, particle fields, camera flythroughs | `[W]` |\n| `@react-three/postprocessing` + `postprocessing` | Zlib/MIT | bloom, DOF, glitch, chromatic aberration on the R3F stack | `[W]` |\n| `@shadergradient/react` | MIT | premium animated 3D gradient hero/section backdrop (peers: `three`, `@react-three/fiber`, `three-stdlib`, `camera-controls`) | `[W]` |\n| `@tsparticles/react` + `@tsparticles/slim` / `@tsparticles/confetti` | MIT | particle fields, interactive backgrounds, confetti (vanilla canvas) | `[C]` |\n| `cobe` | MIT | the 5KB rotating WebGL globe | `[W]` |\n| `ogl` | Unlicense | bespoke custom-shader hero without three.js bulk | `[W]` |\n| `maath` | MIT | math/easing helpers for R3F (point spheres, `easing.damp`) | `[S]` |\n| `animejs` (v4) | MIT | SVG line-draw/morph, staggered reveals, timelines | `[C]` |\n| `@number-flow/react` | MIT | animated odometer number/price transitions | `[C]` |\n| `split-type` | MIT | split text into lines/words/chars (pair with GSAP/Motion) | `[C]` |\n| `swiper` | MIT | touch sliders / coverflow carousels | `[C]` |\n\ndrei caveat: use `\u003cEnvironment files={…}>` with a self-hosted CC0 HDRI in `public/`,\nnot `preset=` (it fetches a CDN at runtime). Keep all `three`/shader code in a\n`React.lazy` + mounted client boundary.\n\n---\n\n## 5. Component & block directory (copy-paste; the source lands in the repo)\n\n`shadcn add` drops the component SOURCE into `app/src/components/` — the user owns it.\nNamespaced registries must be registered in `app/components.json` first (block below);\nURL-based ones install directly.\n\n### `app/components.json` → `registries`\n```jsonc\n\"registries\": {\n \"@magicui\": \"https://magicui.design/r/{name}.json\",\n \"@cult-ui\": \"https://www.cult-ui.com/r/{name}.json\",\n \"@smoothui\": \"https://smoothui.dev/r/{name}.json\",\n \"@ncdai\": \"https://chanhdai.com/r/{name}.json\",\n \"@motion-primitives\": \"https://motion-primitives.com/c/{name}.json\",\n \"@kokonutui\": \"https://kokonutui.com/r/{name}.json\",\n \"@tailark\": \"https://tailark.com/r/{name}.json\",\n \"@eldoraui\": \"https://eldoraui.site/r/{name}.json\"\n}\n```\n(`@shadcn` is built in. Verify a URL at wire-time if an `add` 404s.)\n\n### shadcn/ui — base layer + app blocks · `npx shadcn@latest add \u003cname>`\nPrimitives (mostly `[S]`/`[C]`): `button` `card` `input` `select` `dialog` `drawer` `sheet` `sonner` `tabs` `accordion` `breadcrumb` `navigation-menu` `command` (⌘K) `table` `skeleton` `spinner`. Rich `[C]`: `carousel` (embla), `data-table` (TanStack Table), `chart` (recharts), `date-picker`. Full blocks: `sidebar-07/03/08/15/16`, `dashboard-01`, `login-01/03/04`, `calendar-01…32`, `products-01`.\n\n### Magic UI — effects/backgrounds/text · `npx shadcn@latest add \"https://magicui.design/r/\u003cname>.json\"`\nBackgrounds: `light-rays` `[W]`, `warp-background` `[C]`, `retro-grid` `[S]`, `ripple` `[S]`, `meteors` `[S]`, `particles` `[C]`, `flickering-grid` `[C]`, `dot-pattern`/`grid-pattern` `[S]`, `border-beam` `[S]`. Cards: `magic-card` `[C]`, `neon-gradient-card` `[S]`, `bento-grid` `[S]`, `shine-border` `[S]`. Buttons: `shimmer-button`/`rainbow-button`/`interactive-hover-button`/`pulsating-button` `[S]`. Text: `aurora-text`/`animated-shiny-text` `[S]`, `sparkles-text`/`morphing-text`/`hyper-text`/`number-ticker` `[C]`. Scroll/marquee: `text-reveal`/`blur-fade` `[C]`, `marquee` `[S]`, `scroll-based-velocity` `[C]`. Misc: `globe` `[W]`, `orbiting-circles` `[S]`, `animated-beam` `[C]`, `confetti` `[C]`, `dock` `[C]`, `terminal` `[C]`, `safari`/`iphone` mockups `[S]`, `smooth-cursor` `[C]`.\n\n### Cult UI (free) · `npx shadcn@latest add https://www.cult-ui.com/r/\u003cname>.json`\nHeroes: `hero-liquid-metal`/`hero-dithering`/`hero-heatmap` `[W]`, `hero-color-panel` `[C]`, `hero-static-radial-gradient` `[S]`. Backgrounds/shader: `shader-lens-blur`/`canvas-fractal-grid`/`distorted-glass`/`dither-image`/`morph-surface` `[W]`, `bg-animated-gradient`/`bg-media` `[C]`, `stripe-bg-guides` `[S]`. Text: `gradient-heading` `[S]`, `text-animate`/`typewriter`/`pixel-heading-word`/`text-gif` `[C]`. Buttons: `texture-button`/`metal-button`/`neumorph-button` `[S]`, `family-button`/`bg-animate-button`/`border-beam-button` `[C]`. Cards: `texture-card`/`minimal-card`/`cutout-card` `[S]`, `expandable-card`/`shift-card` `[C]`. Nav/misc `[C]`: `dock`/`floating-panel`/`side-panel`/`direction-aware-tabs`/`three-d-carousel`/`feature-carousel`/`logo-carousel`/`dynamic-island`/`sortable-list`/`color-picker`/`terminal-animation`/`animated-number`.\n\n### SmoothUI · `npx shadcn@latest add @smoothui/\u003cname>`\nCards: `apple-invites`/`app-download-stack`/`expandable-cards`/`glow-hover-card`/`scrollable-card-stack` `[C]`. Text: `reveal-text`/`scramble-hover`/`typewriter-text`/`wave-text`/`number-flow`/`price-flow`/`scroll-reveal-paragraph` `[C]`. Buttons: `magnetic-button`/`clip-corners-button`/`dot-morph-button`/`button-copy` `[C]`. Inputs: `power-off-slide`/`exposure-slider`/`scrubber`/`animated-file-upload` `[C]`. Misc: `siri-orb`/`dynamic-island`/`animated-tabs`/`gooey-popover`/`cursor-follow`/`infinite-slider`/`reviews-carousel`/`book`/`animated-avatar-group` `[C]`, `contribution-graph` `[S]`.\n\n### chanhdai (@ncdai) · `npx shadcn@latest add @ncdai/\u003cname>`\nText: `apple-hello-effect` (handwritten SVG signature)/`shimmering-text`/`fluid-gradient-text`/`text-flip` `[C]`, `spinning-circular-text` `[S]`. Hero/bg/cards `[C]`: `hero-01`/`dot-grid-spotlight`/`glow-card-grid`/`testimonial-spotlight`/`team-01`. Inputs `[C]`: `wheel-picker`/`elastic-slider`/`slide-to-unlock`. Marquee/nav: `testimonials-marquee` `[S]`, `logos-carousel`/`line-nav`/`share-menu`/`toc-minimap`/`scroll-fade-effect` `[C]`. Misc `[C]`: `theme-switcher`/`copy-button`/`spotlight-logo`/`icon-swap`/`github-contributions`.\n\n### motion-primitives · `npx shadcn@latest add \"https://motion-primitives.com/c/\u003cname>.json\"`\nText `[C]`: `text-effect`/`text-shimmer`/`text-morph`/`text-scramble`/`text-roll`/`text-loop`/`spinning-text`/`animated-number`/`sliding-number`. Scroll `[C]`: `in-view`/`animated-group`/`scroll-progress`. Surfaces: `glow-effect`/`spotlight`/`border-trail`/`tilt`/`animated-background` `[C]`, `progressive-blur` `[S]`. Interaction `[C]`: `magnetic`/`cursor`/`dock`/`infinite-slider`/`carousel`/`image-comparison`/`morphing-dialog`/`morphing-popover`/`transition-panel`/`disclosure`/`accordion`.\n\n### Kokonut UI (free) · `npx shadcn@latest add @kokonutui/\u003cname>`\nBackgrounds/hero `[C]`: `background-paths`/`beams-background`/`shape-hero`. Cards: `liquid-glass-card`/`tweet-card`/`bento-grid` `[S]`, `card-flip`/`card-stack`/`apple-activity-card`/`carousel-cards` `[C]`. Text: `shimmer-text`/`glitch-text` `[S]`, `type-writer`/`matrix-text`/`dynamic-text`/`scroll-text` `[C]`. Buttons: `gradient-button`/`social-button`/`switch-button` `[S]`, `particle-button`/`attract-button`/`hold-button` `[C]`. AI inputs `[C]`: `ai-prompt`/`ai-input-search`/`ai-voice`/`action-search-bar`/`file-upload`. Nav/misc `[C]`: `smooth-tab`/`toolbar`/`profile-dropdown`/`smooth-drawer`.\n\n### Tailark — full marketing blocks · `npx shadcn@latest add @tailark/\u003cname>` (themes: `dusk` dark, `mist` light) — nearly all `[S]`\nHeroes: `hero-section-1/4/7/9`, `mist-hero-section-1`. Features: `features-2/8/11`, `mist-features-3`. Pricing: `pricing-1/4`, `mist-pricing-1`. Testimonials: `testimonials-1/4`, `mist-testimonials-2`. CTA: `call-to-action-1/3`. Footer: `footer-1/4`, `mist-footer-2`. Also: `faqs-2` `[C]`, `stats-1`, `logo-cloud-1`, `integrations-1`, `team-1`, `comparator-1`, `content-1`, `contact-1`/`login-1`/`sign-up-1` `[C]`.\n\n### Animata · `npx shadcn add https://animata.design/r/\u003ccategory>/\u003cname>.json`\nBackgrounds: `animated-beam`/`blurry-blob`/`moving-gradient` `[S]`, `shooting-stars`/`interactive-grid` `[C]`, `boids-ecosystem` `[W]`. Buttons: `ai-button`/`shining-button`/`duolingo`/`slide-arrow-button` `[S]`, `ripple-button` `[C]`. Cards: `flip-card`/`animated-border-trail` `[S]`, `glowing-card`/`tilted-card`/`card-stack`/`expandable` `[C]`. Text: `animated-gradient-text`/`glitch-text` `[S]`, `typing-text`/`wave-reveal`/`counter`/`scroll-reveal` `[C]`. Misc: `marquee`/`spinner` `[S]`, `animated-dock`/`cursor-tracker`/`trailing-image`/`images-reveal`/`orbiting-items`/`split-reveal` `[C]`.\n\n### Eldora UI (@eldoraui) · `npx shadcn@latest add @eldoraui/\u003cname>`\nText `[C]`: `wavytext`/`blurintext`/`wordpullup`/`letterpullup`/`fadetext`/`gradualspacing`/`multidirectionslide`/`scaleletter`/`docktext`. Backgrounds: `novatrixbackground` `[W]`, `grid`/`hackerbackground`/`photonbeam` `[C]`. 3D/maps: `cobeglobe` `[W]`, `map`/`animatedframeworks` `[C]`. Sections/misc: `marquee` `[S]`, `animatedlist`/`testimonialslider`/`logotimeline`/`integrations`/`terminal` `[C]`, `cardfliphover`/`animatedbadge`/`animatedshinybutton`/`livebutton` `[S]`, device frames (`safaribrowser`/`macbookpro`/`iphone17pro`/`ipad`/`browser`) `[S]`.\n\n---\n\n## 6. SSR pattern (use for every `[C]` / `[W]` item)\n\n```tsx\n// app/src/components/client-only.tsx — render children only after mount\nimport { useEffect, useState, type ReactNode } from \"react\";\nexport function ClientOnly({ children, fallback = null }: { children: ReactNode; fallback?: ReactNode }) {\n const [m, setM] = useState(false);\n useEffect(() => setM(true), []);\n return m ? \u003c>{children}\u003c/> : \u003c>{fallback}\u003c/>;\n}\n```\n```tsx\n// [W] WebGL: lazy + ClientOnly so three/shaders never run during SSR\nconst Scene = lazy(() => import(\"./scene\")); // default export\n\u003cClientOnly fallback={\u003cdiv className=\"min-h-dvh bg-[var(--bg)]\" />}>\n \u003cSuspense fallback={null}>\u003cScene />\u003c/Suspense>\n\u003c/ClientOnly>\n```\nGate motion behind reduced-motion: `useReducedMotion()` (Motion) or\n`matchMedia(\"(prefers-reduced-motion: reduce)\")`, and render the static fallback when set.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22207,"content_sha256":"221a9ce2ec00144693c8f8fd88c9d085b51948108af47119a6531d50460fe41c"},{"filename":"scripts/fbx2glb.py","content":"# fbx2glb.py — Blender CLI: FBX -> GLB preserving ALL animation clips.\n#\n# Usage:\n# blender -b -P fbx2glb.py -- input.fbx output.glb\n#\n# Why this exists (the critical pitfall): a naive import_scene.fbx +\n# export_scene.gltf exports only ONE clip — the active action (e.g.\n# \"Rig|T-Pose\") — even when 76 actions imported. Fix: push every action onto\n# its own NLA track and export with export_animation_mode='NLA_TRACKS'.\n#\n# Also baked in:\n# - strips \"Rig|\" / \"Armature|\" / \"mixamo.com|\" prefixes -> clean clip\n# names for Three.js (Idle, Walking_A, ...)\n# - normals_make_consistent(inside=False) on every mesh + outward sanity\n# check (FBX imports often arrive with some normals inverted)\n# - forces materials to OPAQUE / not backface-culled (FBX import loves to\n# set blended transparency -> glTF alphaMode BLEND -> looks like\n# inverted normals in Three.js). A stdlib post-patch of the GLB JSON\n# chunk runs as a safety net (works regardless of Blender version\n# differences in material API).\n\nimport struct\nimport json\nimport sys\n\nimport bpy\n\n\ndef get_args():\n argv = sys.argv\n if \"--\" not in argv:\n raise SystemExit(\"usage: blender -b -P fbx2glb.py -- input.fbx output.glb\")\n args = argv[argv.index(\"--\") + 1:]\n if len(args) != 2:\n raise SystemExit(\"usage: blender -b -P fbx2glb.py -- input.fbx output.glb\")\n return args[0], args[1]\n\n\ndef clean_action_name(name):\n if \"|\" in name:\n name = name.split(\"|\")[-1]\n return name\n\n\ndef push_all_actions_to_nla():\n armatures = [o for o in bpy.data.objects if o.type == \"ARMATURE\"]\n if not armatures:\n print(\"WARNING: no armature found — exporting without skeletal animation\")\n return\n arm = armatures[0]\n if arm.animation_data is None:\n arm.animation_data_create()\n ad = arm.animation_data\n ad.action = None # active action would otherwise shadow the NLA stack\n already = {strip.action for t in ad.nla_tracks for strip in t.strips}\n n = 0\n for act in list(bpy.data.actions):\n if act in already:\n continue\n act.name = clean_action_name(act.name)\n act.use_fake_user = True\n track = ad.nla_tracks.new()\n track.name = act.name\n start = int(act.frame_range[0])\n track.strips.new(act.name, max(start, 0), act)\n n += 1\n print(f\"NLA: pushed {n} actions onto tracks of '{arm.name}'\")\n\n\ndef fix_normals():\n from mathutils import Vector\n for obj in bpy.data.objects:\n if obj.type != \"MESH\":\n continue\n bpy.ops.object.select_all(action=\"DESELECT\")\n obj.select_set(True)\n bpy.context.view_layer.objects.active = obj\n bpy.ops.object.mode_set(mode=\"EDIT\")\n bpy.ops.mesh.select_all(action=\"SELECT\")\n bpy.ops.mesh.normals_make_consistent(inside=False)\n bpy.ops.object.mode_set(mode=\"OBJECT\")\n # sanity check: mean dot(poly normal, poly center - centroid) > 0\n me = obj.data\n if not me.polygons:\n continue\n centroid = Vector((0, 0, 0))\n for v in me.vertices:\n centroid += v.co\n centroid /= len(me.vertices)\n s = 0.0\n for p in me.polygons:\n d = (p.center - centroid)\n if d.length > 1e-9:\n s += p.normal.dot(d.normalized())\n s /= len(me.polygons)\n flag = \"\" if s > 0 else \" \u003c-- WARNING: normals look inward\"\n print(f\"normals '{obj.name}': outward score {s:.3f}{flag}\")\n\n\ndef force_opaque_materials():\n for mat in bpy.data.materials:\n try:\n mat.blend_method = \"OPAQUE\" # pre-4.2 / legacy EEVEE path\n except Exception:\n pass\n try:\n mat.surface_render_method = \"DITHERED\" # 4.2+ EEVEE Next path\n except Exception:\n pass\n try:\n mat.use_backface_culling = False # -> doubleSided: true\n except Exception:\n pass\n\n\n# --- stdlib GLB JSON-chunk patch (safety net, mirrors scripts/glb_patch.py) ---\n\ndef patch_glb_opaque(path):\n MAGIC, CHUNK_JSON = 0x46546C67, 0x4E4F534A\n with open(path, \"rb\") as f:\n data = f.read()\n magic, version, _ = struct.unpack_from(\"\u003cIII\", data, 0)\n if magic != MAGIC or version != 2:\n print(f\"patch: {path} is not GLB v2, skipping\")\n return\n chunks, off = [], 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n chunks.append((ctype, data[off + 8: off + 8 + clen]))\n off += 8 + clen\n out = []\n for ctype, payload in chunks:\n if ctype == CHUNK_JSON:\n g = json.loads(payload.decode(\"utf-8\"))\n for m in g.get(\"materials\", []):\n m[\"alphaMode\"] = \"OPAQUE\"\n m.pop(\"alphaCutoff\", None)\n m[\"doubleSided\"] = True\n payload = json.dumps(g, separators=(\",\", \":\")).encode(\"utf-8\")\n out.append((ctype, payload))\n body = b\"\"\n for ctype, payload in out:\n pad = (4 - len(payload) % 4) % 4\n payload += (b\" \" if ctype == CHUNK_JSON else b\"\\x00\") * pad\n body += struct.pack(\"\u003cII\", len(payload), ctype) + payload\n with open(path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, 12 + len(body)) + body)\n print(f\"patch: forced OPAQUE/doubleSided in {path}\")\n\n\ndef main():\n src, dst = get_args()\n bpy.ops.wm.read_factory_settings(use_empty=True)\n bpy.ops.import_scene.fbx(filepath=src)\n print(f\"imported: {src} — actions: {len(bpy.data.actions)}, \"\n f\"meshes: {sum(1 for o in bpy.data.objects if o.type == 'MESH')}\")\n\n push_all_actions_to_nla()\n fix_normals()\n force_opaque_materials()\n\n bpy.ops.export_scene.gltf(\n filepath=dst,\n export_format=\"GLB\",\n export_animations=True,\n export_animation_mode=\"NLA_TRACKS\",\n export_skins=True,\n export_yup=True,\n export_apply=False, # applying modifiers would break the armature binding\n )\n patch_glb_opaque(dst)\n print(f\"done: {dst}\")\n print(\"verify with: python3 glb_inspect.py \" + dst)\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6108,"content_sha256":"37e8421c748bcd61c848d70b48b33bd185d16216bb854286b5df1ba85a2527e7"},{"filename":"scripts/glb_inspect.py","content":"#!/usr/bin/env python3\n\"\"\"glb_inspect.py — stdlib-only GLB animation/skin inspector.\n\nPrints: animation clip count + names, skins/meshes/images/materials,\nmaterial alphaMode (warns on non-OPAQUE), and per-clip root-node scale\nranges (catches the Meshy library-animation baked-scale bug, e.g. Hips\nscale 1.176 on one clip while others are 1.0).\n\nA correct character GLB: skins >= 1, expected clip count, all alphaMode\nOPAQUE, every clip's root scale ~= 1.0. If clips == 1 and the name contains\n\"T-Pose\", the NLA-tracks export step was skipped.\n\nUsage:\n python3 glb_inspect.py model.glb\n\"\"\"\nimport json\nimport struct\nimport sys\n\nMAGIC = 0x46546C67\nCHUNK_JSON = 0x4E4F534A\nCHUNK_BIN = 0x004E4942\n\nCOMPONENT_FMT = {5120: \"b\", 5121: \"B\", 5122: \"h\", 5123: \"H\", 5125: \"I\", 5126: \"f\"}\nTYPE_COUNT = {\"SCALAR\": 1, \"VEC2\": 2, \"VEC3\": 3, \"VEC4\": 4, \"MAT4\": 16}\n\n\ndef read_glb(path):\n with open(path, \"rb\") as f:\n data = f.read()\n magic, version, _ = struct.unpack_from(\"\u003cIII\", data, 0)\n if magic != MAGIC or version != 2:\n raise SystemExit(f\"{path}: not a GLB v2 file\")\n gltf, binchunk = None, b\"\"\n off = 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n payload = data[off + 8: off + 8 + clen]\n if ctype == CHUNK_JSON:\n gltf = json.loads(payload.decode(\"utf-8\"))\n elif ctype == CHUNK_BIN:\n binchunk = payload\n off += 8 + clen\n return gltf, binchunk\n\n\ndef accessor_values(gltf, binchunk, idx):\n acc = gltf[\"accessors\"][idx]\n if \"min\" in acc and \"max\" in acc:\n return acc[\"min\"], acc[\"max\"], None\n bv = gltf[\"bufferViews\"][acc[\"bufferView\"]]\n fmt = COMPONENT_FMT[acc[\"componentType\"]]\n ncomp = TYPE_COUNT[acc[\"type\"]]\n base = bv.get(\"byteOffset\", 0) + acc.get(\"byteOffset\", 0)\n stride = bv.get(\"byteStride\") or ncomp * struct.calcsize(fmt)\n vals = []\n for i in range(acc[\"count\"]):\n vals.append(struct.unpack_from(\"\u003c\" + fmt * ncomp, binchunk, base + i * stride))\n mins = [min(v[c] for v in vals) for c in range(ncomp)]\n maxs = [max(v[c] for v in vals) for c in range(ncomp)]\n return mins, maxs, vals\n\n\ndef find_skeleton_roots(gltf):\n \"\"\"Joint nodes whose parent is not itself a joint (per skin).\"\"\"\n joints = set()\n for skin in gltf.get(\"skins\", []):\n joints.update(skin.get(\"joints\", []))\n parented = {}\n for ni, node in enumerate(gltf.get(\"nodes\", [])):\n for child in node.get(\"children\", []):\n parented[child] = ni\n return {j for j in joints if parented.get(j) not in joints}\n\n\ndef main():\n if len(sys.argv) != 2:\n raise SystemExit(__doc__)\n gltf, binchunk = read_glb(sys.argv[1])\n anims = gltf.get(\"animations\", [])\n print(f\"file: {sys.argv[1]}\")\n print(f\"meshes: {len(gltf.get('meshes', []))}\")\n print(f\"skins: {len(gltf.get('skins', []))}\")\n print(f\"images: {len(gltf.get('images', []))}\")\n print(f\"materials: {len(gltf.get('materials', []))}\")\n for i, mat in enumerate(gltf.get(\"materials\", [])):\n mode = mat.get(\"alphaMode\", \"OPAQUE\")\n ds = mat.get(\"doubleSided\", False)\n flag = \"\" if mode == \"OPAQUE\" else \" \u003c-- WARNING: not OPAQUE (three.js depth-sorting artifacts)\"\n print(f\" material[{i}] '{mat.get('name', '?')}': alphaMode={mode} doubleSided={ds}{flag}\")\n print(f\"animations: {len(anims)}\")\n\n roots = find_skeleton_roots(gltf)\n warnings = []\n for a in anims:\n name = a.get(\"name\", \"?\")\n scale_note = \"\"\n for ch in a.get(\"channels\", []):\n tgt = ch.get(\"target\", {})\n if tgt.get(\"path\") != \"scale\" or tgt.get(\"node\") not in roots:\n continue\n sampler = a[\"samplers\"][ch[\"sampler\"]]\n mins, maxs, _ = accessor_values(gltf, binchunk, sampler[\"output\"])\n lo, hi = min(mins), max(maxs)\n if abs(lo - 1.0) > 1e-3 or abs(hi - 1.0) > 1e-3:\n node_name = gltf[\"nodes\"][tgt[\"node\"]].get(\"name\", tgt[\"node\"])\n scale_note = (f\" \u003c-- WARNING: root '{node_name}' scale in \"\n f\"[{lo:.4f}, {hi:.4f}] != 1.0 (Meshy baked-scale bug; \"\n f\"re-run merge_anim_glbs.py or fix manually)\")\n warnings.append(name)\n print(f\" clip: {name}{scale_note}\")\n\n if len(anims) == 1 and \"t-pose\" in anims[0].get(\"name\", \"\").lower().replace(\"_\", \"-\"):\n print(\"WARNING: single clip named like T-Pose — NLA-tracks export step was skipped\")\n if not gltf.get(\"skins\"):\n print(\"WARNING: skins == 0 — weight transfer failed (check data_transfer direction: source must be ACTIVE)\")\n if warnings:\n sys.exit(2)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4765,"content_sha256":"9e86d5e20d2f86bada6aeac442384eaa68a3f4b7149fe19b16a4815853c9f7de"},{"filename":"scripts/glb_merge_anims.py","content":"#!/usr/bin/env python3\n\"\"\"glb_merge_anims.py — stdlib-only merger of single-clip GLBs (Meshy outputs)\ninto one multi-clip GLB. No Blender required.\n\nDesigned for Meshy: rigged.glb (mesh+skeleton from /rigging) + N animation\nGLBs from the same rig task (identical node names). Animations are remapped\ninto the base file by NODE NAME, so it also works across re-exports as long\nas bone names match.\n\nIncludes the two mandatory Meshy post-fixes from the skill:\n * root-bone baked-scale bug: if a clip's root scale channel != 1.0\n (observed 1.176 on idle), scale values are rewritten to 1.0 and the same\n clip's root translation is divided by the factor;\n * materials forced to alphaMode OPAQUE + doubleSided (the \"inverted\n normals\" lookalike).\n\nUsage:\n python3 glb_merge_anims.py base.glb walk.glb:Walk run.glb:Run \\\n idle.glb:Idle attack.glb:Attack out.glb\n\nVerify the result with glb_inspect.py (clip count, skins>=1, root scale 1.0).\n\"\"\"\nimport json\nimport struct\nimport sys\n\nMAGIC = 0x46546C67\nCHUNK_JSON = 0x4E4F534A\nCHUNK_BIN = 0x004E4942\nCOMP_SIZE = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4}\nTYPE_COUNT = {\"SCALAR\": 1, \"VEC2\": 2, \"VEC3\": 3, \"VEC4\": 4, \"MAT4\": 16}\nSCALE_TOL = 0.02\n\n\ndef read_glb(path):\n with open(path, \"rb\") as f:\n data = f.read()\n magic, version, _ = struct.unpack_from(\"\u003cIII\", data, 0)\n if magic != MAGIC or version != 2:\n raise SystemExit(f\"{path}: not a GLB v2\")\n gltf, binc = None, b\"\"\n off = 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n payload = data[off + 8: off + 8 + clen]\n if ctype == CHUNK_JSON:\n gltf = json.loads(payload.decode(\"utf-8\"))\n elif ctype == CHUNK_BIN:\n binc = payload\n off += 8 + clen\n return gltf, bytearray(binc)\n\n\ndef write_glb(path, gltf, binc):\n if gltf.get(\"buffers\"):\n gltf[\"buffers\"][0][\"byteLength\"] = len(binc)\n j = json.dumps(gltf, separators=(\",\", \":\")).encode(\"utf-8\")\n j += b\" \" * ((4 - len(j) % 4) % 4)\n b = bytes(binc) + b\"\\x00\" * ((4 - len(binc) % 4) % 4)\n total = 12 + 8 + len(j) + 8 + len(b)\n with open(path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, total))\n f.write(struct.pack(\"\u003cII\", len(j), CHUNK_JSON) + j)\n f.write(struct.pack(\"\u003cII\", len(b), CHUNK_BIN) + b)\n\n\ndef accessor_bytes(gltf, binc, idx):\n acc = gltf[\"accessors\"][idx]\n bv = gltf[\"bufferViews\"][acc[\"bufferView\"]]\n n = COMP_SIZE[acc[\"componentType\"]] * TYPE_COUNT[acc[\"type\"]]\n start = bv.get(\"byteOffset\", 0) + acc.get(\"byteOffset\", 0)\n return acc, bytes(binc[start: start + acc[\"count\"] * n])\n\n\ndef append_accessor(base, base_bin, acc, raw):\n while len(base_bin) % 4:\n base_bin.append(0)\n bv_idx = len(base.setdefault(\"bufferViews\", []))\n base[\"bufferViews\"].append({\"buffer\": 0, \"byteOffset\": len(base_bin),\n \"byteLength\": len(raw)})\n base_bin.extend(raw)\n new_acc = {k: acc[k] for k in\n (\"componentType\", \"count\", \"type\", \"min\", \"max\", \"normalized\")\n if k in acc}\n new_acc[\"bufferView\"] = bv_idx\n acc_idx = len(base.setdefault(\"accessors\", []))\n base[\"accessors\"].append(new_acc)\n return acc_idx\n\n\ndef node_names(gltf):\n return {n.get(\"name\", f\"node_{i}\"): i for i, n in enumerate(gltf.get(\"nodes\", []))}\n\n\ndef root_joints(gltf):\n joints = set()\n for skin in gltf.get(\"skins\", []):\n joints.update(skin.get(\"joints\", []))\n parent = {}\n for ni, n in enumerate(gltf.get(\"nodes\", [])):\n for c in n.get(\"children\", []):\n parent[c] = ni\n return {j for j in joints if parent.get(j) not in joints}\n\n\ndef f32_list(raw):\n return list(struct.unpack(f\"\u003c{len(raw)//4}f\", raw))\n\n\ndef f32_bytes(vals):\n return struct.pack(f\"\u003c{len(vals)}f\", *vals)\n\n\ndef merge_clip(base, base_bin, donor, donor_bin, clip_name):\n names = node_names(base)\n merged = 0\n for anim in donor.get(\"animations\", []):\n new_anim = {\"name\": clip_name if len(donor.get(\"animations\", [])) == 1\n else anim.get(\"name\", clip_name),\n \"samplers\": [], \"channels\": []}\n for smp in anim[\"samplers\"]:\n in_acc, in_raw = accessor_bytes(donor, donor_bin, smp[\"input\"])\n out_acc, out_raw = accessor_bytes(donor, donor_bin, smp[\"output\"])\n new_anim[\"samplers\"].append({\n \"input\": append_accessor(base, base_bin, in_acc, in_raw),\n \"output\": append_accessor(base, base_bin, out_acc, out_raw),\n \"interpolation\": smp.get(\"interpolation\", \"LINEAR\")})\n skipped = 0\n for ch in anim[\"channels\"]:\n tgt = ch[\"target\"]\n dn = donor[\"nodes\"][tgt[\"node\"]].get(\"name\")\n if dn not in names:\n skipped += 1\n continue\n new_anim[\"channels\"].append({\"sampler\": ch[\"sampler\"],\n \"target\": {\"node\": names[dn],\n \"path\": tgt[\"path\"]}})\n if skipped:\n print(f\" '{new_anim['name']}': skipped {skipped} channels (no matching node name)\")\n if not new_anim[\"channels\"]:\n print(f\" '{new_anim['name']}': NO matching channels — skeletons incompatible?\")\n continue\n base.setdefault(\"animations\", []).append(new_anim)\n merged += 1\n print(f\" merged clip '{new_anim['name']}' \"\n f\"({len(new_anim['channels'])} channels)\")\n return merged\n\n\ndef fix_root_scale(base, base_bin):\n roots = root_joints(base)\n for anim in base.get(\"animations\", []):\n scale_ch = [c for c in anim.get(\"channels\", [])\n if c[\"target\"][\"path\"] == \"scale\" and c[\"target\"][\"node\"] in roots]\n for ch in scale_ch:\n smp = anim[\"samplers\"][ch[\"sampler\"]]\n acc = base[\"accessors\"][smp[\"output\"]]\n bv = base[\"bufferViews\"][acc[\"bufferView\"]]\n start = bv.get(\"byteOffset\", 0) + acc.get(\"byteOffset\", 0)\n n = acc[\"count\"] * 3\n vals = f32_list(bytes(base_bin[start: start + n * 4]))\n mean = sum(vals) / len(vals)\n if abs(mean - 1.0) \u003c= SCALE_TOL:\n continue\n print(f\"FIX root scale in '{anim.get('name')}': mean {mean:.4f} -> 1.0\")\n base_bin[start: start + n * 4] = f32_bytes([1.0] * n)\n acc.pop(\"min\", None); acc.pop(\"max\", None)\n for ch2 in anim[\"channels\"]:\n if (ch2[\"target\"][\"path\"] == \"translation\"\n and ch2[\"target\"][\"node\"] == ch[\"target\"][\"node\"]):\n smp2 = anim[\"samplers\"][ch2[\"sampler\"]]\n acc2 = base[\"accessors\"][smp2[\"output\"]]\n bv2 = base[\"bufferViews\"][acc2[\"bufferView\"]]\n s2 = bv2.get(\"byteOffset\", 0) + acc2.get(\"byteOffset\", 0)\n n2 = acc2[\"count\"] * 3\n tv = f32_list(bytes(base_bin[s2: s2 + n2 * 4]))\n base_bin[s2: s2 + n2 * 4] = f32_bytes([v / mean for v in tv])\n acc2.pop(\"min\", None); acc2.pop(\"max\", None)\n print(f\" translation of same root divided by {mean:.4f}\")\n\n\ndef main():\n if len(sys.argv) \u003c 4:\n raise SystemExit(__doc__)\n base_path, out_path = sys.argv[1], sys.argv[-1]\n base, base_bin = read_glb(base_path)\n print(f\"base: {base_path} ({len(base.get('animations', []))} clips, \"\n f\"{len(base.get('skins', []))} skins)\")\n for spec in sys.argv[2:-1]:\n if \":\" in spec:\n path, name = spec.rsplit(\":\", 1)\n else:\n path, name = spec, spec.rsplit(\"/\", 1)[-1].rsplit(\".\", 1)[0]\n donor, donor_bin = read_glb(path)\n print(f\"donor: {path} -> '{name}'\")\n merge_clip(base, base_bin, donor, donor_bin, name)\n fix_root_scale(base, base_bin)\n for m in base.get(\"materials\", []):\n m[\"alphaMode\"] = \"OPAQUE\"\n m.pop(\"alphaCutoff\", None)\n m[\"doubleSided\"] = True\n write_glb(out_path, base, base_bin)\n print(f\"done: {out_path} ({len(base.get('animations', []))} clips)\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8221,"content_sha256":"bef0e1fa9e690406bfd90a1ec77a3fa7647d55fdd75d83bd94cad0119bdbe501"},{"filename":"scripts/glb_patch.py","content":"#!/usr/bin/env python3\n\"\"\"glb_patch.py — stdlib-only GLB material patcher.\n\nForces alphaMode: OPAQUE + doubleSided: true on every material by rewriting\nthe JSON chunk of a binary glTF. Use on any GLB you did not export yourself\n(Meshy outputs, downloaded assets) — FBX-derived exports love to carry\nalphaMode: BLEND, which in Three.js renders back faces over front faces and\nlooks exactly like inverted normals.\n\nUsage:\n python3 glb_patch.py input.glb [output.glb]\n(in-place if output omitted)\n\"\"\"\nimport json\nimport struct\nimport sys\n\nMAGIC = 0x46546C67 # 'glTF'\nCHUNK_JSON = 0x4E4F534A # 'JSON'\n\n\ndef read_glb(path):\n with open(path, \"rb\") as f:\n data = f.read()\n magic, version, _length = struct.unpack_from(\"\u003cIII\", data, 0)\n if magic != MAGIC or version != 2:\n raise SystemExit(f\"{path}: not a GLB v2 file\")\n chunks = []\n off = 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n chunks.append((ctype, data[off + 8: off + 8 + clen]))\n off += 8 + clen\n return chunks\n\n\ndef write_glb(path, chunks):\n body = b\"\"\n for ctype, payload in chunks:\n pad = (4 - len(payload) % 4) % 4\n payload = payload + (b\" \" if ctype == CHUNK_JSON else b\"\\x00\") * pad\n body += struct.pack(\"\u003cII\", len(payload), ctype) + payload\n with open(path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, 12 + len(body)) + body)\n\n\ndef patch_materials(gltf):\n changed = []\n for i, mat in enumerate(gltf.get(\"materials\", [])):\n before = (mat.get(\"alphaMode\", \"OPAQUE\"), mat.get(\"doubleSided\", False))\n mat[\"alphaMode\"] = \"OPAQUE\"\n mat.pop(\"alphaCutoff\", None)\n mat[\"doubleSided\"] = True\n if before != (\"OPAQUE\", True):\n changed.append((i, mat.get(\"name\", f\"material_{i}\"), before))\n return changed\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n raise SystemExit(__doc__)\n src = sys.argv[1]\n dst = sys.argv[2] if len(sys.argv) > 2 else src\n chunks = read_glb(src)\n out = []\n changed = None\n for ctype, payload in chunks:\n if ctype == CHUNK_JSON:\n gltf = json.loads(payload.decode(\"utf-8\"))\n changed = patch_materials(gltf)\n payload = json.dumps(gltf, separators=(\",\", \":\")).encode(\"utf-8\")\n out.append((ctype, payload))\n write_glb(dst, out)\n if changed is None:\n raise SystemExit(\"no JSON chunk found — corrupt GLB?\")\n if changed:\n for i, name, before in changed:\n print(f\"patched material[{i}] '{name}': {before} -> ('OPAQUE', True)\")\n else:\n print(\"all materials already OPAQUE/doubleSided\")\n print(f\"written: {dst}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2727,"content_sha256":"77566c01a29590035db7121b55c152bc30525c24ec01dd3fa9c34a04be190984"},{"filename":"scripts/merge_anim_glbs.py","content":"# merge_anim_glbs.py — Blender CLI: merge single-clip GLBs (Meshy outputs)\n# into one multi-clip GLB, with the MANDATORY root-bone scale fix.\n#\n# Usage:\n# blender -b -P merge_anim_glbs.py -- \\\n# rigged.glb walk.glb:Walk run.glb:Run idle.glb:Idle attack.glb:Attack out.glb\n#\n# rigged.glb — Meshy rigged_character_glb_url (mesh + skeleton, base)\n# clipN.glb:Name — Meshy animation GLBs (same skeleton), clip renamed to Name\n# out.glb — merged result\n#\n# VERIFIED BUG this script fixes: Meshy *library* animations (POST\n# /openapi/v1/animations) can bake a scale factor into the root bone's scale\n# channel (observed: Hips scale 1.176 on idle while walk/run from the rig\n# were 1.0) — the character visibly grows when the clip plays. Fix: zero the\n# scale keys to 1.0 AND divide the same clip's root translation keys by the\n# same factor (otherwise the character floats above ground).\n\nimport struct\nimport json\nimport sys\n\nimport bpy\n\nSCALE_TOL = 0.02 # treat |scale-1| > 2% as the baked-scale bug\n\n\ndef get_args():\n argv = sys.argv\n if \"--\" not in argv:\n raise SystemExit(__doc__)\n args = argv[argv.index(\"--\") + 1:]\n if len(args) \u003c 3:\n raise SystemExit(__doc__)\n return args[0], args[1:-1], args[-1]\n\n\ndef import_clip(path, clip_name):\n \"\"\"Import an animation GLB, keep its action (renamed), delete its objects.\"\"\"\n before_objs = set(bpy.data.objects)\n before_acts = set(bpy.data.actions)\n bpy.ops.import_scene.gltf(filepath=path)\n new_acts = [a for a in bpy.data.actions if a not in before_acts]\n if not new_acts:\n raise SystemExit(f\"{path}: no animation found\")\n act = max(new_acts, key=lambda a: len(a.fcurves))\n act.name = clip_name\n act.use_fake_user = True # survive object deletion\n for extra in new_acts:\n if extra is not act:\n bpy.data.actions.remove(extra)\n bpy.ops.object.select_all(action=\"DESELECT\")\n for o in set(bpy.data.objects) - before_objs:\n o.select_set(True)\n bpy.ops.object.delete()\n return act\n\n\ndef root_bone_name(arm):\n for b in arm.data.bones:\n if b.parent is None:\n return b.name\n raise SystemExit(\"armature has no root bone\")\n\n\ndef fix_root_scale(act, root):\n \"\"\"Detect & fix baked root scale; returns the factor or None.\"\"\"\n scale_curves = [fc for fc in act.fcurves\n if fc.data_path == f'pose.bones[\"{root}\"].scale']\n if not scale_curves:\n return None\n vals = [kp.co.y for fc in scale_curves for kp in fc.keyframe_points]\n if not vals:\n return None\n factor = sum(vals) / len(vals)\n if abs(factor - 1.0) \u003c= SCALE_TOL:\n return None\n for fc in scale_curves:\n for kp in fc.keyframe_points:\n kp.co.y = 1.0\n kp.handle_left.y = 1.0\n kp.handle_right.y = 1.0\n fc.update()\n for fc in act.fcurves:\n if fc.data_path == f'pose.bones[\"{root}\"].location':\n for kp in fc.keyframe_points:\n kp.co.y /= factor\n kp.handle_left.y /= factor\n kp.handle_right.y /= factor\n fc.update()\n return factor\n\n\ndef main():\n base_path, clip_args, out_path = get_args()\n bpy.ops.wm.read_factory_settings(use_empty=True)\n\n bpy.ops.import_scene.gltf(filepath=base_path)\n armatures = [o for o in bpy.data.objects if o.type == \"ARMATURE\"]\n if not armatures:\n raise SystemExit(\"base GLB has no armature — use the rigged_character GLB\")\n arm = armatures[0]\n base_acts = list(bpy.data.actions) # base may carry its own clip(s)\n\n clips = []\n for spec in clip_args:\n if \":\" in spec:\n path, name = spec.rsplit(\":\", 1)\n else:\n path, name = spec, spec.rsplit(\"/\", 1)[-1].rsplit(\".\", 1)[0]\n clips.append(import_clip(path, name))\n print(f\"imported clip '{name}' from {path}\")\n\n root = root_bone_name(arm)\n for act in clips + base_acts:\n factor = fix_root_scale(act, root)\n if factor:\n print(f\"FIXED baked root scale in '{act.name}': {factor:.4f} -> 1.0 \"\n f\"(translation divided by the same factor)\")\n\n # all actions -> NLA tracks on the base armature\n if arm.animation_data is None:\n arm.animation_data_create()\n ad = arm.animation_data\n ad.action = None\n for act in clips + base_acts:\n track = ad.nla_tracks.new()\n track.name = act.name\n track.strips.new(act.name, max(int(act.frame_range[0]), 0), act)\n print(f\"NLA: {len(clips) + len(base_acts)} clips on '{arm.name}' (root bone: {root})\")\n\n bpy.ops.export_scene.gltf(\n filepath=out_path,\n export_format=\"GLB\",\n export_animations=True,\n export_animation_mode=\"NLA_TRACKS\",\n export_skins=True,\n export_yup=True,\n export_apply=False,\n )\n\n # stdlib OPAQUE/doubleSided patch (the \"inverted normals\" fix)\n MAGIC, CHUNK_JSON = 0x46546C67, 0x4E4F534A\n with open(out_path, \"rb\") as f:\n data = f.read()\n chunks, off = [], 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n chunks.append((ctype, data[off + 8: off + 8 + clen]))\n off += 8 + clen\n body = b\"\"\n for ctype, payload in chunks:\n if ctype == CHUNK_JSON:\n g = json.loads(payload.decode(\"utf-8\"))\n for m in g.get(\"materials\", []):\n m[\"alphaMode\"] = \"OPAQUE\"\n m.pop(\"alphaCutoff\", None)\n m[\"doubleSided\"] = True\n payload = json.dumps(g, separators=(\",\", \":\")).encode(\"utf-8\")\n pad = (4 - len(payload) % 4) % 4\n payload += (b\" \" if ctype == CHUNK_JSON else b\"\\x00\") * pad\n body += struct.pack(\"\u003cII\", len(payload), ctype) + payload\n with open(out_path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, 12 + len(body)) + body)\n\n print(f\"done: {out_path}\")\n print(\"verify with: python3 glb_inspect.py \" + out_path +\n \" (all clips present, root scale == 1.0 everywhere)\")\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6058,"content_sha256":"7bf06cd4e6fe70866cb11f0893ffb5fd6f11edb5cf7919c5496a31844ca60aa3"},{"filename":"scripts/pipeline.py","content":"#!/usr/bin/env python3\n\"\"\"Texture factory post-process: GPT output -> seamless tile + PBR maps.\\n\\nSeam fix modes: blend=\"cut\" (default) hides the junction with a hard\\nminimal-error cut that dodges detailed features - fully sharp; \"feather\"\\nis the legacy cross-fade (soft band on structured materials).\n\nSelf-contained (numpy + pillow only). Steps: border trim -> exact palette\ntransfer back to the reference -> mathematical seam fix (Moisan periodic\ndecomposition + offset blend + luminance flatten) -> PBR maps computed with\nwrap-around filters (basecolor / normal / roughness / height).\n\nUsage:\n python3 pipeline.py gpt_output.png -o textures/{id} --ref textures/{id}_ref.png\n python3 pipeline.py tile.png -o textures/{id} --trim 0 # non-GPT input\n python3 pipeline.py tile.png -o textures/{id} --trim 0 --no-seam # maps only\n python3 pipeline.py tile.png -o textures/{id} --overlap 0.08 # narrow blend\n\"\"\"\nimport argparse\nfrom pathlib import Path\n\nimport numpy as np\nfrom PIL import Image\n\n\n# ---------------- seam fix (former seamless.py) ----------------\n\ndef periodic_component(img):\n img = img.astype(np.float64)\n h, w = img.shape[:2]\n out = np.empty_like(img)\n for c in range(img.shape[2]):\n u = img[..., c]\n v = np.zeros_like(u)\n v[0, :] += u[-1, :] - u[0, :]\n v[-1, :] += u[0, :] - u[-1, :]\n v[:, 0] += u[:, -1] - u[:, 0]\n v[:, -1] += u[:, 0] - u[:, -1]\n fy = np.cos(2 * np.pi * np.arange(h) / h)[:, None]\n fx = np.cos(2 * np.pi * np.arange(w) / w)[None, :]\n denom = 2 * fy + 2 * fx - 4\n denom[0, 0] = 1.0\n s = np.fft.fft2(v) / denom\n s[0, 0] = 0.0\n out[..., c] = u - np.real(np.fft.ifft2(s))\n return np.clip(out, 0, 255)\n\n\ndef offset_blend(img, overlap=0.25):\n img = img.astype(np.float64)\n\n def blend_seam(a, axis):\n size = a.shape[axis]\n rolled = np.roll(a, size // 2, axis=axis)\n k = max(int(size * overlap / 2), 1)\n c = size // 2\n idx = [slice(None)] * 3\n idx[axis] = slice(c - k, c + k)\n donor = np.take(a, range(c - k, c + k), axis=axis)\n w = 1 - np.abs(np.linspace(-1, 1, 2 * k))\n shape = [1, 1, 1]\n shape[axis] = 2 * k\n w = w.reshape(shape)\n rolled[tuple(idx)] = rolled[tuple(idx)] * (1 - w) + donor * w\n return np.roll(rolled, -(size // 2), axis=axis)\n\n return np.clip(blend_seam(blend_seam(img, 1), 0), 0, 255)\n\n\ndef flatten_luminance(img, sigma_frac=0.07):\n img = img.astype(np.float64)\n h, w = img.shape[:2]\n fy = np.fft.fftfreq(h)[:, None]\n fx = np.fft.fftfreq(w)[None, :]\n sigma = sigma_frac * min(h, w)\n k = np.exp(-2 * (np.pi * sigma) ** 2 * (fy ** 2 + fx ** 2))\n low = np.real(np.fft.ifft2(np.fft.fft2(img.mean(axis=2)) * k))\n return np.clip(img + (low.mean() - low)[..., None], 0, 255)\n\n\n\n\n# ---------------- min-cut seam (sharp default) ----------------\n\ndef _hcut_cyclic(D, tries=14):\n \"\"\"Min-cost horizontal path through D (h,w), one row per column, step ±1,\n constrained to path[0] == path[-1] so the cut respects the wrap.\"\"\"\n h, w = D.shape\n starts = np.argsort(D[:, 0] + D[:, -1])[:tries]\n best_cost, best_path = np.inf, None\n for s in starts:\n M = np.full((h, w), np.inf)\n M[s, 0] = D[s, 0]\n back = np.zeros((h, w), int)\n for j in range(1, w):\n prev = M[:, j - 1]\n up = np.concatenate(([np.inf], prev[:-1]))\n down = np.concatenate((prev[1:], [np.inf]))\n cand = np.stack([up, prev, down])\n M[:, j] = D[:, j] + cand.min(0)\n back[:, j] = cand.argmin(0) - 1\n if M[s, -1] >= best_cost:\n continue\n best_cost = M[s, -1]\n path = np.zeros(w, int)\n path[-1] = s\n for j in range(w - 1, 0, -1):\n path[j - 1] = path[j] + back[path[j], j]\n best_path = path\n return best_path\n\n\ndef _edge_energy(x):\n return (abs(np.diff(x, axis=1, prepend=x[:, :1]))\n + abs(np.diff(x, axis=0, prepend=x[:1])))\n\n\ndef _cut_axis(a, overlap, lam=0.6, stone_w=1.5, feather_px=3):\n \"\"\"Repair the axis-0 wrap junction with a hard minimal-error cut: the seam\n is covered by a thin donor tube from the tile center, bounded by two cyclic\n min-cost paths that dodge high-detail features. No averaging except a\n feather_px-wide feather along the cut line itself.\"\"\"\n n = a.shape[0]\n c = n // 2\n k = max(int(n * overlap / 2), 2)\n rolled = np.roll(a, c, axis=0)\n band = rolled[c - k:c + k].copy()\n # donor: the cleanest contiguous strip (its own middle rows must be smooth,\n # since they become the new wrap pair) that also matches the band\n cands = [int(n * f) for f in (0.25, 0.33, 0.5, 0.66, 0.75)]\n best, donor = np.inf, None\n for q in cands:\n if q - k \u003c 0 or q + k > n:\n continue\n d = a[q - k:q + k]\n smooth = np.abs(d[k] - d[k - 1]).mean() # future wrap pair\n fit = np.abs(band - d).mean()\n score = smooth * 3.0 + fit\n if score \u003c best:\n best, donor = score, d\n D = np.abs(band - donor).mean(-1)\n cost = D + stone_w * (_edge_energy(band.mean(-1)) + _edge_energy(donor.mean(-1)))\n r = np.arange(k - 1)[:, None]\n up = _hcut_cyclic(cost[:k - 1] + lam * (k - 1 - r))\n lo = _hcut_cyclic(cost[k + 1:] + lam * r) + (k + 1)\n rows = np.arange(2 * k)[:, None]\n alpha = ((rows > up[None, :]) & (rows \u003c lo[None, :])).astype(np.float64)\n if feather_px > 0:\n kern = np.ones(2 * feather_px + 1)\n kern /= kern.sum()\n alpha = np.apply_along_axis(\n lambda v: np.convolve(v, kern, mode=\"same\"), 0, alpha)\n alpha[k - 1:k + 1] = 1.0 # the junction itself stays 100% donor\n rolled[c - k:c + k] = band * (1 - alpha[..., None]) + donor * alpha[..., None]\n return np.roll(rolled, -c, axis=0)\n\n\ndef make_seamless(img, overlap=0.25, flatten=True, blend=\"cut\"):\n arr = np.asarray(img.convert(\"RGB\")).astype(np.float64)\n if flatten:\n arr = flatten_luminance(arr)\n arr = periodic_component(arr)\n if blend == \"cut\":\n arr = _cut_axis(arr, overlap)\n arr = np.swapaxes(_cut_axis(np.swapaxes(arr, 0, 1), overlap), 0, 1)\n arr = np.clip(arr, 0, 255)\n else:\n arr = offset_blend(arr, overlap)\n return Image.fromarray(arr.astype(np.uint8))\n\n\ndef composite_cross(orig_img, gpt_img, k_in=40, k_out=110, feather_px=3):\n \"\"\"Masked-inpaint emulation for the offset-inpaint pass: the model repaints\n the whole image, but only its center cross is taken - bounded by cyclic\n min-cut paths where original and repaint agree; everything else stays the\n untouched original. Returns the UNROLLED tile (the repaired cross becomes\n the wrap, the pristine original becomes the interior).\"\"\"\n O = np.asarray(orig_img.convert(\"RGB\")).astype(np.float64)\n G = np.asarray(gpt_img.convert(\"RGB\").resize(orig_img.size, Image.LANCZOS)\n ).astype(np.float64)\n n = O.shape[0]\n c = n // 2\n diff = np.abs(O - G).mean(-1)\n\n def band_alpha(d, axis):\n if axis == 1:\n d = d.T\n up = _hcut_cyclic(d[c - k_out:c - k_in]) + (c - k_out)\n lo = _hcut_cyclic(d[c + k_in:c + k_out]) + (c + k_in)\n rows = np.arange(n)[:, None]\n a = ((rows > up[None, :]) & (rows \u003c lo[None, :])).astype(np.float64)\n if feather_px > 0:\n kern = np.ones(2 * feather_px + 1)\n kern /= kern.sum()\n a = np.apply_along_axis(\n lambda v: np.convolve(v, kern, mode=\"same\"), 0, a)\n a[c - k_in:c + k_in] = 1.0\n return a if axis == 0 else a.T\n\n alpha = np.maximum(band_alpha(diff, 0), band_alpha(diff, 1))[..., None]\n out = np.clip(O * (1 - alpha) + G * alpha, 0, 255).astype(np.uint8)\n out = np.roll(np.roll(out, -c, 0), -(O.shape[1] // 2), 1)\n return Image.fromarray(out)\n\n\n# ---------------- post-process + PBR (former pipeline.py) ----------------\n\ndef trim_border(img, frac):\n if frac \u003c= 0:\n return img\n w, h = img.size\n k = int(min(w, h) * frac)\n return img.crop((k, k, w - k, h - k))\n\n\ndef match_colors(img, ref):\n a = np.asarray(img.convert(\"RGB\")).astype(np.float64)\n r = np.asarray(ref.convert(\"RGB\")).astype(np.float64)\n out = np.empty_like(a)\n bins = np.arange(257)\n for c in range(3):\n sh, _ = np.histogram(a[..., c], bins=bins, density=True)\n rh, _ = np.histogram(r[..., c], bins=bins, density=True)\n lut = np.interp(np.cumsum(sh) / sh.sum(),\n np.cumsum(rh) / rh.sum(), np.arange(256))\n out[..., c] = lut[a[..., c].astype(np.uint8)]\n return Image.fromarray(np.clip(out, 0, 255).astype(np.uint8))\n\n\ndef _wrap_blur(a, sigma):\n fy = np.fft.fftfreq(a.shape[0])[:, None]\n fx = np.fft.fftfreq(a.shape[1])[None, :]\n k = np.exp(-2 * (np.pi * sigma) ** 2 * (fy ** 2 + fx ** 2))\n return np.real(np.fft.ifft2(np.fft.fft2(a) * k))\n\n\ndef pbr_maps(img, strength=2.0):\n rgb = np.asarray(img.convert(\"RGB\")).astype(np.float64)\n lum = rgb.mean(axis=2) / 255.0\n\n height = _wrap_blur(lum, 2.0) - _wrap_blur(lum, 48.0)\n lo, hi = np.percentile(height, [1, 99])\n height = np.clip((height - lo) / (hi - lo + 1e-9), 0, 1)\n\n gx = (np.roll(height, -1, 1) - np.roll(height, 1, 1)) * strength * 255\n gy = (np.roll(height, -1, 0) - np.roll(height, 1, 0)) * strength * 255\n nz = np.ones_like(gx) * 255.0 / strength\n norm = np.sqrt(gx ** 2 + gy ** 2 + nz ** 2)\n normal = np.stack([(-gx / norm + 1) / 2, (gy / norm + 1) / 2,\n (nz / norm + 1) / 2], axis=-1)\n\n rough = 1.0 - np.clip((lum - _wrap_blur(lum, 8.0)) * 3 + 0.25, 0, 0.6)\n\n u8 = lambda a: Image.fromarray((np.clip(a, 0, 1) * 255).astype(np.uint8))\n return {\"basecolor\": img.convert(\"RGB\"), \"normal\": u8(normal),\n \"height\": u8(height), \"roughness\": u8(rough)}\n\n\ndef run(inp, prefix, ref=None, trim=0.04, overlap=0.25, seam=True, blend=\"cut\",\n inpaint=None):\n out = Path(prefix)\n out.parent.mkdir(parents=True, exist_ok=True)\n if inpaint:\n # inp = the ROLLED original (seam as a center cross), inpaint = the\n # model's repaint of it; composite takes only the cross from the model\n img = composite_cross(Image.open(inp), Image.open(inpaint))\n trim = 0\n img = trim_border(img, trim)\n else:\n img = trim_border(Image.open(inp), trim)\n if ref:\n img = match_colors(img, Image.open(ref))\n if seam:\n img = make_seamless(img, overlap, blend=blend)\n img.save(f\"{prefix}_seamless.png\")\n for n, m in pbr_maps(img).items():\n m.save(f\"{prefix}_{n}.png\")\n\n\nif __name__ == \"__main__\":\n p = argparse.ArgumentParser()\n p.add_argument(\"input\")\n p.add_argument(\"-o\", \"--prefix\", required=True)\n p.add_argument(\"--ref\", default=None)\n p.add_argument(\"--trim\", type=float, default=0.04)\n p.add_argument(\"--overlap\", type=float, default=0.25,\n help=\"seam corridor width: cut-path room for blend=cut, \"\n \"blend band for blend=feather (0.08 = narrow)\")\n p.add_argument(\"--blend\", choices=[\"cut\", \"feather\"], default=\"cut\",\n help=\"cut = sharp minimal-error cut (default); \"\n \"feather = legacy cross-fade (soft band at joints)\")\n p.add_argument(\"--no-seam\", action=\"store_true\",\n help=\"skip the seam fix (maps from an already-seamless tile)\")\n p.add_argument(\"--inpaint\", default=None,\n help=\"offset-inpaint finish: input is the ROLLED original, \"\n \"this is the model's repaint; only its center cross is \"\n \"composited in (masked-inpaint emulation)\")\n a = p.parse_args()\n run(a.input, a.prefix, a.ref, a.trim, a.overlap,\n seam=not a.no_seam, blend=a.blend, inpaint=a.inpaint)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11911,"content_sha256":"5a2d7859f21fc03182adb8089f859757907e867b84d46dbedf9ed496a67ee567"},{"filename":"scripts/proc_anim_dragon.py","content":"# proc_anim_dragon.py — sine-based idle/fly clips baked to keyframes.\n#\n# Usage (after proc_rig_dragon.py + proc_weights.py):\n# blender -b work.blend -P proc_anim_dragon.py -- dragon_anim.glb\n#\n# Pure math, no mocap: every clip is a sum of sinusoids per bone, baked to\n# keyframes (every 2 frames @ 24 fps) and exported via NLA tracks.\n#\n# fly (2 s loop): wings +/-35-45deg with the OUTER segment lagging the\n# inner by ~0.2 of the cycle (flex illusion); body bobs in\n# COUNTER-phase to the downstroke; legs tucked (constant);\n# traveling wave down the tail.\n# idle (4 s loop): chest breathing, head sway, tail micro-wave, wing\n# micro-adjust.\n#\n# Other creatures: swap the MOVES tables (gait recipes in\n# references/procedural-animation.md — quadruped diagonal pairs, serpentine\n# traveling wave, slime squash & stretch, etc.).\n\nimport math\nimport struct\nimport json\nimport sys\n\nimport bpy\n\nFPS = 24\nSTEP = 2 # bake every 2 frames\n\n\ndef get_args():\n argv = sys.argv\n if \"--\" not in argv:\n raise SystemExit(\"usage: blender -b work.blend -P proc_anim_dragon.py -- out.glb\")\n rest = argv[argv.index(\"--\") + 1:]\n if len(rest) != 1:\n raise SystemExit(\"usage: blender -b work.blend -P proc_anim_dragon.py -- out.glb\")\n return rest[0]\n\n\ndef deg(d):\n return math.radians(d)\n\n\n# MOVES: bone -> list of (channel, axis, amplitude, phase, constant_offset)\n# channel: \"rot\" (rotation_euler, radians) or \"loc\" (location, blender units)\n# value(t) = offset + amplitude * sin(2*pi*t + phase), t in [0,1) cycle phase\n\nFLY_SECONDS = 2.0\nFLY = {\n \"wing_L_1\": [(\"rot\", 0, deg(40), 0.0, 0.0)],\n \"wing_R_1\": [(\"rot\", 0, deg(40), math.pi, 0.0)], # mirrored\n \"wing_L_2\": [(\"rot\", 0, deg(25), -0.2 * 2 * math.pi, 0.0)], # outer lags inner\n \"wing_R_2\": [(\"rot\", 0, deg(25), math.pi - 0.2 * 2 * math.pi, 0.0)],\n \"spine\": [(\"loc\", 2, 0.06, math.pi, 0.0)], # bob counter-phase\n \"chest\": [(\"rot\", 0, deg(4), math.pi, 0.0)],\n \"tail_1\": [(\"rot\", 0, deg(6), 0.00, 0.0)],\n \"tail_2\": [(\"rot\", 0, deg(8), -0.6, 0.0)], # wave: growing lag\n \"tail_3\": [(\"rot\", 0, deg(10), -1.2, 0.0)],\n \"tail_4\": [(\"rot\", 0, deg(12), -1.8, 0.0)],\n \"leg_FL\": [(\"rot\", 0, 0.0, 0.0, deg(35))], # tucked, constant\n \"leg_FR\": [(\"rot\", 0, 0.0, 0.0, deg(35))],\n \"leg_BL\": [(\"rot\", 0, 0.0, 0.0, deg(40))],\n \"leg_BR\": [(\"rot\", 0, 0.0, 0.0, deg(40))],\n}\n\nIDLE_SECONDS = 4.0\nIDLE = {\n \"chest\": [(\"rot\", 0, deg(2.5), 0.0, 0.0)], # breathing\n \"neck\": [(\"rot\", 2, deg(4), 0.7, 0.0)], # slow sway\n \"head\": [(\"rot\", 2, deg(3), 1.2, 0.0)],\n \"tail_1\": [(\"rot\", 2, deg(3), 0.0, 0.0)],\n \"tail_2\": [(\"rot\", 2, deg(4), -0.6, 0.0)],\n \"tail_3\": [(\"rot\", 2, deg(5), -1.2, 0.0)],\n \"tail_4\": [(\"rot\", 2, deg(6), -1.8, 0.0)],\n \"wing_L_1\": [(\"rot\", 0, deg(2), 0.0, deg(5))],\n \"wing_R_1\": [(\"rot\", 0, deg(2), math.pi, deg(5))],\n}\n\n\ndef bake_action(arm, name, moves, seconds):\n frames = int(seconds * FPS)\n act = bpy.data.actions.new(name)\n act.use_fake_user = True\n arm.animation_data.action = act\n for bone_name, channels in moves.items():\n pb = arm.pose.bones.get(bone_name)\n if pb is None:\n print(f\" (skip missing bone '{bone_name}')\")\n continue\n pb.rotation_mode = \"XYZ\"\n for f in range(1, frames + 1, STEP):\n t = (f - 1) / frames # cycle phase 0..1\n for channel, axis, amp, phase, offset in channels:\n val = offset + amp * math.sin(2 * math.pi * t + phase)\n if channel == \"rot\":\n pb.rotation_euler[axis] = val\n pb.keyframe_insert(\"rotation_euler\", index=axis, frame=f)\n else:\n pb.location[axis] = val\n pb.keyframe_insert(\"location\", index=axis, frame=f)\n # close the loop exactly\n for channel, axis, amp, phase, offset in channels:\n val = offset + amp * math.sin(phase)\n if channel == \"rot\":\n pb.rotation_euler[axis] = val\n pb.keyframe_insert(\"rotation_euler\", index=axis, frame=frames + 1)\n else:\n pb.location[axis] = val\n pb.keyframe_insert(\"location\", index=axis, frame=frames + 1)\n arm.animation_data.action = None\n print(f\"baked '{name}': {frames} frames ({seconds}s @ {FPS}fps)\")\n return act\n\n\ndef main():\n out_path = get_args()\n armatures = [o for o in bpy.data.objects if o.type == \"ARMATURE\"]\n if not armatures:\n raise SystemExit(\"no armature — run proc_rig + proc_weights first\")\n arm = armatures[0]\n if arm.animation_data is None:\n arm.animation_data_create()\n bpy.context.scene.render.fps = FPS\n\n actions = [\n bake_action(arm, \"idle\", IDLE, IDLE_SECONDS),\n bake_action(arm, \"fly\", FLY, FLY_SECONDS),\n ]\n\n ad = arm.animation_data\n ad.action = None\n for act in actions:\n track = ad.nla_tracks.new()\n track.name = act.name\n track.strips.new(act.name, max(int(act.frame_range[0]), 0), act)\n\n bpy.ops.export_scene.gltf(\n filepath=out_path,\n export_format=\"GLB\",\n export_animations=True,\n export_animation_mode=\"NLA_TRACKS\",\n export_skins=True,\n export_yup=True,\n export_apply=False,\n )\n\n # stdlib OPAQUE/doubleSided patch\n MAGIC, CHUNK_JSON = 0x46546C67, 0x4E4F534A\n with open(out_path, \"rb\") as f:\n data = f.read()\n chunks, off = [], 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n chunks.append((ctype, data[off + 8: off + 8 + clen]))\n off += 8 + clen\n body = b\"\"\n for ctype, payload in chunks:\n if ctype == CHUNK_JSON:\n g = json.loads(payload.decode(\"utf-8\"))\n for m in g.get(\"materials\", []):\n m[\"alphaMode\"] = \"OPAQUE\"\n m.pop(\"alphaCutoff\", None)\n m[\"doubleSided\"] = True\n payload = json.dumps(g, separators=(\",\", \":\")).encode(\"utf-8\")\n pad = (4 - len(payload) % 4) % 4\n payload += (b\" \" if ctype == CHUNK_JSON else b\"\\x00\") * pad\n body += struct.pack(\"\u003cII\", len(payload), ctype) + payload\n with open(out_path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, 12 + len(body)) + body)\n\n print(f\"done: {out_path}\")\n print(\"QC: render phase grid at non-uniform cycle fractions \"\n \"(0.0, 0.23, 0.41, 0.68, 0.87) — see references/procedural-animation.md\")\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6679,"content_sha256":"969f404fa6472566c8c5b8ed61524e66871b8fc10b56f4d8159b06c7d892aeaf"},{"filename":"scripts/proc_rig_dragon.py","content":"# proc_rig_dragon.py — Blender CLI: procedural skeleton from bbox analysis.\n#\n# Usage:\n# blender -b -P proc_rig_dragon.py -- dragon.glb work.blend\n#\n# Builds a 16-bone skeleton for a winged quadruped (dragon) purely from\n# bounding-box fractions — no mesh-specific magic numbers, so the SAME\n# script reruns on the textured mesh later (texture-swap rule, see\n# references/procedural-animation.md).\n#\n# Layout: spine -> chest -> neck -> head; tail x4 (opposite the head);\n# wing_L/R x2 (inner+outer); legs x4.\n#\n# Head-side detection: the half along the body axis with more vertex volume\n# in the UPPER part of the bbox is the head side.\n#\n# For other creatures: change BONES below (recipes in\n# references/procedural-animation.md) — the weights and animation scripts\n# are bone-list-driven.\n\nimport sys\n\nimport bpy\nfrom mathutils import Vector\n\n\ndef get_args():\n argv = sys.argv\n if \"--\" not in argv:\n raise SystemExit(\"usage: blender -b -P proc_rig_dragon.py -- model.glb work.blend\")\n args = argv[argv.index(\"--\") + 1:]\n if len(args) != 2:\n raise SystemExit(\"usage: blender -b -P proc_rig_dragon.py -- model.glb work.blend\")\n return args\n\n\ndef main():\n src, dst = get_args()\n bpy.ops.wm.read_factory_settings(use_empty=True)\n bpy.ops.import_scene.gltf(filepath=src)\n meshes = [o for o in bpy.data.objects if o.type == \"MESH\"]\n if not meshes:\n raise SystemExit(\"no meshes in input\")\n\n # world bbox\n pts = [o.matrix_world @ Vector(c) for o in meshes for c in o.bound_box]\n lo = Vector((min(p.x for p in pts), min(p.y for p in pts), min(p.z for p in pts)))\n hi = Vector((max(p.x for p in pts), max(p.y for p in pts), max(p.z for p in pts)))\n size = hi - lo\n # body axis = longer horizontal axis (x or y)\n body_x = size.x >= size.y\n\n def P(fb, fs, fz):\n \"\"\"point from bbox fractions: fb along body axis, fs along side axis, fz up\"\"\"\n if body_x:\n return Vector((lo.x + fb * size.x, lo.y + fs * size.y, lo.z + fz * size.z))\n return Vector((lo.x + fs * size.x, lo.y + fb * size.y, lo.z + fz * size.z))\n\n # --- head side: more vertex volume in the upper half ---\n upper_lo_half = upper_hi_half = 0\n zmid = lo.z + 0.5 * size.z\n for o in meshes:\n mw = o.matrix_world\n for v in o.data.vertices:\n w = mw @ v.co\n if w.z \u003c zmid:\n continue\n t = ((w.x - lo.x) / max(size.x, 1e-9)) if body_x else ((w.y - lo.y) / max(size.y, 1e-9))\n if t \u003c 0.5:\n upper_lo_half += 1\n else:\n upper_hi_half += 1\n head_hi = upper_hi_half >= upper_lo_half\n print(f\"head side: {'+' if head_hi else '-'}body-axis \"\n f\"(upper-half verts {upper_hi_half} vs {upper_lo_half})\")\n\n def B(fb, fs, fz):\n \"\"\"fraction along body axis, flipped so head is always at fb=1\"\"\"\n return P(fb if head_hi else 1.0 - fb, fs, fz)\n\n # --- bone table: name, head point, tail point, parent ---\n BONES = [\n (\"spine\", B(0.45, 0.5, 0.55), B(0.60, 0.5, 0.58), None),\n (\"chest\", B(0.60, 0.5, 0.58), B(0.72, 0.5, 0.60), \"spine\"),\n (\"neck\", B(0.72, 0.5, 0.60), B(0.85, 0.5, 0.72), \"chest\"),\n (\"head\", B(0.85, 0.5, 0.72), B(0.97, 0.5, 0.80), \"neck\"),\n (\"tail_1\", B(0.45, 0.5, 0.55), B(0.33, 0.5, 0.52), \"spine\"),\n (\"tail_2\", B(0.33, 0.5, 0.52), B(0.22, 0.5, 0.48), \"tail_1\"),\n (\"tail_3\", B(0.22, 0.5, 0.48), B(0.12, 0.5, 0.44), \"tail_2\"),\n (\"tail_4\", B(0.12, 0.5, 0.44), B(0.03, 0.5, 0.40), \"tail_3\"),\n (\"wing_L_1\", B(0.58, 0.5, 0.62), B(0.58, 0.78, 0.70), \"chest\"),\n (\"wing_L_2\", B(0.58, 0.78, 0.70), B(0.58, 0.99, 0.72), \"wing_L_1\"),\n (\"wing_R_1\", B(0.58, 0.5, 0.62), B(0.58, 0.22, 0.70), \"chest\"),\n (\"wing_R_2\", B(0.58, 0.22, 0.70), B(0.58, 0.01, 0.72), \"wing_R_1\"),\n (\"leg_FL\", B(0.62, 0.62, 0.45), B(0.62, 0.64, 0.05), \"chest\"),\n (\"leg_FR\", B(0.62, 0.38, 0.45), B(0.62, 0.36, 0.05), \"chest\"),\n (\"leg_BL\", B(0.42, 0.62, 0.45), B(0.42, 0.64, 0.05), \"spine\"),\n (\"leg_BR\", B(0.42, 0.38, 0.45), B(0.42, 0.36, 0.05), \"spine\"),\n ]\n\n arm_data = bpy.data.armatures.new(\"ProcRig\")\n arm = bpy.data.objects.new(\"ProcRig\", arm_data)\n bpy.context.collection.objects.link(arm)\n bpy.context.view_layer.objects.active = arm\n bpy.ops.object.mode_set(mode=\"EDIT\")\n ebones = {}\n for name, head, tail, parent in BONES:\n eb = arm_data.edit_bones.new(name)\n eb.head, eb.tail = head, tail\n if parent:\n eb.parent = ebones[parent]\n ebones[name] = eb\n bpy.ops.object.mode_set(mode=\"OBJECT\")\n print(f\"skeleton: {len(BONES)} bones\")\n\n bpy.ops.wm.save_as_mainfile(filepath=dst)\n print(f\"saved: {dst}\")\n print(\"next: blender -b \" + dst + \" -P proc_weights.py -- \" + dst)\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4882,"content_sha256":"9c89bbb1084ca20624ee6fe0cf8ac0bc2998b6eec04f13f4ee44117b74264833"},{"filename":"scripts/proc_weights.py","content":"# proc_weights.py — distance-based skin weights for procedural rigs.\n#\n# Usage (after proc_rig_dragon.py):\n# blender -b work.blend -P proc_weights.py -- work.blend\n#\n# WHY NOT parent_set(type='ARMATURE_AUTO'): bone heat fails on generated\n# meshes (\"failed to find solution...\") and in headless mode can SILENTLY\n# produce all-zero weights -> the exported GLB has skins:0. This script\n# assigns every vertex to its 2 NEAREST BONES (distance from the vertex to\n# the bone head-tail segment) with inverse-distance blending. Crude but\n# stable on any topology; fine for stylized/low-poly creatures.\n#\n# Hard-fails if any mesh ends up with 0 weighted vertices.\n\nimport sys\n\nimport bpy\nfrom mathutils import Vector\n\n\ndef seg_dist(p, a, b):\n \"\"\"distance from point p to segment ab\"\"\"\n ab = b - a\n L2 = ab.length_squared\n if L2 \u003c 1e-12:\n return (p - a).length\n t = max(0.0, min(1.0, (p - a).dot(ab) / L2))\n return (p - (a + ab * t)).length\n\n\ndef main():\n out = None\n if \"--\" in sys.argv:\n rest = sys.argv[sys.argv.index(\"--\") + 1:]\n out = rest[0] if rest else None\n\n armatures = [o for o in bpy.data.objects if o.type == \"ARMATURE\"]\n meshes = [o for o in bpy.data.objects if o.type == \"MESH\"]\n if not armatures or not meshes:\n raise SystemExit(\"scene must contain an armature and meshes (run proc_rig first)\")\n arm = armatures[0]\n\n # bone segments in world space\n segs = []\n mw = arm.matrix_world\n for b in arm.data.bones:\n segs.append((b.name, mw @ b.head_local, mw @ b.tail_local))\n\n K = 2 # nearest bones per vertex\n for obj in meshes:\n groups = {}\n for name, _, _ in segs:\n groups[name] = obj.vertex_groups.get(name) or obj.vertex_groups.new(name=name)\n omw = obj.matrix_world\n weighted = 0\n for v in obj.data.vertices:\n p = omw @ v.co\n dists = sorted(((seg_dist(p, a, b), name) for name, a, b in segs))[:K]\n inv = [(1.0 / max(d, 1e-6), name) for d, name in dists]\n total = sum(w for w, _ in inv)\n for w, name in inv:\n groups[name].add([v.index], w / total, \"REPLACE\")\n weighted += 1\n print(f\"weights '{obj.name}': {weighted}/{len(obj.data.vertices)} verts\")\n if weighted == 0:\n raise SystemExit(\"0 weighted verts — aborting\")\n\n obj.parent = arm\n if not any(m.type == \"ARMATURE\" for m in obj.modifiers):\n mod = obj.modifiers.new(\"Armature\", \"ARMATURE\")\n mod.object = arm\n\n if out:\n bpy.ops.wm.save_as_mainfile(filepath=out)\n print(f\"saved: {out}\")\n print(\"next: blender -b work.blend -P proc_anim_dragon.py -- out.glb\")\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2730,"content_sha256":"dcefb493d002e0ac77197c5467b56080ddb492f98ea136bacdd9c8d42e6ab678"},{"filename":"scripts/rig_transfer.py","content":"# rig_transfer.py — Blender CLI: static GLB + rigged donor FBX -> animated GLB.\n#\n# Usage:\n# blender -b -P rig_transfer.py -- target_static.glb donor.fbx out.glb\n#\n# Borrows the skeleton + ALL animations from a rigged donor (KayKit knight,\n# any Mixamo rig) and binds them to a generated static mesh (sam_3_3d /\n# Meshy preview). One donor animates unlimited generated characters.\n#\n# CRITICAL PITFALLS baked in (do not regress):\n# 1. parent_set(type='ARMATURE_AUTO') (bone heat) FAILS on image-to-3D\n# meshes (\"failed to find solution for one or more bones\") — the\n# topology is photogrammetry-style. We use Data Transfer of vertex\n# weights from the donor's body meshes instead (VGROUP_WEIGHTS,\n# POLYINTERP_NEAREST).\n# 2. data_transfer direction: with use_reverse_transfer=False it goes\n# ACTIVE -> SELECTED. The weight SOURCE (donor mesh) must be ACTIVE.\n# Getting it backwards silently produces 0 weighted verts and skins:0\n# in the export — we hard-fail on that.\n# 3. Targets must be humanoid, in T-pose matching the donor's rest pose.\n#\n# Quality limits (state honestly): depends on donor/target proportion\n# similarity; thin dangling parts (capes, skirts) inherit approximate\n# weights.\n\nimport struct\nimport json\nimport sys\n\nimport bpy\nfrom mathutils import Vector\n\n\ndef get_args():\n argv = sys.argv\n if \"--\" not in argv:\n raise SystemExit(\"usage: blender -b -P rig_transfer.py -- target.glb donor.fbx out.glb\")\n args = argv[argv.index(\"--\") + 1:]\n if len(args) != 3:\n raise SystemExit(\"usage: blender -b -P rig_transfer.py -- target.glb donor.fbx out.glb\")\n return args\n\n\ndef world_bbox(objs):\n pts = []\n for o in objs:\n for c in o.bound_box:\n pts.append(o.matrix_world @ Vector(c))\n lo = Vector((min(p.x for p in pts), min(p.y for p in pts), min(p.z for p in pts)))\n hi = Vector((max(p.x for p in pts), max(p.y for p in pts), max(p.z for p in pts)))\n return lo, hi\n\n\ndef clean_action_name(name):\n return name.split(\"|\")[-1] if \"|\" in name else name\n\n\ndef main():\n target_path, donor_path, out_path = get_args()\n bpy.ops.wm.read_factory_settings(use_empty=True)\n\n # --- donor ---\n bpy.ops.import_scene.fbx(filepath=donor_path)\n donor_objs = list(bpy.data.objects)\n armatures = [o for o in donor_objs if o.type == \"ARMATURE\"]\n if not armatures:\n raise SystemExit(\"donor has no armature\")\n arm = armatures[0]\n donor_meshes = [o for o in donor_objs if o.type == \"MESH\"]\n if not donor_meshes:\n raise SystemExit(\"donor has no meshes (need them as weight source)\")\n print(f\"donor: armature '{arm.name}' ({len(arm.data.bones)} bones), \"\n f\"{len(donor_meshes)} meshes, {len(bpy.data.actions)} actions\")\n\n # --- target ---\n before = set(bpy.data.objects)\n bpy.ops.import_scene.gltf(filepath=target_path)\n target_meshes = [o for o in set(bpy.data.objects) - before if o.type == \"MESH\"]\n if not target_meshes:\n raise SystemExit(\"target GLB contains no meshes\")\n\n # --- scale & align donor armature (and meshes, used as weight source) ---\n d_lo, d_hi = world_bbox(donor_meshes)\n t_lo, t_hi = world_bbox(target_meshes)\n scale = (t_hi.z - t_lo.z) / max(d_hi.z - d_lo.z, 1e-9)\n d_center = (d_lo + d_hi) / 2\n t_center = (t_lo + t_hi) / 2\n for o in [arm] + donor_meshes:\n o.scale *= scale\n bpy.context.view_layer.update()\n d_lo, d_hi = world_bbox(donor_meshes)\n d_center = (d_lo + d_hi) / 2\n offset = Vector((t_center.x - d_center.x, t_center.y - d_center.y, t_lo.z - d_lo.z))\n for o in [arm] + donor_meshes:\n o.location += offset\n bpy.context.view_layer.update()\n print(f\"aligned donor: scale x{scale:.3f}, offset {tuple(round(v, 3) for v in offset)}\")\n\n # apply transforms so weights/armature bind in final space\n bpy.ops.object.select_all(action=\"DESELECT\")\n for o in [arm] + donor_meshes + target_meshes:\n o.select_set(True)\n bpy.context.view_layer.objects.active = arm\n bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)\n\n # --- join donor meshes into one weight source ---\n bpy.ops.object.select_all(action=\"DESELECT\")\n for o in donor_meshes:\n o.select_set(True)\n bpy.context.view_layer.objects.active = donor_meshes[0]\n if len(donor_meshes) > 1:\n bpy.ops.object.join()\n source = bpy.context.view_layer.objects.active\n\n # --- weight transfer: source must be ACTIVE, target SELECTED ---\n for tmesh in target_meshes:\n bpy.ops.object.select_all(action=\"DESELECT\")\n tmesh.select_set(True)\n source.select_set(True)\n bpy.context.view_layer.objects.active = source # ACTIVE = weight source!\n bpy.ops.object.data_transfer(\n use_reverse_transfer=False, # ACTIVE -> SELECTED\n data_type=\"VGROUP_WEIGHTS\",\n vert_mapping=\"POLYINTERP_NEAREST\",\n layers_select_src=\"ALL\",\n layers_select_dst=\"NAME\",\n mix_mode=\"REPLACE\",\n )\n weighted = sum(1 for v in tmesh.data.vertices if any(g.weight > 0 for g in v.groups))\n print(f\"weights '{tmesh.name}': {weighted}/{len(tmesh.data.vertices)} verts\")\n if weighted == 0:\n raise SystemExit(\"0 weighted verts — transfer direction wrong or meshes not overlapping; aborting (would export skins:0)\")\n\n # bind to armature WITHOUT auto weights\n tmesh.parent = arm\n mod = tmesh.modifiers.new(\"Armature\", \"ARMATURE\")\n mod.object = arm\n\n # --- drop donor mesh, keep armature + actions ---\n bpy.ops.object.select_all(action=\"DESELECT\")\n source.select_set(True)\n bpy.ops.object.delete()\n\n # --- normals fix on target ---\n for obj in target_meshes:\n bpy.ops.object.select_all(action=\"DESELECT\")\n obj.select_set(True)\n bpy.context.view_layer.objects.active = obj\n bpy.ops.object.mode_set(mode=\"EDIT\")\n bpy.ops.mesh.select_all(action=\"SELECT\")\n bpy.ops.mesh.normals_make_consistent(inside=False)\n bpy.ops.object.mode_set(mode=\"OBJECT\")\n\n # --- all actions -> NLA tracks ---\n if arm.animation_data is None:\n arm.animation_data_create()\n ad = arm.animation_data\n ad.action = None\n n = 0\n for act in list(bpy.data.actions):\n act.name = clean_action_name(act.name)\n act.use_fake_user = True\n track = ad.nla_tracks.new()\n track.name = act.name\n track.strips.new(act.name, max(int(act.frame_range[0]), 0), act)\n n += 1\n print(f\"NLA: {n} clips\")\n\n # --- materials + export ---\n for mat in bpy.data.materials:\n for attr, val in ((\"blend_method\", \"OPAQUE\"),\n (\"surface_render_method\", \"DITHERED\"),\n (\"use_backface_culling\", False)):\n try:\n setattr(mat, attr, val)\n except Exception:\n pass\n\n bpy.ops.export_scene.gltf(\n filepath=out_path,\n export_format=\"GLB\",\n export_animations=True,\n export_animation_mode=\"NLA_TRACKS\",\n export_skins=True,\n export_yup=True,\n export_apply=False,\n )\n\n # stdlib OPAQUE patch (safety net)\n MAGIC, CHUNK_JSON = 0x46546C67, 0x4E4F534A\n with open(out_path, \"rb\") as f:\n data = f.read()\n chunks, off = [], 12\n while off \u003c len(data):\n clen, ctype = struct.unpack_from(\"\u003cII\", data, off)\n chunks.append((ctype, data[off + 8: off + 8 + clen]))\n off += 8 + clen\n body = b\"\"\n for ctype, payload in chunks:\n if ctype == CHUNK_JSON:\n g = json.loads(payload.decode(\"utf-8\"))\n for m in g.get(\"materials\", []):\n m[\"alphaMode\"] = \"OPAQUE\"\n m.pop(\"alphaCutoff\", None)\n m[\"doubleSided\"] = True\n payload = json.dumps(g, separators=(\",\", \":\")).encode(\"utf-8\")\n pad = (4 - len(payload) % 4) % 4\n payload += (b\" \" if ctype == CHUNK_JSON else b\"\\x00\") * pad\n body += struct.pack(\"\u003cII\", len(payload), ctype) + payload\n with open(out_path, \"wb\") as f:\n f.write(struct.pack(\"\u003cIII\", MAGIC, 2, 12 + len(body)) + body)\n\n print(f\"done: {out_path}\")\n print(\"verify with: python3 glb_inspect.py \" + out_path + \" (skins must be >= 1)\")\n\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8326,"content_sha256":"59597690de9f415793776d0dbbd6f65b8279a5294ebb71e2b829199609add34b"},{"filename":"scripts/seamless.py","content":"#!/usr/bin/env python3\n\"\"\"Make a texture seamless: Moisan periodic decomposition + offset blend.\"\"\"\nimport argparse\n\nimport numpy as np\nfrom PIL import Image\n\n\ndef periodic_component(img):\n img = img.astype(np.float64)\n h, w = img.shape[:2]\n out = np.empty_like(img)\n for c in range(img.shape[2]):\n u = img[..., c]\n v = np.zeros_like(u)\n v[0, :] += u[-1, :] - u[0, :]\n v[-1, :] += u[0, :] - u[-1, :]\n v[:, 0] += u[:, -1] - u[:, 0]\n v[:, -1] += u[:, 0] - u[:, -1]\n fy = np.cos(2 * np.pi * np.arange(h) / h)[:, None]\n fx = np.cos(2 * np.pi * np.arange(w) / w)[None, :]\n denom = 2 * fy + 2 * fx - 4\n denom[0, 0] = 1.0\n s = np.fft.fft2(v) / denom\n s[0, 0] = 0.0\n out[..., c] = u - np.real(np.fft.ifft2(s))\n return np.clip(out, 0, 255)\n\n\ndef offset_blend(img, overlap=0.25):\n img = img.astype(np.float64)\n\n def blend_seam(a, axis):\n size = a.shape[axis]\n rolled = np.roll(a, size // 2, axis=axis)\n k = max(int(size * overlap / 2), 1)\n c = size // 2\n idx = [slice(None)] * 3\n idx[axis] = slice(c - k, c + k)\n donor = np.take(a, range(c - k, c + k), axis=axis)\n w = 1 - np.abs(np.linspace(-1, 1, 2 * k))\n shape = [1, 1, 1]\n shape[axis] = 2 * k\n w = w.reshape(shape)\n rolled[tuple(idx)] = rolled[tuple(idx)] * (1 - w) + donor * w\n return np.roll(rolled, -(size // 2), axis=axis)\n\n return np.clip(blend_seam(blend_seam(img, 1), 0), 0, 255)\n\n\ndef flatten_luminance(img, sigma_frac=0.07):\n img = img.astype(np.float64)\n h, w = img.shape[:2]\n fy = np.fft.fftfreq(h)[:, None]\n fx = np.fft.fftfreq(w)[None, :]\n sigma = sigma_frac * min(h, w)\n k = np.exp(-2 * (np.pi * sigma) ** 2 * (fy ** 2 + fx ** 2))\n low = np.real(np.fft.ifft2(np.fft.fft2(img.mean(axis=2)) * k))\n return np.clip(img + (low.mean() - low)[..., None], 0, 255)\n\n\ndef make_seamless(img, overlap=0.25, flatten=True):\n arr = np.asarray(img.convert(\"RGB\")).astype(np.float64)\n if flatten:\n arr = flatten_luminance(arr)\n arr = periodic_component(arr)\n arr = offset_blend(arr, overlap)\n return Image.fromarray(arr.astype(np.uint8))\n\n\nif __name__ == \"__main__\":\n p = argparse.ArgumentParser()\n p.add_argument(\"input\")\n p.add_argument(\"-o\", \"--output\", required=True)\n p.add_argument(\"--overlap\", type=float, default=0.25)\n a = p.parse_args()\n make_seamless(Image.open(a.input), a.overlap).save(a.output)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2520,"content_sha256":"c38f350eee6475fb16ec7db45bdbb83791946a2def32998324df54bbfc8deeae"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Higgsfield website builder (CLI) — three product types, three flows","type":"text"}]},{"type":"paragraph","content":[{"text":"You drive the whole lifecycle through the ","type":"text"},{"text":"Higgsfield CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"higgsfield website …","type":"text","marks":[{"type":"code_inline"}]},{"text":"), then edit code on the local filesystem with ","type":"text"},{"text":"git","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"bun","type":"text","marks":[{"type":"code_inline"}]},{"text":". You are building ONE per-website Cloudflare Worker: a ","type":"text"},{"text":"React 19 + TanStack Start","type":"text","marks":[{"type":"strong"}]},{"text":" app, ","type":"text"},{"text":"server-rendered (SSR)","type":"text","marks":[{"type":"strong"}]},{"text":", deployed as a single Worker at the product's own subdomain. The project lives in ","type":"text"},{"text":"app/","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — run every ","type":"text"},{"text":"bun","type":"text","marks":[{"type":"code_inline"}]},{"text":"/build command from there.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"The three types — and the REQUIRED ","type":"text"},{"text":"--type","type":"text","marks":[{"type":"code_inline"}]},{"text":" on create","type":"text"}]},{"type":"paragraph","content":[{"text":"higgsfield website create","type":"text","marks":[{"type":"code_inline"}]},{"text":" requires ","type":"text"},{"text":"--type","type":"text","marks":[{"type":"code_inline"}]},{"text":", and it is the ","type":"text"},{"text":"USER'S choice","type":"text","marks":[{"type":"strong"}]},{"text":" — when the request doesn't make it obvious, ask the user before creating (one question, up front):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--type website","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — a standalone product with NO Higgsfield integration and ","type":"text"},{"text":"NO AI generation of any kind","type":"text","marks":[{"type":"strong"}]},{"text":" (no image/video/audio/text generation — not via Higgsfield, and not via some other provider): no \"Sign in with Higgsfield\", no requests to Higgsfield, no fnf SDK. Every website gets a fully independent brand: own palette, type, and chrome from a design brief, custom Tailwind/CSS only — never import ","type":"text"},{"text":"@higgsfield/quanta/*","type":"text","marks":[{"type":"code_inline"}]},{"text":" or use q-prefixed tokens anywhere, and no \"Powered by / Built on Higgsfield\" badges or mentions in page content. The user's brand is the only brand on the page.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"higgsfield website create --type website","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--type app","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — a product tightly integrated with Higgsfield: its users Sign in with Higgsfield and generate images/videos through the fnf SDK (the full auth + D1 contract applies). An app must look and feel like a Higgsfield product: UI built with ","type":"text"},{"text":"Quanta","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"references/quanta-design.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") — and, for anything Quanta lacks, your own component built from Quanta primitives (never a third-party UI library) — starting from a standard app layout (","type":"text"},{"text":"references/app-layouts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Quanta and the app layouts are app-only — never applied to a ","type":"text"},{"text":"--type website","type":"text","marks":[{"type":"code_inline"}]},{"text":" build. The independent-brand rule and the wow pipeline (","type":"text"},{"text":"design-taste-frontend","type":"text","marks":[{"type":"code_inline"}]},{"text":", boards, wow catalog) are the website path; apps never get a custom brand — Quanta is the brand.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"higgsfield website create --type app","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--type game","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — a browser game: realtime multiplayer rooms on the game template, where the game itself is six pure functions in ","type":"text"},{"text":"app/src/logic.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the platform already owns sockets, rooms and persistence. Requires a ","type":"text"},{"text":"game genre","type":"text","marks":[{"type":"strong"}]},{"text":" as ","type":"text"},{"text":"--category","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"arcade","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"puzzle","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"shooter","type":"text","marks":[{"type":"code_inline"}]},{"text":", …, from ","type":"text"},{"text":"higgsfield website categories","type":"text","marks":[{"type":"code_inline"}]},{"text":") and takes ","type":"text"},{"text":"no","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"--template","type":"text","marks":[{"type":"code_inline"}]},{"text":" — a game scaffolds from the only template it can use. Single-player counts: set ","type":"text"},{"text":"minPlayers: 1","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"references/game-flow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"higgsfield website create --type game --category arcade","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Generation is ALWAYS an app.","type":"text","marks":[{"type":"strong"}]},{"text":" Any product that generates images, video, audio, or other AI media runs on Higgsfield — build it as ","type":"text"},{"text":"--type app","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Sign in with Higgsfield, generation on the user's Higgsfield credits). NEVER offer the user an option to \"bring your own image/video API\" or plug in their own generation key for a website — that path does not exist. ","type":"text"},{"text":"--type website","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ONLY for sites with no generation and no tie to Higgsfield or any other generation service. (A website may still use ordinary non-generation third-party APIs — payments, maps, email — with the user's own keys; that is unrelated to this rule.)","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick tells: \"landing page / portfolio / marketing site / SaaS with its own users, no AI generation\" → website. \"generates images/video/audio, or anything with Higgsfield models, credits, or generation history\" → app. \"something you play — a game, multiplayer or single-player\" → game.","type":"text"}]},{"type":"paragraph","content":[{"text":"Games moved onto this pipeline from a separate engine that is being retired. The ","type":"text"},{"text":"higgsfield game …","type":"text","marks":[{"type":"code_inline"}]},{"text":" commands are gone: a game is created, deployed and published exactly like a website. Any doc saying otherwise is out of date.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Always set a subdomain on create","type":"text"}]},{"type":"paragraph","content":[{"text":"higgsfield website create","type":"text","marks":[{"type":"code_inline"}]},{"text":" takes an optional ","type":"text"},{"text":"--subdomain","type":"text","marks":[{"type":"code_inline"}]},{"text":" — it becomes the site's slug, so the live URL is ","type":"text"},{"text":"\u003csubdomain>.\u003chost>","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"Always set it:","type":"text","marks":[{"type":"strong"}]},{"text":" pick one from the product's name or purpose; only omit it (which yields a random slug) if the user explicitly wants a random one. Rules for a good subdomain:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"More than 4 characters","type":"text","marks":[{"type":"strong"}]},{"text":" — short single words are reserved, so go a bit longer.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Memorable","type":"text","marks":[{"type":"strong"}]},{"text":" — derive it from the product name/purpose (e.g. ","type":"text"},{"text":"lumen-notes","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"pixelforge","type":"text","marks":[{"type":"code_inline"}]},{"text":"), not a random string.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Allowed characters only","type":"text","marks":[{"type":"strong"}]},{"text":" — lowercase letters, digits, and single hyphens (DNS-safe). No spaces, underscores, uppercase, or leading/trailing hyphens.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"A few reserved labels (e.g. ","type":"text"},{"text":"api","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"www","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"app","type":"text","marks":[{"type":"code_inline"}]},{"text":") and already-taken subdomains are rejected — if that happens, try a close variant.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"higgsfield","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not on ","type":"text"},{"text":"$PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":", install it:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -fsSL https://raw.githubusercontent.com/higgsfield-ai/cli/main/install.sh | sh","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"higgsfield account status","type":"text","marks":[{"type":"code_inline"}]},{"text":" reports ","type":"text"},{"text":"Session expired","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"Not authenticated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ask the user to run ","type":"text"},{"text":"higgsfield auth login","type":"text","marks":[{"type":"code_inline"}]},{"text":" (interactive) and wait for confirmation.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"git","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"bun","type":"text","marks":[{"type":"code_inline"}]},{"text":" are used locally once you clone the repo. The CLI itself handles create / repo / deploy / publish / status / db / secrets — and the asset generation jobs (","type":"text"},{"text":"higgsfield generate …","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"higgsfield model …","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pick the path, then follow ONE flow end-to-end","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolve the ","type":"text"},{"text":"--type","type":"text","marks":[{"type":"code_inline"}]},{"text":" (ask the user if unclear — it's their choice). In the SAME first question, also ask whether they want to ","type":"text"},{"text":"publish it to the Higgsfield community feed (marketplace)","type":"text","marks":[{"type":"strong"}]},{"text":" when it's ready (yes/no). Remember the answer: if yes, publish automatically at the end (after deploy + metadata), no need to ask again; if no, only deploy. Don't block the build on it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the matching flow and follow it — it is the complete workflow for that type, including its own references, hard rules, editing map, and deploy/publish gates:","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"For every ","type":"text"},{"text":"--type website","type":"text","marks":[{"type":"code_inline"}]},{"text":" build the intake ALWAYS asks the user to choose between an ","type":"text"},{"text":"Animated (recommended)","type":"text","marks":[{"type":"strong"}]},{"text":" website — a scroll-driven journey through a generated film (","type":"text"},{"text":"references/scroll-scrub.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") — and a ","type":"text"},{"text":"Non-animated","type":"text","marks":[{"type":"strong"}]},{"text":" one. This question is mandatory: never skip it, even when the request seems to imply a choice. Animated is the recommended default (used only when the user is unreachable / doesn't answer); the flow below carries both paths and the full pipeline.","type":"text"}]},{"type":"paragraph","content":[{"text":"Inside the animated path the default is a ","type":"text"},{"text":"single-shot","type":"text","marks":[{"type":"strong"}]},{"text":" film — ONE continuous ~15s take, scrubbed end to end, no seams. The multi-scene chain is opt-in and costs several extra minutes per leg; take it only when the brief genuinely travels between distinct worlds. ","type":"text"},{"text":"references/scroll-scrub.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" owns that call.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flow","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--type website","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/website-flow.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — phased pipeline (animated website by default): intake → concept → reference boards → asset system → build-to-boards → motion → cover + metadata → mechanical gate → deploy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--type app","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/app-flow.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — the Quanta toolkit, the six code layouts, fnf SDK + auth + D1 contract, launch cover + metadata, publish gate","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--type game","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/game-flow.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — the six-function ","type":"text"},{"text":"logic.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" contract, realtime rooms, a game-genre ","type":"text"},{"text":"--category","type":"text","marks":[{"type":"code_inline"}]},{"text":", play-testing, deploy + publish","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"A game's ART and AUDIO live here too, under the ","type":"text"},{"text":"game-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix, and ","type":"text"},{"text":"references/game-flow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" indexes them: ","type":"text"},{"text":"references/game-design-system.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (read first — profile, core loop, asset manifest), ","type":"text"},{"text":"references/game-stylization.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the STYLE FORMULA every visual reuses), ","type":"text"},{"text":"references/game-2d-animation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/game-textures.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/game-3d-animation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/game-procedural-animation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/game-audio.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/game-meshy-api.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"references/game-meshy-input-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". The GLB/rigging/texture tooling they drive ships in this skill's ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"All three flows share the same platform mechanics (SSR Worker, ","type":"text"},{"text":"app.manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" infra, a single live deploy via ","type":"text"},{"text":"higgsfield website deploy \u003cwebsite_id>","type":"text","marks":[{"type":"code_inline"}]},{"text":", the cover + metadata requirement below, and the publish gate) — each flow restates what it needs, so you never have to read another.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cover + metadata — ALWAYS part of building, never publish-only","type":"text"}]},{"type":"paragraph","content":[{"text":"Every build — website or app, no matter how small — ships with the branded launch cover and filled feed-card metadata, generated per ","type":"text"},{"text":"references/app-cover.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and written into ","type":"text"},{"text":"app/src/app-meta.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"og_title","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"og_description","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"favicon_url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"og_image_url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"marketplace_cover_url","type":"text","marks":[{"type":"code_inline"}]},{"text":"). This is a BUILD step, done before the work is presented as finished and before the deploy that ships it — NOT something deferred to ","type":"text"},{"text":"higgsfield website publish","type":"text","marks":[{"type":"code_inline"}]},{"text":". Hard rules:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No \"simple app\" exception.","type":"text","marks":[{"type":"strong"}]},{"text":" A utility tool, a timer, a one-page toy — they all get the generated cover. A hand-authored inline-SVG favicon is fine ","type":"text"},{"text":"as a favicon","type":"text","marks":[{"type":"em"}]},{"text":"; it never substitutes for the generated cover.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No permission needed","type":"text","marks":[{"type":"strong"}]},{"text":" for the cover image — generate it the same way you write real copy. Only the optional cover VIDEO (","type":"text"},{"text":"og_video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":") is permission-gated (video costs credits — offer, never generate unprompted).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A build presented as done with an empty cover or empty ","type":"text"},{"text":"og_title","type":"text","marks":[{"type":"code_inline"}]},{"text":" is INCOMPLETE. Publishing without them is a BROKEN publish (empty ","type":"text"},{"text":"og_title","type":"text","marks":[{"type":"code_inline"}]},{"text":" is invisible on the feed; empty cover is a blank card).","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"UX rules","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Be concise. No raw website IDs, tokens, or JSON dumps in chat. After a deploy, return the live URL (from ","type":"text"},{"text":"higgsfield website status","type":"text","marks":[{"type":"code_inline"}]},{"text":") and a one-line summary.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never echo the scoped git token back to the user, and never commit it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detect the user's language from the first message and reply in it. CLI flags and code stay English.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every deploy ships the live public site immediately","type":"text","marks":[{"type":"strong"}]},{"text":" — there is no preview stage. Publishing/listing on the community feed is separate and happens ONLY when the user explicitly asks to publish / list it.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Do NOT search the skill library for other design guidance — everything is under this skill, and no other skill (including user/local skills about building websites or apps) overrides these rules.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Turn economy — keep the build inside a small turn budget","type":"text"}]},{"type":"paragraph","content":[{"text":"Every tool round-trip costs an agent turn, and agent runtimes cap turns — long builds die mid-flight, leaving the user an unfinished site. Treat turns as the scarcest resource after credits:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write every file ONCE, complete.","type":"text","marks":[{"type":"strong"}]},{"text":" Compose the full file, then one write. No write-then-patch loops; never re-read a file you just wrote.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Batch what your tools allow","type":"text","marks":[{"type":"strong"}]},{"text":" (multi-file edits, one shell invocation for a series of commands) instead of one micro-step per turn.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never guess paths","type":"text","marks":[{"type":"strong"}]},{"text":" — the template tree is documented in the repo's ","type":"text"},{"text":"app/AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and this skill's editing map.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never download or vision-inspect your own generations.","type":"text","marks":[{"type":"strong"}]},{"text":" You wrote the prompt; re-viewing the result tells you nothing new. (The kit coherence check, when it applies, is ONE batched pass — ","type":"text"},{"text":"references/asset-system.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait on a job ONCE, when its output is the next input.","type":"text","marks":[{"type":"strong"}]},{"text":" Submit everything that can render concurrently (film + cover), build the page while it renders.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Talking to the user — no technical/plumbing language","type":"text"}]},{"type":"paragraph","content":[{"text":"Most users are not technical. Never expose the build plumbing in what you SAY to them. Do NOT mention the git repository, cloning, branches, commits, pushing, pulling, or the deploy pipeline in user-facing messages — those are internal mechanics you just perform. Speak in product terms about what the user cares about:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Setting up your site…\" — not \"cloning the repo\" / \"scaffolding the project\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Saving your changes…\" / \"Updating the site…\" — not \"committing\" / \"pushing\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Your preview is ready: \u003curl>\" — not \"deployed the branch\" / \"the build passed\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Publishing your site…\" — not \"merging to main\" / \"pushing to production\".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"This is about the WORDS in chat only — keep doing the real steps behind the scenes; just don't narrate them in developer terms. (The one exception: a user who is clearly technical and explicitly asks about the repo, branch, or deploy mechanics — then answer plainly. CLI flags and code stay English.)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference index (what's in this bundle)","type":"text"}]},{"type":"paragraph","content":[{"text":"The two flow files pull in the rest as needed — you don't read these directly unless a flow sends you there.","type":"text"}]},{"type":"paragraph","content":[{"text":"Both flows:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"references/app-cover.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (launch cover + OG image), ","type":"text"},{"text":"references/runtime-and-infra.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (TanStack routes, SSR, Worker runtime), ","type":"text"},{"text":"references/security.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Worker hardening, OWASP audit, threat model).","type":"text"}]},{"type":"paragraph","content":[{"text":"Website flow:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"references/design-recipe.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/wow-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/wow-maker.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/reference-boards.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/asset-system.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/image-to-code.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/design-taste-frontend.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/review-rubric.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/seo.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/scroll-scrub.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (A4 seam-locked journey), ","type":"text"},{"text":"references/scroll-scrub-asset-react.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/scroll-scrub-asset-css.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"references/scroll-scrub-asset-video.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bundled Markdown code assets loaded only when A4 is selected).","type":"text"}]},{"type":"paragraph","content":[{"text":"App flow:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"references/app-quickstart.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (START HERE — the working critical path: auth, generation submit/poll, result rendering, common Quanta components), ","type":"text"},{"text":"references/quanta-design.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/app-layouts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/fnf-sdk.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/fnf-react.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/auth.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/containers.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"references/cover-animator.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (permission-gated ~5s cover video → ","type":"text"},{"text":"og_video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":"), ","type":"text"},{"text":"references/contest.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the $100k app contest — the entry auto-publishes the app; submit with social links).","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"higgsfield-websites","author":"@skillopedia","source":{"stars":1854,"repo_name":"skills","origin_url":"https://github.com/higgsfield-ai/skills/blob/HEAD/higgsfield-websites/SKILL.md","repo_owner":"higgsfield-ai","body_sha256":"6de160a62a0c0195c51174a97abf878d68404c354a19a411b441273a68aad19b","cluster_key":"91307b148bd05c567ff28556fca0a125354dc1cf7d562f87cd639419feb087bb","clean_bundle":{"format":"clean-skill-bundle-v1","source":"higgsfield-ai/skills/higgsfield-websites/SKILL.md","text_bytes":700567,"source_commit":"4d0757471a704dac8729ea3c2ef0f3f330e0d9b6","skills_sh_rank":430,"attachment_count":48,"text_attachments":48,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"higgsfield-websites/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"0.14.0","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"0.14.0","category":"web-development","import_tag":"skills-sh-top20k-2026-11","description":"Build, edit, and deploy full-stack websites, apps and games via the Higgsfield CLI (`higgsfield website …`). Each is a React 19 + TanStack Start SSR app in one Cloudflare Worker (D1/R2/KV/DO/Containers). THREE product types, picked via `--type` on create: `website` (standalone, no Higgsfield integration — references/website-flow.md), `app` (Sign in with Higgsfield + fnf SDK, Quanta — references/app-flow.md), `game` (realtime multiplayer rooms — references/game-flow.md). Routes to the right flow; each carries its own rules and deploy/publish gates.\nUse when: \"build me a website\", \"make a landing page\", \"create a web app\", \"build a SaaS dashboard / portfolio\", \"make me a game\", \"deploy this site\", \"publish\". Also owns GAME ART: \"make a spritesheet\", \"tileable texture\", \"animate a 3D character\", game music/SFX — see the game-* references.\nNOT for: single image/video/audio generation (higgsfield-generate), product photos (higgsfield-product-photoshoot), marketplace cards (higgsfield-marketplace-cards).\n","allowed-tools":"Bash","argument-hint":"[what to build or edit] [--type website|app|game]"}},"renderedAt":1793666663662}

Higgsfield website builder (CLI) — three product types, three flows You drive the whole lifecycle through the Higgsfield CLI ( ), then edit code on the local filesystem with + . You are building ONE per-website Cloudflare Worker: a React 19 + TanStack Start app, server-rendered (SSR) , deployed as a single Worker at the product's own subdomain. The project lives in — run every /build command from there. The three types — and the REQUIRED on create requires , and it is the USER'S choice — when the request doesn't make it obvious, ask the user before creating (one question, up front): - — a sta…