@genex-ai/cli-demo 0.47.0-dev.70 → 0.47.0-dev.74
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/README.md +25 -2
- package/dist/index.js +1158 -16
- package/package.json +4 -2
- package/templates/skills/genex-ai-hud/SKILL.md +381 -0
- package/templates/skills/genex-ai-hud/references/masked-fill.md +145 -0
- package/templates/skills/genex-ai-hud/references/stage1-prompt-template.md +44 -0
- package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +73 -0
- package/templates/skills/genex-ai-image/SKILL.md +14 -4
- package/templates/skills/genex-ai-menu/SKILL.md +291 -0
- package/templates/skills/genex-ai-video/SKILL.md +11 -4
- package/templates/skills/genex-getting-started/SKILL.md +5 -2
- package/templates/skills/genex-threejs-game-ui/SKILL.md +149 -18
- package/templates/skills/genex-threejs-game-ui/references/style-capsules.md +89 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +27 -1
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +37 -17
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +10 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.47.0-dev.
|
|
3
|
+
"version": "0.47.0-dev.74",
|
|
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@dev 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`.
|