@genex-ai/cli-demo 0.47.0-dev.71 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.47.0-dev.71",
3
+ "version": "0.47.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,12 +36,14 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@sentry/node": "^10.63.0"
39
+ "@sentry/node": "^10.63.0",
40
+ "pngjs": "^7.0.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@dimforge/rapier3d-compat": "^0.19.3",
43
44
  "@genex-ai/multiplayer": "workspace:*",
44
45
  "@pixiv/three-vrm": "^3.5.4",
46
+ "@types/pngjs": "^6.0.5",
45
47
  "@types/three": "^0.185.0",
46
48
  "three": "^0.185.1",
47
49
  "tsup": "^8.0.0",
@@ -0,0 +1,381 @@
1
+ ---
2
+ name: genex-ai-hud
3
+ description: Generate a production HUD sprite set — matched frames, masks, and icons in one coherent art style — via the mockup-then-deconstruct pipeline (`npx genex image` + `npx genex ui`), then wire it as a DOM overlay with channel-accurate masked fills. Use when the game wants a cohesive art-directed HUD (fantasy brass, sci-fi glass, racing carbon) rather than a clean minimal CSS HUD.
4
+ ---
5
+
6
+ # Genex AI · HUD
7
+
8
+ Generate every sprite of a game HUD in ONE art style — a full-HUD mockup, then
9
+ a deconstruction into individual assets — and wire them as a plain-DOM overlay
10
+ with masked fills that track the art's real channels. The result is an
11
+ art-directed HUD (etched steel ammo strip, parchment HP bar, chrome minimap
12
+ ring) that no hand-written CSS can fake.
13
+
14
+ ## When to use this vs. a hand-coded CSS HUD
15
+
16
+ - **Use `$genex-threejs-game-ui` alone** (clean CSS bars, text, plates) for
17
+ most games — it's faster, crisper, and always readable. That skill also owns
18
+ the overlay architecture this one plugs into.
19
+ - **Use this skill** when the game's identity wants a *materially* art-directed
20
+ HUD — gothic filigree frames, rusted post-apocalyptic panels, carbon-fiber
21
+ racing telemetry, hand-painted storybook plates — where the frames themselves
22
+ are illustrations. You'll spend ~10 image generations and real wiring effort;
23
+ make sure the game deserves it.
24
+
25
+ ## Framing rules — internalize before prompting
26
+
27
+ The HUD overlays a **live 3D scene**. The pixels between widgets show the game.
28
+
29
+ - **NEVER generate a fullscreen ambient background** (parchment sheet, paper,
30
+ fabric, wood board) as part of the HUD. Material aesthetics belong on the
31
+ widget FRAMES — the HP bar frame is parchment, the minimap ring is leather —
32
+ never on a screen-filling sprite.
33
+ - **4–7 widgets.** Fewer doesn't read as a HUD; more clutters the screen and
34
+ burns generations.
35
+ - **Every widget needs internal contrast** — a panel fill, outline stroke, or
36
+ semi-opaque backdrop — because the scene behind it might be a snowfield or a
37
+ torchlit dungeon. Pure-outline widgets over arbitrary scenes fail.
38
+ - **The mockup must be flat and head-on** — a 2D screen-space overlay, never
39
+ tilted, isometric, or in perspective. Angled panels have no clean silhouette
40
+ and deconstruct into skewed slices. If the mockup comes back tilted,
41
+ regenerate it before going further.
42
+
43
+ ## Style brief
44
+
45
+ Start from the game's ONE shared style brief (`$genex-threejs-game-ui`'s "Plan
46
+ the UI first" gate) — the HUD, menu, and loader consume the same brief; a HUD
47
+ styled in a second world is a bug. Expand it here with what sprites need:
48
+ materials, an explicit palette of **4–5 named hues**, weathering, line
49
+ treatment, atmosphere. Keep outline stroke width
50
+ consistent across widgets — per-widget stroke changes read as amateur fastest.
51
+ For **per-sprite prompts** (single-sprite regeneration, below) the brief must
52
+ end EXACTLY with:
53
+
54
+ > Sharp edge silhouette on a flat plain background for clean cutout. No drop shadows. No vignette. Subject centered.
55
+
56
+ That sentence is what gives clean cutouts; skip it and the sprites come back
57
+ with halos. (The Stage-1 mockup and Stage-2 sheet prompts do NOT use it — they
58
+ are full frames, not cutouts; the templates below handle that.)
59
+
60
+ ## Text ownership — classify every word before Stage 2
61
+
62
+ Walk every visible word, digit, suffix, and glyph in the mockup into three
63
+ buckets. The Stage-2 prompt KEEPS or REMOVES accordingly:
64
+
65
+ - **`baked_static`** — identity text fused into the art (a "SPECIAL" label
66
+ painted into a meter frame, carved scale numerals, engraved panel titles).
67
+ Stage 2 **keeps** it in the asset.
68
+ - **`runtime_static`** — fixed but editable/localizable labels ("AMMO"
69
+ captions, objective headers not fused with the frame). Stage 2 **removes**
70
+ it; you render it as DOM text.
71
+ - **`runtime_dynamic`** — anything driven by game state: values, timers, bar
72
+ fills, item icons, map content. Stage 2 **removes** it; the DOM renders it.
73
+
74
+ The test: "is this fixed identity artwork, or should runtime code own it?"
75
+ Baking a runtime value into a sprite is a failure; stripping a carved identity
76
+ label flattens the art.
77
+
78
+ ## The pipeline
79
+
80
+ Each step is one command; every `<...-url>` is the R2 URL the previous command
81
+ printed. Fill the two prompt templates —
82
+ [references/stage1-prompt-template.md](references/stage1-prompt-template.md)
83
+ and [references/stage2-prompt-template.md](references/stage2-prompt-template.md)
84
+ — before Stages 1 and 2.
85
+
86
+ **Order of work: kick Stage 1 off as your FIRST action after the UI plan
87
+ gate**, then write the widget layout and wiring code (placement, masked-fill
88
+ scaffolding, plain-CSS placeholder bars) while it renders — the CSS HUD keeps
89
+ the game playable until the sprites land. Generate 2 mockup candidates in one
90
+ call (`--candidates 2`) and pick the better one: a re-roll costs the whole
91
+ serial chain, a second candidate costs nothing extra in wall-clock.
92
+
93
+ ```bash
94
+ # Stage 1 — full-HUD mockup over a placeholder scene (the LAYOUT REFERENCE).
95
+ # Two candidates in one call; pick the better, save its URL:
96
+ npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high --candidates 2
97
+
98
+ # Stage 2 — deconstruct the mockup into an asset sheet on white:
99
+ npx genex image "<filled stage-2 prompt>" --edit <mockup-url> --quality high
100
+
101
+ # Stage 2b — ML-clean the sheet background (preserves anti-aliased edges and
102
+ # interior whites that a naive white-key would eat; the prompt is recorded, unused):
103
+ npx genex image "clean sheet" --clean <sheet-url>
104
+
105
+ # Stage 3 — extract individual transparent PNGs locally (free, no generation).
106
+ # Names in READING ORDER: top-to-bottom, then left-to-right within a band:
107
+ npx genex ui extract --in <cleaned-url> --out-dir public/assets/hud --names hp-frame,ammo-frame,minimap-ring,weapon-icon
108
+
109
+ # Progress masks — for every bar/meter with a generated frame. The Stage-2
110
+ # sheet holds a clean frame cell PLUS a same-size annotated duplicate with the
111
+ # fill zones painted pure green #00ff00; this converts the green into an alpha
112
+ # mask + the fillBox (FB) the DOM fill reads:
113
+ npx genex ui masks --in <cleaned-url> --out-dir public/assets/hud --pairs "hp:cx,cy,w,h:ax,ay,w,h:1"
114
+ # pairs = name : clean-cell crop (px) : annotated-cell crop (px) : expected green components
115
+
116
+ # Text colors — sample the mockup instead of free-picking from the palette:
117
+ npx genex ui text-color --in <mockup-url> --box x,y,w,h
118
+ ```
119
+
120
+ Why mockup-then-deconstruct instead of N per-sprite generations: one
121
+ generation produces a **matched set** (no style drift between widgets) and the
122
+ model composes the layout for you — the mockup is the ground-truth reference
123
+ you verify the final HUD against.
124
+
125
+ ### Sprite iteration
126
+
127
+ To fix ONE sprite, don't re-run the pipeline:
128
+
129
+ - **Regenerate it** in the house style:
130
+ `npx genex image "<style brief + sprite subject>" --quality medium --remove-bg`
131
+ — add `--bg-mode glyph` for digits and closed shapes (an `8` or `$` otherwise
132
+ comes back with its loops hollowed out). Use the mandatory cutout sentence.
133
+ - **Or region-edit the sheet**: `npx genex image "<targeted change>" --edit <sheet-url>`
134
+ then re-run `genex ui extract` — surgical, preserves everything else.
135
+ - `npx genex ui trim --in public/assets/hud/<sprite>.png` crops a sprite to
136
+ its alpha content and reports its real pixel dims — the source of truth for
137
+ the box↔art aspect rule below.
138
+
139
+ ## Composition rules (each one is a documented failure class)
140
+
141
+ - **Continuous fills = masked reveal.** HP, mana, fuel, XP, stamina, charge —
142
+ NEVER generate `*-fill.png` sprites, and NEVER use a naive `width`/`height`
143
+ percentage of the widget box (it drifts ~12% off the art's real channel:
144
+ empty at 25%, full by 75%). The frame stays a sprite; the fill is the
145
+ masked-fill DOM pattern below.
146
+ - **Minimap/radar interiors are an empty CSS disc** (`border-radius: 50%`,
147
+ dark background). Only the decorative ring frame is a sprite, prompted with
148
+ an explicitly **transparent center — no map, no terrain inside**. Gameplay
149
+ code paints blips into the disc at runtime; the model otherwise invents
150
+ garbled fake streets.
151
+ - **Dial needles = ONE up-pointing sprite + a computed rotation.** Never one
152
+ sprite per angle. `transform: rotate(<deg>)` with `transform-origin: 50% 100%`
153
+ (pivot at the needle base), sweeping `-135deg..135deg` for a classic dial:
154
+ `rotate(${-135 + 270 * clamp(v / max, 0, 1)}deg)`.
155
+ - **Segmented bars = 3 stacked divs**: (1) a full-width dark track, (2) the
156
+ solid scaling fill, (3) a fixed full-width notch overlay
157
+ (`repeating-linear-gradient(to right, transparent 0 9%, #1c1c24 9% 10%)`)
158
+ painted ON TOP. Never put the gradient on the scaling fill itself — its `%`
159
+ stops anchor to the fill's own width and the segments visibly compress as
160
+ the value drops.
161
+ - **Every numeric readout gets a backing plate** — a
162
+ `background: rgba(10, 8, 6, 0.6)` rounded div behind the text by default, an
163
+ extracted plate sprite when the art has one, or a deliberate skip documented
164
+ in a comment (`/* no plate — reads against <reason> */`). Bare digits look
165
+ fine over the dev background and vanish over a bright scene.
166
+
167
+ ## The masked-fill DOM pattern (the crown jewel)
168
+
169
+ Full annotated version with all direction variants and a worked HP-bar
170
+ example: [references/masked-fill.md](references/masked-fill.md). The compact
171
+ form:
172
+
173
+ ```html
174
+ <div class="widget" id="hud-hp"> <!-- root: overflow VISIBLE (glows may bleed) -->
175
+ <div class="hp-mask"> <!-- outer: the silhouette mask -->
176
+ <div class="hp-fill" data-fill data-fill-mask="hp-mask.png"
177
+ data-fill-box="0.0784,0.3469,0.9216,0.3605"
178
+ data-fill-from="left" data-fill-ratio="0.72"></div>
179
+ </div>
180
+ <img class="hp-frame" src="/assets/hud/hp-frame.png" alt="">
181
+ </div>
182
+ ```
183
+
184
+ ```css
185
+ .widget { position: absolute; overflow: visible; }
186
+ .hp-mask { position: absolute; inset: 0; overflow: hidden;
187
+ -webkit-mask-image: url(/assets/hud/hp-mask.png);
188
+ mask-image: url(/assets/hud/hp-mask.png);
189
+ -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
190
+ mask-repeat: no-repeat; }
191
+ .hp-fill { position: absolute; inset: 0; /* FULL-BOX gradient; the clip places the edge */
192
+ background: linear-gradient(180deg, #ef5b5b, #7a1d18); }
193
+ .hp-frame { position: absolute; inset: 0; width: 100%; height: 100%;
194
+ object-fit: fill; } /* frame paints on top */
195
+ ```
196
+
197
+ ```ts
198
+ // FB = the mask's fillBox from the `genex ui masks` JSON — the TRUE channel.
199
+ const FB = { x: 0.0784, y: 0.3469, w: 0.9216, h: 0.3605 };
200
+ const fill = document.querySelector<HTMLElement>("#hud-hp .hp-fill")!;
201
+
202
+ function setHp(hp: number, maxHp: number): void {
203
+ const ratio = Math.max(0, Math.min(1, hp / maxHp));
204
+ const cx = (FB.x + ratio * FB.w) * 100; // left-fill: edge at the CHANNEL level
205
+ fill.style.clipPath = `polygon(0% 0%, ${cx}% 0%, ${cx}% 100%, 0% 100%)`;
206
+ fill.dataset.fillRatio = String(ratio); // keeps the fill auditable
207
+ const low = hp / maxHp < 0.25; // the reactive element (see below)
208
+ fill.style.filter = low ? "drop-shadow(0 0 12px #ff2a36)" : "none";
209
+ }
210
+ ```
211
+
212
+ The structure, in one breath: the OUTER div carries the alpha mask
213
+ (`mask-image` + `mask-size: 100% 100%`, `overflow: hidden`) so the fill can
214
+ only exist inside the art's silhouette; the INNER div is a FULL-BOX gradient
215
+ whose `clip-path` places the leading edge at the **channel-relative** level
216
+ using `FB` — for a left fill the right edge sits at `(FB.x + ratio * FB.w) * 100%`,
217
+ for a bottom fill the top edge at `(FB.y + (1 - ratio) * FB.h) * 100%`. Stamp
218
+ `data-fill`, `data-fill-mask`, `data-fill-box`, `data-fill-from`, and
219
+ `data-fill-ratio` on the inner div — they make every fill auditable against
220
+ the mask JSON. The frame `<img>` sits on top with `object-fit: fill`, and the
221
+ widget root keeps `overflow: visible` so glow effects bleed past the art
222
+ instead of clipping to a hard square.
223
+
224
+ ## Reactivity — the juice floor
225
+
226
+ A live HUD moves; a static one is a mockup screenshot. The floor for THIS
227
+ skill (each is a few lines of plain JS + CSS):
228
+
229
+ - **Numbers tween, never snap.** Score/coins/ammo tick to the new value over
230
+ ~300 ms (`requestAnimationFrame` lerp on the displayed number).
231
+ - **Damage-chip bar**: behind the instant red fill, a white/pale "chip" layer
232
+ eases down ~600 ms later — the player reads exactly how much that hit cost.
233
+ (Two stacked fills in the same mask; the chip's `clip-path` lags the real
234
+ one via a delayed transition.)
235
+ - **A damage flash**: a screen-edge vignette pulse on hit (one full-screen
236
+ div, `opacity` keyframe), not a number cloud.
237
+ - **At least one threshold state** — a change of VISUAL, not just of fill:
238
+
239
+ ```ts
240
+ const low = hp / maxHp < 0.25;
241
+ el.style.color = low ? "#ff6b6b" : "#f4e3b8";
242
+ el.classList.toggle("pulse", low); // @keyframes pulse in your CSS + a transition
243
+ ```
244
+
245
+ - **Pickup pop**: the touched counter scales 1 → 1.15 → 1 (~150 ms) when it
246
+ gains.
247
+
248
+ Impact feedback beyond the HUD (hitstop, camera shake, difficulty ramp) is
249
+ `$genex-threejs-game-feel` — run its pass before calling the game done; the
250
+ HUD's reactive layer and that skill's feedback layer are designed together.
251
+
252
+ ## Placement
253
+
254
+ Position widgets by **anchor + reference-1920×1080 pixels**, scaled by
255
+ `viewportH / 1080` so the HUD holds its physical position on any aspect ratio:
256
+
257
+ ```ts
258
+ function layoutHud(): void {
259
+ const s = innerHeight / 1080;
260
+ const hp = document.getElementById("hud-hp")!;
261
+ hp.style.left = `${40 * s}px`;
262
+ hp.style.bottom = `${40 * s}px`;
263
+ hp.style.width = `${560 * s}px`;
264
+ // h derived from the frame art's REAL aspect (trim/extract sidecar dims), never eyeballed:
265
+ hp.style.height = `${Math.round(560 * (102 / 760)) * s}px`; // hp-frame.png is 760x102
266
+ }
267
+ layoutHud();
268
+ window.addEventListener("resize", layoutHud);
269
+ ```
270
+
271
+ Genre defaults (tweak per game): **hp** bottom-left `(40, -40)`; **ammo**
272
+ bottom-center; **crosshair** dead center, ~48px, aspect locked; **score**
273
+ top-center; **minimap** top-right, ~220px square.
274
+
275
+ Two hard rules:
276
+
277
+ - **The box aspect must match the frame PNG's aspect** (use the dims from the
278
+ `genex ui trim` output or the extract `.bbox.json` sidecar): pick ONE
279
+ dimension by design and COMPUTE the other. `object-fit: fill` on a
280
+ mismatched box squashes the art.
281
+ - **≥ 20px gaps** (reference pixels) between widgets sharing an anchor —
282
+ tighter visibly overlaps once sprite padding is counted.
283
+
284
+ ## Verify before calling it done
285
+
286
+ - **Open every extracted PNG and eyeball it** — right subject, right name.
287
+ The reading-order sort can mismap when rows are uneven; re-run `extract`
288
+ with corrected name order rather than regenerating anything.
289
+ - **Fills grow monotonically** — drive each fill through 0 → 50 → 100 and
290
+ confirm the reveal grows and stays inside the art's channel (never bulging
291
+ past the frame's track).
292
+ - **No text clips** — for every text node, `scrollWidth <= clientWidth`.
293
+ Display fonts run 25–50% wider than a naive estimate; widen the box or drop
294
+ the weight, don't shrink the font.
295
+ - **No widget overlaps** at the reference viewport, and none off-screen.
296
+ - **Screenshot-check over real gameplay** — bright AND dark scenes — with
297
+ `$genex-threejs-visual-validation`'s capture discipline. Judge the pixels,
298
+ not your intent.
299
+
300
+ ## Cost & latency honesty
301
+
302
+ A full HUD is **~10 image generations** (mockup + deconstruct + clean + a few
303
+ sprite re-rolls), a couple of minutes each at high quality — budget an hour
304
+ end to end, not five minutes. That fits comfortably inside the image rate
305
+ limit; local `genex ui` steps are free and instant. Two rules keep the clock
306
+ honest: the sprite pipeline runs WHILE you build (kick Stage 1 first, code
307
+ against CSS placeholders, swap sprites in as stages land — never make the
308
+ player-facing v0 wait on it), and candidates beat re-rolls (`--candidates 2`
309
+ on the mockup: choosing between two costs zero extra wall-clock; regenerating
310
+ after a bad single costs the whole chain).
311
+
312
+ ## Publish checklist
313
+
314
+ - **Extracted sprites and masks are LOCAL files in `public/assets/hud/`** —
315
+ unlike other generated assets, they ship with the build and belong in the
316
+ repo. Reference them by relative path (`/assets/hud/hp-frame.png`), which
317
+ resolves at the domain root in dev and published alike.
318
+ - Keep the mockup and cleaned-sheet **R2 URLs in a comment** near the HUD
319
+ code — sheet region-edits and re-extraction need them, and they're permanent.
320
+ - Every fill uses the masked reveal with a real `FB` from the masks JSON — no
321
+ naive percentage fills hiding anywhere (`data-fill-*` stamps make this
322
+ greppable).
323
+
324
+ ## Options
325
+
326
+ - `npx genex image` — `--size <WxH>` exact pixels (multiples of 16, each side
327
+ ≤ 3840, aspect at most 3:1); `--quality <low|medium|high>` (high for the
328
+ mockup/sheet, medium for single sprites); `--candidates <2|3|4>` several
329
+ variants in ONE call (use 2 for the mockup — pick the better, skip the
330
+ re-roll); `--edit <url>` image-to-image edit of an R2 URL; `--clean <url>`
331
+ background removal only; `--remove-bg` chains removal after a
332
+ generation/edit; `--bg-mode <sprite|glyph|sheet>` picks the removal model
333
+ (`glyph` for digits/closed shapes; `--clean` defaults to `sheet`);
334
+ `--no-wait` enqueue and continue — `npx genex wait <id>` picks the result
335
+ up later (safe to re-run; never creates a new generation).
336
+ - `npx genex ui extract --in <png|url> --out-dir <dir> --names a,b,c` — also
337
+ writes a `.bbox.json` sidecar per sprite; `--min-pixels <n>` lowers the
338
+ component threshold when a small sprite is missed.
339
+ - `npx genex ui masks --in <png|url> --out-dir <dir> --pairs "name:cx,cy,w,h:ax,ay,w,h:n"` —
340
+ emits `<name>-frame.png`, `<name>-mask.png`, an overlay, and the JSON with
341
+ `fillBox` (`FB`).
342
+ - `npx genex ui text-color --in <png|url> --box x,y,w,h` — deterministic
343
+ eyedropper: background/plate color + dark/light/chromatic ink candidates.
344
+ Use the returned hex verbatim for the DOM text over that region.
345
+ - `npx genex ui trim --in <png>` — crop to alpha content + report real dims.
346
+
347
+ ## Troubleshooting
348
+
349
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it
350
+ writes your `GENEX_TOKEN`).
351
+ - **"Prompt rejected"** — the provider's content-safety filter blocked the
352
+ prompt. Non-retryable; rewrite the wording.
353
+ - **Generation takes minutes** — normal at high quality; let the command wait
354
+ and print the URL (the next pipeline step needs it, and re-running creates —
355
+ and bills — a new generation).
356
+ - **A sprite got the wrong name** — extraction sorts by reading order
357
+ (top-to-bottom bands, left-to-right within a band); uneven rows can shift
358
+ the mapping. Re-run `extract` with the names reordered — no generation
359
+ needed.
360
+ - **A small element is missing from extraction** — it fell under the component
361
+ threshold; re-run with `--min-pixels 500`.
362
+ - **The fill drifts off the art's channel** — a naive `width`/`height` % crept
363
+ in, or `FB` doesn't match the mask. Use the clipPath formula with the
364
+ `fillBox` from the masks JSON, verbatim.
365
+ - **The fill never shows, at any ratio** — the frame PNG's channel interior is
366
+ opaque track art occluding the fill beneath it. Raise the mask div above the
367
+ frame (`z-index`); the mask keeps the fill inside the channel, so the rim
368
+ and baked labels are unaffected (see the z-order note in
369
+ [references/masked-fill.md](references/masked-fill.md)).
370
+ - **`genex ui masks` fails registration/coverage checks** — the annotated
371
+ duplicate drifted from the clean cell (different scale/position) or the
372
+ green isn't flat `#00ff00`. Region-edit the sheet (`--edit <sheet-url>`)
373
+ asking for a same-size duplicate with flat green fill zones, then re-run.
374
+ - **Digits or `$`/`%` come back hollow after `--remove-bg`** — closed loops
375
+ read as background to the default cutter. Regenerate with `--bg-mode glyph`.
376
+ - **A widget looks squashed** — the placement box aspect doesn't match the
377
+ frame PNG (`object-fit: fill` stretches). Get the real dims from
378
+ `genex ui trim` and compute the box from them.
379
+ - **The mockup came back tilted / in perspective** — it won't deconstruct
380
+ cleanly. Keep the flat-framing paragraph in the Stage-1 prompt and
381
+ regenerate before Stage 2.
@@ -0,0 +1,145 @@
1
+ # The masked-fill DOM pattern
2
+
3
+ Every continuous fill (bar, meter, orb, gauge) inside a generated frame is a
4
+ **masked directional reveal**: two nested divs plus the frame image. The mask
5
+ confines the fill to the art's exact silhouette; a `clip-path` on a full-box
6
+ inner div places the leading edge at the channel-relative level.
7
+
8
+ ## Why not `width: ratio * 100%`?
9
+
10
+ The art's fillable channel does not span the widget box — the frame's
11
+ ornament eats the ends. Scaling the fill's own width/height against the FULL
12
+ box drifts up to ~12% off the real channel (measured: reads empty at 25%,
13
+ full by 75%). The reveal edge must be placed at `ratio` along the **true
14
+ channel** — the `fillBox` (`FB`) that `npx genex ui masks` measures from the
15
+ mask's opaque pixels and prints in its JSON.
16
+
17
+ ## Structure
18
+
19
+ ```
20
+ widget root position:absolute; overflow: VISIBLE ← glows may bleed past the art
21
+ ├─ outer mask div inset:0; overflow:hidden; mask-image:url(<mask.png>); mask-size:100% 100%
22
+ │ └─ inner fill div inset:0; FULL-BOX gradient; clip-path places the edge; data-fill-* stamps
23
+ └─ frame <img> inset:0; width/height:100%; object-fit:fill ← painted on top
24
+ ```
25
+
26
+ Confinement is the outer div's job (`overflow: hidden` + the mask). The
27
+ widget **root stays `overflow: visible`** — otherwise any glow/halo that
28
+ blooms past the silhouette gets sliced to a hard square.
29
+
30
+ **Z-order depends on the frame art's channel.** "Frame on top" assumes the
31
+ frame PNG's fill channel is transparent or translucent glass. This pipeline's
32
+ deconstructed frames usually keep an **opaque** dark track texture inside the
33
+ channel — a fill underneath is completely occluded (the bar looks stuck at
34
+ empty no matter the ratio). In that case raise the mask div above the frame
35
+ (`z-index` on the outer mask div): the mask confines the fill to the channel
36
+ pixels, so the rim and any baked labels stay untouched. Check one extracted
37
+ frame's channel pixels before wiring; if you can't see the fill move, this
38
+ z-order is the first suspect.
39
+
40
+ ## The reveal formulas
41
+
42
+ `ratio = clamp(value / max, 0, 1)`. `FB = { x, y, w, h }` in 0..1 fractions
43
+ of the widget box (from the masks JSON). The edge coordinate is a percent of
44
+ the FULL box:
45
+
46
+ | `from` | leading edge | clipPath |
47
+ | --- | --- | --- |
48
+ | **left** | `cx = (FB.x + ratio * FB.w) * 100` | `polygon(0% 0%, cx% 0%, cx% 100%, 0% 100%)` |
49
+ | **right** | `cx = (FB.x + (1 - ratio) * FB.w) * 100` | `polygon(cx% 0%, 100% 0%, 100% 100%, cx% 100%)` |
50
+ | **bottom** | `cy = (FB.y + (1 - ratio) * FB.h) * 100` | `polygon(0% cy%, 100% cy%, 100% 100%, 0% 100%)` |
51
+ | **top** | `cy = (FB.y + ratio * FB.h) * 100` | `polygon(0% 0%, 100% 0%, 100% cy%, 0% cy%)` |
52
+
53
+ Left/right suit horizontal bars; bottom suits orbs and vertical gauges (the
54
+ liquid rises). For an orb whose empty top would show the scene through the
55
+ glass, add a dark cavity div (a circle at the `FB` box) BEHIND the fill so
56
+ the empty portion reads as deep glass rather than a hole.
57
+
58
+ ## The `data-fill-*` contract
59
+
60
+ Stamp these on the inner fill div, derived from the SAME variables that
61
+ drive the clip — they make every fill auditable against the masks JSON
62
+ (and greppable, so no naive percentage fill hides in the codebase):
63
+
64
+ | Attribute | Value |
65
+ | --- | --- |
66
+ | `data-fill` | present on every fill element |
67
+ | `data-fill-mask` | the mask PNG basename, e.g. `hp-mask.png` |
68
+ | `data-fill-box` | `` `${FB.x},${FB.y},${FB.w},${FB.h}` `` |
69
+ | `data-fill-from` | `left` \| `right` \| `top` \| `bottom` |
70
+ | `data-fill-ratio` | the current `ratio`, updated with the clip |
71
+
72
+ ## Worked example — an HP bar (left fill)
73
+
74
+ `genex ui masks --out-dir public/assets/hud` wrote `hp-frame.png` +
75
+ `hp-mask.png` and, beside them, `hp.annotated-progress.json` (one object per
76
+ pair; the run's index `annotated-progress.json` nests the same objects in a
77
+ `pairs` array). `fillBox` sits at the object's top level:
78
+
79
+ ```json
80
+ {
81
+ "input": "<cleaned-sheet-url>",
82
+ "pairs": [
83
+ {
84
+ "name": "hp",
85
+ "fillBox": { "x": 0.0784, "y": 0.3469, "w": 0.9216, "h": 0.3605 },
86
+ "mask": { "path": "public/assets/hud/hp-mask.png", "...": "..." },
87
+ "...": "..."
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ```html
94
+ <div class="widget" id="hud-hp">
95
+ <div class="fill-mask" style="-webkit-mask-image: url(/assets/hud/hp-mask.png); mask-image: url(/assets/hud/hp-mask.png);">
96
+ <div class="fill" data-fill data-fill-mask="hp-mask.png"
97
+ data-fill-box="0.0784,0.3469,0.9216,0.3605"
98
+ data-fill-from="left" data-fill-ratio="0.72"></div>
99
+ </div>
100
+ <img class="frame" src="/assets/hud/hp-frame.png" alt="">
101
+ </div>
102
+ ```
103
+
104
+ ```css
105
+ .widget { position: absolute; overflow: visible; }
106
+ .fill-mask {
107
+ position: absolute; inset: 0; overflow: hidden;
108
+ -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
109
+ -webkit-mask-position: center; mask-position: center;
110
+ -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
111
+ }
112
+ .fill {
113
+ position: absolute; inset: 0; /* FULL box — the clip does the work */
114
+ background: linear-gradient(180deg, #ef5b5b 0%, #c84134 60%, #7a1d18 100%);
115
+ transition: clip-path 0.2s ease-out; /* smooth damage ticks */
116
+ }
117
+ .fill.low { filter: drop-shadow(0 0 12px #ff2a36); animation: hud-pulse 1s ease-in-out infinite; }
118
+ .frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; }
119
+ @keyframes hud-pulse { 50% { opacity: 0.75; } }
120
+ ```
121
+
122
+ ```ts
123
+ // FB comes from the masks JSON — never eyeball it, never re-measure by hand.
124
+ const FB = { x: 0.0784, y: 0.3469, w: 0.9216, h: 0.3605 };
125
+ const fill = document.querySelector<HTMLElement>("#hud-hp .fill")!;
126
+
127
+ export function setHp(hp: number, maxHp: number): void {
128
+ const ratio = Math.max(0, Math.min(1, hp / maxHp));
129
+ const cx = (FB.x + ratio * FB.w) * 100;
130
+ fill.style.clipPath = `polygon(0% 0%, ${cx}% 0%, ${cx}% 100%, 0% 100%)`;
131
+ fill.dataset.fillRatio = String(ratio);
132
+ fill.classList.toggle("low", ratio < 0.25); // the reactive branch
133
+ }
134
+ ```
135
+
136
+ Cosmetics (glow, pulse, transition) live alongside the clip but must never
137
+ fight it — no `transform`, `width`, or `height` on the fill element; the
138
+ computed `clip-path` is the single source of the reveal.
139
+
140
+ ## Verification
141
+
142
+ Drive each fill through 0 → 50 → 100: it must read empty at 0, grow
143
+ monotonically, read full at 100, and never bulge past the frame's visible
144
+ track. If the edge lands off the channel, the `FB` in code doesn't match the
145
+ masks JSON — or a naive percentage fill crept back in.
@@ -0,0 +1,44 @@
1
+ # Stage 1 prompt template — full-HUD mockup
2
+
3
+ Fill the four placeholders, then pass the whole text as the prompt to
4
+ `npx genex image "<filled prompt>" --size 2560x1440 --quality high`.
5
+
6
+ Placeholders:
7
+
8
+ - `[GENRE_BRIEF]` — one-line genre description, anchored in a recognizable
9
+ register ("a dark gothic action RPG", "a sun-bleached rally racing game",
10
+ "a hand-painted storybook platformer").
11
+ - `[GAME_SCENE]` — what's behind the HUD ("a dark medieval cathedral interior
12
+ with candlelight"). Only the scene may show 3D depth — never the HUD.
13
+ - `[STYLE_BRIEF]` — your full 3–5 sentence style brief with the named 4–5 hue
14
+ palette. Do NOT append the per-sprite cutout sentence here — this is a full
15
+ frame, not a cutout.
16
+ - `[ELEMENT_LIST]` — a bullet list of your 4–7 chosen widgets with one-line
17
+ descriptions. List only asset-backed widgets; pure-geometry elements
18
+ (crosshairs, tick rails, plain shapes) are built in code and stay off the
19
+ list.
20
+
21
+ ## The template
22
+
23
+ ```
24
+ A screenshot of a complete game HUD for [GENRE_BRIEF]. The HUD is composited over a [GAME_SCENE] visible behind it.
25
+
26
+ Visual style: [STYLE_BRIEF]
27
+
28
+ The HUD includes these elements, arranged naturally as a real game would lay them out (you decide the layout — do not force a grid, place each element where it makes the HUD readable and combat-ready):
29
+
30
+ [ELEMENT_LIST]
31
+
32
+ This is a real in-game screenshot. Sharp detail on every UI element. No motion blur on the HUD. The HUD is clear and combat-readable. No watermarks. No external annotations.
33
+
34
+ CRITICAL — flat HUD framing: draw every HUD element flat and head-on, parallel to the screen plane, like a 2D overlay painted directly onto the display (orthographic / screen-space UI). The HUD must NOT be tilted, angled, skewed, rotated in 3D, shown in perspective, or made to recede into depth — no isometric interface, no vanishing point on the panels, no 3D-extruded or floating-at-an-angle widgets, no curved/wrapped screen. ONLY the game scene behind the HUD may show 3D depth and perspective; the HUD layer itself is a flat 2D plane with square-on, axis-aligned edges, so each widget can be cleanly cut out as a flat sprite.
35
+ ```
36
+
37
+ Never delete the flat-framing paragraph — a tilted or perspective mockup does
38
+ not deconstruct cleanly (angled panel edges have no clean silhouette, so
39
+ Stage 2 produces sliced, skewed cutouts). If the mockup comes back tilted,
40
+ regenerate it before proceeding; do not try to salvage it downstream.
41
+
42
+ The mockup is the **layout reference**: save its URL, compare the finished
43
+ HUD against it, and sample runtime text colors from it with
44
+ `npx genex ui text-color`.