@ericrisco/rsc 3.1.3 → 3.3.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/README.md +8 -6
- package/manifest.json +43 -69
- package/package.json +6 -1
- package/scripts/design-upstream.js +92 -0
- package/scripts/doctor.js +0 -24
- package/scripts/lib/default-skill-floor.js +5 -1
- package/scripts/lib/domains.js +2 -2
- package/scripts/lib/recommend.js +1 -3
- package/scripts/lib/retired-skills.js +7 -1
- package/skills/ai-media/SKILL.md +2 -2
- package/skills/design/IMPECCABLE-LICENSE.txt +191 -0
- package/skills/design/SKILL.md +79 -328
- package/skills/design/THIRD_PARTY.md +26 -0
- package/skills/design/engine/LICENSE +21 -0
- package/skills/design/engine/RUN.md +515 -0
- package/skills/design/engine/UPSTREAM.json +4 -0
- package/skills/design/engine/assets/app.css +216 -0
- package/skills/design/engine/assets/base.css +105 -0
- package/skills/design/engine/assets/base.js +37 -0
- package/skills/design/engine/assets/charts.js +617 -0
- package/skills/design/engine/assets/scenes/stage.css +28 -0
- package/skills/design/engine/assets/scenes/stage.js +133 -0
- package/skills/design/engine/library/app.json +1327 -0
- package/skills/design/engine/library/directions.json +1078 -0
- package/skills/design/engine/library/heroes.json +1086 -0
- package/skills/design/engine/library/motions.json +92 -0
- package/skills/design/engine/library/palettes.json +1485 -0
- package/skills/design/engine/library/photos.json +7240 -0
- package/skills/design/engine/library/sections.json +4429 -0
- package/skills/design/engine/library/sites.json +8350 -0
- package/skills/design/engine/library/type.json +2024 -0
- package/skills/design/engine/references/app-screens.md +1199 -0
- package/skills/design/engine/references/colour.md +141 -0
- package/skills/design/engine/references/copy.md +177 -0
- package/skills/design/engine/references/critique.md +182 -0
- package/skills/design/engine/references/imagery.md +791 -0
- package/skills/design/engine/references/layout.md +292 -0
- package/skills/design/engine/references/motion.md +980 -0
- package/skills/design/engine/references/research.md +356 -0
- package/skills/design/engine/references/scenes.md +174 -0
- package/skills/design/engine/references/tells.md +115 -0
- package/skills/design/engine/references/type.md +170 -0
- package/skills/design/engine/scripts/assets.mjs +865 -0
- package/skills/design/engine/scripts/draw.mjs +584 -0
- package/skills/design/engine/scripts/inspect.mjs +303 -0
- package/skills/design/engine/scripts/lib/app-draw.mjs +203 -0
- package/skills/design/engine/scripts/lib/cdp.mjs +248 -0
- package/skills/design/engine/scripts/lib/probe.mjs +909 -0
- package/skills/design/engine/scripts/lib/rules.mjs +243 -0
- package/skills/design/engine/scripts/lint.mjs +119 -0
- package/skills/design/engine/scripts/scenes.mjs +124 -0
- package/skills/design/evals/cases.yaml +35 -46
- package/skills/design/references/design-md.md +49 -0
- package/skills/design/references/finish-review.md +46 -0
- package/skills/design/references/modes.md +51 -0
- package/skills/expo/SKILL.md +3 -4
- package/skills/marketing/references/landing-copy.md +2 -2
- package/skills/react/SKILL.md +2 -2
- package/skills/react-native/SKILL.md +3 -3
- package/skills/scroll-film/SKILL.md +220 -0
- package/skills/scroll-film/assets/page-template.html +128 -0
- package/skills/scroll-film/assets/scrub-engine.js +352 -0
- package/skills/scroll-film/evals/README.md +26 -0
- package/skills/scroll-film/evals/cases.yaml +81 -0
- package/skills/scroll-film/references/chain.md +218 -0
- package/skills/scroll-film/references/engine.md +132 -0
- package/skills/scroll-film/scripts/verify.sh +158 -0
- package/skills/swift-ios/SKILL.md +1 -1
- package/skills/ui-engineering/SKILL.md +3 -4
- package/skills/ui-engineering/evals/cases.yaml +2 -2
- package/skills/variant-explorer/SKILL.md +4 -4
- package/skills/variant-explorer/evals/cases.yaml +1 -1
- package/targets/clone-bootstrap.mjs +1 -1
- package/scripts/lib/design-identity.js +0 -117
- package/scripts/lib/starting-point.js +0 -314
- package/skills/design/evals/README.md +0 -53
- package/skills/design/references/ai-tells.md +0 -75
- package/skills/design/references/brand-grounding.md +0 -197
- package/skills/design/references/copywriting-frameworks.md +0 -138
- package/skills/design/references/design-systems.md +0 -49
- package/skills/design/references/inspiration-sources.md +0 -96
- package/skills/design/references/landing-anatomy-and-cro.md +0 -202
- package/skills/design/references/motion-and-interaction.md +0 -228
- package/skills/design/references/research-method.md +0 -154
- package/skills/design/references/signature-and-craft.md +0 -148
- package/skills/design/references/starting-point.md +0 -75
- package/skills/design/references/trends-2026.md +0 -80
- package/skills/design/references/visual-system.md +0 -237
- package/skills/design/scripts/verify.sh +0 -357
- package/skills/design-dna/PROMPT.md +0 -211
- package/skills/design-dna/SKILL.md +0 -162
- package/skills/design-dna/design-dna.schema.json +0 -230
- package/skills/design-dna/evals/README.md +0 -64
- package/skills/design-dna/evals/cases.yaml +0 -68
- package/skills/design-dna/references/method.md +0 -436
- package/skills/design-dna/scripts/emit.py +0 -164
- package/skills/design-dna/templates/PROMPT.md.tmpl +0 -42
- package/skills/design-dna/templates/SKILL.md.tmpl +0 -42
- package/skills/design-dna/templates/check.py.tmpl +0 -143
- package/skills/design-loop/SKILL.md +0 -160
- package/skills/design-loop/evals/README.md +0 -56
- package/skills/design-loop/evals/cases.yaml +0 -60
- package/skills/motion-craft/SKILL.md +0 -125
- package/skills/motion-craft/evals/cases.yaml +0 -38
- package/skills/motion-craft/scripts/verify.sh +0 -75
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Building the chain — prompts, probe, batch loops
|
|
2
|
+
|
|
3
|
+
Everything the body of `scroll-film` points at for sections 1 to 4. Fill the slots; keep the
|
|
4
|
+
clauses marked **verbatim** exactly as written, because they are what the seams depend on.
|
|
5
|
+
|
|
6
|
+
## The brief to write down before anything renders
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
subject: "a bubble-tea brand, from the tea field to the last sip" # the user's words
|
|
10
|
+
brand: { name: "Pearl & Leaf", tone: "warm, playful" }
|
|
11
|
+
palette: { bg: "#F3EDE4", accent: "#6D55B8", others: ["#8FB98A", "#C88A5A", "#3A2E48"] }
|
|
12
|
+
look: clay-diorama # becomes the style preamble below
|
|
13
|
+
camera: dive-and-hop # walkthrough | fixed-angle | dive-and-hop → shape A, A, B
|
|
14
|
+
phones: true # true = a second, 9:16 chain
|
|
15
|
+
scenes: # 4–7, in camera order; the last carries the CTA
|
|
16
|
+
- { id: field, subject: "terraced tea field, pickers with baskets, a small drying shed", focus: "the drying racks" }
|
|
17
|
+
- { id: kitchen, subject: "a kitchen with copper pots of simmering tapioca pearls", focus: "the pearl pots" }
|
|
18
|
+
- { id: shop, subject: "the corner shop with a striped awning and a queue", focus: "the counter" }
|
|
19
|
+
model: { endpoint: "<from the probe>", seconds_leg: 8, seconds_hop: 5, price_per_second: "<read today>" }
|
|
20
|
+
estimate_usd: "<clips × seconds × price + 20%>"
|
|
21
|
+
approved: false # nothing renders until this is true
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Style preamble — one block, pasted unchanged into every still prompt
|
|
25
|
+
|
|
26
|
+
Write it once from the look the user picked. The background colour is the page background.
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Isometric miniature diorama on a plain solid #F3EDE4 background, a soft shadow beneath it.
|
|
30
|
+
Matte clay materials, rounded toy-like shapes, warm low studio light, tilt-shift depth of field.
|
|
31
|
+
Palette limited to #6D55B8, #8FB98A, #C88A5A, #3A2E48. Subject centred. No text, letters,
|
|
32
|
+
numbers or logos anywhere.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Other looks change the first two sentences only and keep the palette and no-text lines:
|
|
36
|
+
|
|
37
|
+
- **Paper cut:** "Layered paper-craft diorama, matte card, clean die-cut edges, thin shadows between layers."
|
|
38
|
+
- **Vinyl toy:** "Glossy vinyl-toy diorama, smooth plastic shading, soft rim light."
|
|
39
|
+
- **Night miniature:** "Miniature town at night, warm lit windows, neon signs, wet reflective streets."
|
|
40
|
+
- **Photoreal:** "Architectural photograph of one continuous space, wide lens, late-afternoon light, natural
|
|
41
|
+
materials, no people." Photoreal scenes are full-bleed (no floating island), the camera moves
|
|
42
|
+
through doorways and glass instead of opening a roof, and it suits shape A. Do not pass one
|
|
43
|
+
approved still as a style reference here: it clones the same room into every scene.
|
|
44
|
+
|
|
45
|
+
## Still prompt
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
<STYLE PREAMBLE>
|
|
49
|
+
Subject: <scene.subject>. Framed for a 16:9 picture with the focal point, <scene.focus>, in the centre.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Render every still from **one** source and model. Two image models drift in warmth and line,
|
|
53
|
+
and the chain inherits the drift.
|
|
54
|
+
- Prepare each still at the clip's aspect before it becomes a start image: 16:9 for desktop, and a
|
|
55
|
+
1080×1920 canvas in the background colour for the phone chain. Handed an off-aspect image, many
|
|
56
|
+
endpoints choose their own framing and frame 0 stops matching.
|
|
57
|
+
- Review the set before any video: same angle, same light, same palette. Re-roll the odd one out.
|
|
58
|
+
|
|
59
|
+
## Leg prompt — shape A
|
|
60
|
+
|
|
61
|
+
Start frame: the previous leg's real last frame (leg 0: the first still). **No end frame.**
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
One continuous camera move, no cuts. Continue the same slow, steady forward drift.
|
|
65
|
+
<MID-LEG MOVE, optional>. The camera travels into <scene.subject> toward <scene.focus>.
|
|
66
|
+
In the final second, settle into a slow, steady forward drift toward <where the next scene lies>.
|
|
67
|
+
<last two sentences of the style preamble>. Smooth, unhurried motion. No text.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The first and the last sentence of that block are **verbatim**: they are the handoff contract
|
|
71
|
+
that keeps velocity continuous across the seam.
|
|
72
|
+
|
|
73
|
+
### Mid-leg moves by concept
|
|
74
|
+
|
|
75
|
+
A reversal inside one leg is safe, since one render has no seam. Pick from the scene's own logic:
|
|
76
|
+
|
|
77
|
+
| Concept | Mid-leg move |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| Product, luxury | "circling the <hero object> in a slow half orbit, keeping it centred, then moving past it" |
|
|
80
|
+
| Real estate, hotels | "gliding through the doorway at eye height, rising gently as the room opens up" |
|
|
81
|
+
| Production, logistics | "tracking low and level alongside the <line>, foreground objects sliding past" |
|
|
82
|
+
| Travel, campus, outdoors | "climbing over the <terrain>, then swooping down toward the <next focus>" |
|
|
83
|
+
| Food, craft | "pushing in until the <craft moment> nearly fills the frame, then easing back" |
|
|
84
|
+
| The scene itself is the show | omit the slot: a plain forward drift is the safest leg |
|
|
85
|
+
|
|
86
|
+
Expressive moves raise the re-roll rate: the model can end mid-orbit instead of in a clean forward
|
|
87
|
+
drift. Budget one extra render for each expressive leg, and check its last frame before chaining.
|
|
88
|
+
|
|
89
|
+
### Fixed-angle clause
|
|
90
|
+
|
|
91
|
+
When the user picked "one fixed angle", put this in the mid-leg slot of **every** leg, **verbatim**:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
The camera holds the same high isometric angle the whole time, with no rotation, orbit or tilt.
|
|
95
|
+
It only travels straight and level while the world slides past beneath it.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Long legs drift the angle slightly. When checking each last frame, also check the view has not
|
|
99
|
+
turned; re-roll a leg that has.
|
|
100
|
+
|
|
101
|
+
## Dive and hop prompts — shape B
|
|
102
|
+
|
|
103
|
+
Dive. Start frame: the scene's still (the solid-background version, so the frame is full).
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
One continuous camera move, no cuts. Start high and far, looking down at the whole <scene.subject>
|
|
107
|
+
like a small model. The camera glides forward and descends toward <scene.focus>; as it closes in,
|
|
108
|
+
the roof lifts gently away to reveal the interior. <style tail>. Smooth, unhurried motion. No text.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For an open scene with no roof (a field, a square), swap the last clause for "the camera skims low
|
|
112
|
+
across <the scene> toward <scene.focus>".
|
|
113
|
+
|
|
114
|
+
Hop. Start frame: dive k's real last frame. End frame: dive k+1's real first frame.
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
One continuous camera move, no cuts. The camera rises up and out of <scene k>, climbs into the sky,
|
|
118
|
+
crosses the connected miniature world and arrives above <scene k+1>, starting to descend toward it.
|
|
119
|
+
<style tail>. Smooth, unhurried motion. No text.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For the hop into a hero-product finale, end it on "…and the world falls away toward a single giant
|
|
123
|
+
<product> floating in open <bg colour> space."
|
|
124
|
+
|
|
125
|
+
## Phone chain clause
|
|
126
|
+
|
|
127
|
+
Prepend to every leg, dive and hop of the 9:16 chain:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
Vertical composition: the scene centred with open <bg colour> space above and below it.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The phone chain hands off frames from its own renders only.
|
|
134
|
+
|
|
135
|
+
## The probe — one cheap clip before the batch
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
ssim() { ffmpeg -i "$1" -i "$2" -lavfi "[0]scale=960:540,setsar=1[a];[1]scale=960:540,setsar=1[b];[a][b]ssim" \
|
|
139
|
+
-f null - 2>&1 | grep -oE 'All:[0-9.]+'; }
|
|
140
|
+
ffmpeg -v error -y -i probe.mp4 -frames:v 2 -vsync 0 probe_%d.png
|
|
141
|
+
ssim probe_1.png still_01.png # frame 0 against the still it was given
|
|
142
|
+
ssim probe_1.png probe_2.png # the clip's own frame-to-frame step, the yardstick
|
|
143
|
+
# Hop duty: the last frame against the end frame it was given.
|
|
144
|
+
ffmpeg -v error -y -sseof -1 -i probe_hop.mp4 -update 1 probe_last.png
|
|
145
|
+
ssim probe_last.png still_02.png
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- Frame 0 against the still should score about as high as the clip's own frame step. Judge it
|
|
149
|
+
against that yardstick, never a fixed number: how alike two frames look depends on the picture.
|
|
150
|
+
On a soft landscape photo, a fresh render of the same scene still scored 0.89. Clearly below the
|
|
151
|
+
step means the input steered a new render instead of starting the clip: the endpoint cannot
|
|
152
|
+
hold a seam.
|
|
153
|
+
- The hop's last frame only has to land on the same composition. Look at it; a near miss is normal.
|
|
154
|
+
- Compare the bill for the probe with the advertised price before multiplying it by the chain.
|
|
155
|
+
- Re-run the probe whenever the endpoint's input schema changes. Catalogues gain and lose
|
|
156
|
+
start-frame support without notice.
|
|
157
|
+
|
|
158
|
+
## Batch loops
|
|
159
|
+
|
|
160
|
+
`make_clip` is the one function you write against the chosen endpoint (the call itself →
|
|
161
|
+
`../../fal/SKILL.md` or `../../replicate/SKILL.md`). Its contract: `make_clip out.mp4 prompt.txt
|
|
162
|
+
start.png [end.png]` renders one clip, downloads it at once (result URLs expire within hours) and
|
|
163
|
+
appends the billed cost to `costs.log`. Run the loops as a background job and poll; a clip takes
|
|
164
|
+
minutes. Keep them in a `bash` script: zsh arrays start at 1, and an inline loop in a zsh shell
|
|
165
|
+
pairs each hop with the wrong scene.
|
|
166
|
+
|
|
167
|
+
Shape A, one leg at a time:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
#!/bin/bash
|
|
171
|
+
set -e
|
|
172
|
+
SCENES="field kitchen shop"
|
|
173
|
+
start="stills/field.png"
|
|
174
|
+
for s in $SCENES; do
|
|
175
|
+
make_clip "legs/$s.mp4" "prompts/leg_$s.txt" "$start"
|
|
176
|
+
ffmpeg -v error -y -sseof -1 -i "legs/$s.mp4" -update 1 -q:v 2 "frames/${s}_last.png"
|
|
177
|
+
start="frames/${s}_last.png" # look at it before the next leg spends money
|
|
178
|
+
done
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Shape B, dives in parallel, then hops:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
#!/bin/bash
|
|
185
|
+
set -e
|
|
186
|
+
SCENES="field kitchen shop"
|
|
187
|
+
for s in $SCENES; do make_clip "dives/$s.mp4" "prompts/dive_$s.txt" "stills/$s.png" & done
|
|
188
|
+
wait
|
|
189
|
+
for s in $SCENES; do
|
|
190
|
+
ffmpeg -v error -y -i "dives/$s.mp4" -frames:v 1 -q:v 2 "frames/${s}_first.png"
|
|
191
|
+
ffmpeg -v error -y -sseof -1 -i "dives/$s.mp4" -update 1 -q:v 2 "frames/${s}_last.png"
|
|
192
|
+
done
|
|
193
|
+
prev=""; k=0
|
|
194
|
+
for s in $SCENES; do
|
|
195
|
+
if [ -n "$prev" ]; then
|
|
196
|
+
k=$((k + 1))
|
|
197
|
+
make_clip "hops/hop_$k.mp4" "prompts/hop_$k.txt" "frames/${prev}_last.png" "frames/${s}_first.png" &
|
|
198
|
+
fi
|
|
199
|
+
prev="$s"
|
|
200
|
+
done
|
|
201
|
+
wait
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Five or six renders at once is usually fine. Many more invites transient rate and credit errors:
|
|
205
|
+
re-roll the one that failed, never the batch.
|
|
206
|
+
|
|
207
|
+
## When a content filter blocks a harmless clip
|
|
208
|
+
|
|
209
|
+
Video filters flag ordinary interiors, mostly bedrooms, pools, spas and bars, from the prompt words
|
|
210
|
+
and from the frames themselves. In order:
|
|
211
|
+
|
|
212
|
+
1. Re-roll. The verdict is often not deterministic.
|
|
213
|
+
2. Take out the trigger words and add "empty, unoccupied, architectural".
|
|
214
|
+
3. Render that one clip on another model that also passed the probe, with the same start and end
|
|
215
|
+
frames. Its render character differs slightly, which a short hop behind the seam dissolve
|
|
216
|
+
usually hides.
|
|
217
|
+
4. Drop that hop. List the two dives next to each other in the engine config and it dissolves
|
|
218
|
+
between them directly. The film completes; that seam reads as a dissolve, not a flight.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# The scrub engine — options, wiring, pacing
|
|
2
|
+
|
|
3
|
+
`assets/scrub-engine.js` is plain JavaScript with no dependencies. Loaded as a script it defines
|
|
4
|
+
`window.ScrollFilm`; under a bundler it also exports the same object. `assets/page-template.html` is
|
|
5
|
+
a complete page that uses it.
|
|
6
|
+
|
|
7
|
+
## Mounting
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const film = ScrollFilm.mount(container, options);
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`container` is the element the film lives in, usually a `<section>`. The engine adds a sticky stage
|
|
14
|
+
(the clips and an overlay slot for your copy) and a spacer that gives the section its scroll length.
|
|
15
|
+
Content after the section scrolls normally once the film ends.
|
|
16
|
+
|
|
17
|
+
### `options`
|
|
18
|
+
|
|
19
|
+
| Option | Default | What it does |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `shots` | required | The clips in chain order (fields below). |
|
|
22
|
+
| `fade` | `0.1` | Width of the seam dissolve, in viewport heights. |
|
|
23
|
+
| `preload` | `1.5` | A clip is fetched when the scroll comes this many viewports from it. |
|
|
24
|
+
| `ease` | `0.2` | How fast the playhead catches up with the scroll each frame (1 = instantly). |
|
|
25
|
+
| `phoneWidth` | `860` | At or below this width, or on any touch-only device, the phone sources are used. |
|
|
26
|
+
| `stillsOnly` | `false` | Posters only, no video. Also forced by reduced motion and by Save-Data. |
|
|
27
|
+
| `listen` | `true` | `false` = the engine adds no scroll listener; call `film.update()` from your own scroll source. |
|
|
28
|
+
| `onChange` | none | Called with the state object whenever the position changes. |
|
|
29
|
+
|
|
30
|
+
### Fields of each shot
|
|
31
|
+
|
|
32
|
+
| Field | What it is |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `src` | The desktop clip (`-g 8` encode). |
|
|
35
|
+
| `srcMobile` | The phone clip (`-m.mp4`, 9:16, `-g 4`). Optional; falls back to `src`. |
|
|
36
|
+
| `poster` / `posterMobile` | The clip's first frame. Shown until the video paints, and as the fallback. |
|
|
37
|
+
| `length` | Scroll distance in viewport heights. More length, longer dwell. Default 1. |
|
|
38
|
+
| `hold` | 0 to 1. Slows the camera mid-shot and speeds it near the seams; the seam frames never move. Above 0.6 the stop starts to read as a stall. |
|
|
39
|
+
| `section` | Which copy block this clip belongs to. A dive and the hop that leaves it share a section. Defaults to the shot's index. |
|
|
40
|
+
|
|
41
|
+
### What `mount` returns
|
|
42
|
+
|
|
43
|
+
| Member | Use |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `overlay` | Append your copy here; it rides the sticky stage above the clips. |
|
|
46
|
+
| `stage` | The sticky element, for styling. |
|
|
47
|
+
| `stillsOnly` | Whether this visitor gets posters only. |
|
|
48
|
+
| `update()` | Re-read the scroll position. Needed only with `listen: false`. |
|
|
49
|
+
| `relayout()` | Recompute positions after content above the film changes height (a late banner, a font swap). |
|
|
50
|
+
| `jumpTo(section)` | Smooth-scroll to the middle of a section; instant under reduced motion. |
|
|
51
|
+
| `destroy()` | Remove listeners, cancel fetches, release the Blob URLs, remove the added nodes. |
|
|
52
|
+
|
|
53
|
+
### The state object passed to `onChange`
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
{ shot: 2, section: 1, sectionProgress: 0.42, progress: 0.37 }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`sectionProgress` runs 0 to 1 across every clip of that section. Fade a section's copy in over its
|
|
60
|
+
first 15% and out over its last 15%; let the first block start visible and the last one hold its
|
|
61
|
+
call to action. The template does exactly this.
|
|
62
|
+
|
|
63
|
+
## Pacing
|
|
64
|
+
|
|
65
|
+
- The hero and the finale earn more `length` (1.4 to 1.6) and a `hold` of 0.3 to 0.5, so the camera
|
|
66
|
+
settles while the copy is readable.
|
|
67
|
+
- Hops are transit: `length` 0.8 to 1.0, no `hold`.
|
|
68
|
+
- Expressive camera work belongs in the clip, restraint in the scroll mapping. Both compound.
|
|
69
|
+
|
|
70
|
+
## Theming and layout
|
|
71
|
+
|
|
72
|
+
- `--film-bg` is the colour behind the stage; set it to the scene background so posters and
|
|
73
|
+
dissolves sit on it. `--film-focus` (default `50% 45%`) is where `object-fit: cover` keeps the
|
|
74
|
+
subject when the viewport crops a clip.
|
|
75
|
+
- The engine's own CSS sits in `@layer scroll-film`, so any unlayered page rule wins without
|
|
76
|
+
specificity tricks.
|
|
77
|
+
- The stage is `position: sticky`. Any ancestor with `overflow: hidden` breaks it; use
|
|
78
|
+
`overflow-x: clip` on `body` if the page needs to stop horizontal overflow.
|
|
79
|
+
- The stage is `100lvh` tall: the film fills the screen when the URL bar is hidden, and the bottom
|
|
80
|
+
edge sits under the bar while it shows. Keep copy clear of the bottom with `svh` units and
|
|
81
|
+
`env(safe-area-inset-bottom)`, as the template does, and add `viewport-fit=cover` to the
|
|
82
|
+
viewport meta tag.
|
|
83
|
+
|
|
84
|
+
## Wiring it into a framework
|
|
85
|
+
|
|
86
|
+
React or Next.js, in a client component (the effect never runs on the server):
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
'use client';
|
|
90
|
+
import { useEffect, useRef } from 'react';
|
|
91
|
+
|
|
92
|
+
export function Film({ shots }: { shots: object[] }) {
|
|
93
|
+
const ref = useRef<HTMLElement>(null);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
let film: { destroy(): void } | undefined;
|
|
96
|
+
let cancelled = false;
|
|
97
|
+
import('./scrub-engine.js').then((mod) => {
|
|
98
|
+
const ScrollFilm = (mod as any).default ?? (window as any).ScrollFilm;
|
|
99
|
+
if (!cancelled && ref.current) film = ScrollFilm.mount(ref.current, { shots });
|
|
100
|
+
});
|
|
101
|
+
return () => { cancelled = true; film?.destroy(); };
|
|
102
|
+
}, [shots]);
|
|
103
|
+
return <section ref={ref} aria-label="The journey" />;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The `cancelled` flag matters: a component that unmounts before the import resolves would otherwise
|
|
108
|
+
mount a film nobody can destroy. Keep `shots` stable (a module constant or `useMemo`) or every
|
|
109
|
+
render rebuilds the film.
|
|
110
|
+
|
|
111
|
+
Vue: mount in `onMounted`, keep the returned object, call `destroy()` in `onBeforeUnmount`. A
|
|
112
|
+
server-rendered page in any other stack: serve the assets and add two `<script>` tags, as the
|
|
113
|
+
template does.
|
|
114
|
+
|
|
115
|
+
To drive it from a scroll source the page already owns:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
const film = ScrollFilm.mount(el, { shots, listen: false });
|
|
119
|
+
ScrollTrigger.create({ trigger: el, start: 'top top', end: 'bottom bottom', onUpdate: () => film.update() });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Accessibility and weight
|
|
123
|
+
|
|
124
|
+
- The clip layer is `aria-hidden`. Everything a visitor needs is in the copy, which is ordinary
|
|
125
|
+
HTML in reading order, available to screen readers and search engines with or without the film.
|
|
126
|
+
- The template adds a skip link past the film and, when keyboard focus enters a faded copy block,
|
|
127
|
+
scrolls to that block's section.
|
|
128
|
+
- Preload the first poster with `fetchpriority="high"`; it is the LCP element. Clips load by
|
|
129
|
+
proximity, so first paint costs one image and one or two clips, not the whole chain.
|
|
130
|
+
- Measure the chain before shipping (`du -ch assets/vid/*.mp4`). A 1080p clip of several seconds
|
|
131
|
+
at GOP 8 runs to megabytes, so a whole chain runs to tens of them. That is why the phone chain is
|
|
132
|
+
720 wide and why a clip is fetched only when the scroll gets near it.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# verify.sh — check a scroll-film clip chain before it ships. Read-only.
|
|
3
|
+
#
|
|
4
|
+
# bash verify.sh [--strict] [--gop N] [--min-ssim X] [--portrait] [--frames DIR] clip1.mp4 clip2.mp4 ...
|
|
5
|
+
#
|
|
6
|
+
# Pass the clips in chain order (the order the engine plays them). Per clip it checks the
|
|
7
|
+
# encode a scrubbed video needs: H.264 yuv420p, no audio track, the index (moov) ahead of the
|
|
8
|
+
# media so the first frame paints early, and a keyframe at least every N frames (default 8; use
|
|
9
|
+
# --gop 4 for phone encodes). Across the chain it checks every clip shares one size and frame
|
|
10
|
+
# rate, and scores each seam: the true last frame of clip i against the first frame of clip i+1.
|
|
11
|
+
#
|
|
12
|
+
# A seam is judged against the clips around it, not against a fixed number: how alike two
|
|
13
|
+
# frames look depends on the picture, so a soft landscape scores high even across a cut. The
|
|
14
|
+
# seam's SSIM must stay within --margin (default 0.03) of the ordinary frame-to-frame SSIM at
|
|
15
|
+
# the end of clip i and the start of clip i+1. A frame-locked seam looks like one more frame; a
|
|
16
|
+
# fresh render of the same scene drops below. --frames DIR writes each pair side by side.
|
|
17
|
+
#
|
|
18
|
+
# Warns by default and exits 0. --strict exits 1 on any warning. Exits 2 on bad usage.
|
|
19
|
+
# bash 3.2-safe: no associative arrays, no mapfile.
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
STRICT=0
|
|
24
|
+
GOP=8
|
|
25
|
+
MARGIN=0.03
|
|
26
|
+
PORTRAIT=0
|
|
27
|
+
FRAMES_DIR=""
|
|
28
|
+
CLIPS=()
|
|
29
|
+
|
|
30
|
+
while [ $# -gt 0 ]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
--strict) STRICT=1 ;;
|
|
33
|
+
--gop) shift; GOP="${1:-}" ;;
|
|
34
|
+
--margin) shift; MARGIN="${1:-}" ;;
|
|
35
|
+
--portrait) PORTRAIT=1 ;;
|
|
36
|
+
--frames) shift; FRAMES_DIR="${1:-}" ;;
|
|
37
|
+
-h|--help) sed -n '2,19p' "$0"; exit 0 ;;
|
|
38
|
+
-*) echo "verify.sh: unknown option $1" >&2; exit 2 ;;
|
|
39
|
+
*) CLIPS+=("$1") ;;
|
|
40
|
+
esac
|
|
41
|
+
shift
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
for tool in ffmpeg ffprobe awk; do
|
|
45
|
+
command -v "$tool" >/dev/null 2>&1 || { echo "verify.sh: $tool not found on PATH" >&2; exit 2; }
|
|
46
|
+
done
|
|
47
|
+
if [ "${#CLIPS[@]}" -eq 0 ]; then
|
|
48
|
+
echo "verify.sh: pass the clips in chain order (see --help)" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
WARNINGS=0
|
|
53
|
+
ok() { echo "ok $*"; }
|
|
54
|
+
warn() { echo "WARN $*"; WARNINGS=$((WARNINGS + 1)); }
|
|
55
|
+
|
|
56
|
+
TMP=$(mktemp -d "${TMPDIR:-/tmp}/scroll-film-verify.XXXXXX")
|
|
57
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
58
|
+
[ -n "$FRAMES_DIR" ] && mkdir -p "$FRAMES_DIR"
|
|
59
|
+
|
|
60
|
+
# Every decoded frame of the last second, one file each: the final file is the real last frame.
|
|
61
|
+
# (A plain -sseof seek with -frames:v 1 stops a few frames short and judges the wrong frame.)
|
|
62
|
+
# The two last and the two first frames give the clips' own frame-to-frame step at the seam.
|
|
63
|
+
edge_frames() { # clip index
|
|
64
|
+
mkdir -p "$TMP/tail_$2" "$TMP/head_$2"
|
|
65
|
+
ffmpeg -v error -y -sseof -1 -i "$1" -vsync 0 "$TMP/tail_$2/%04d.png"
|
|
66
|
+
ffmpeg -v error -y -i "$1" -frames:v 2 -vsync 0 "$TMP/head_$2/%04d.png"
|
|
67
|
+
ls "$TMP/tail_$2" | tail -2 > "$TMP/tail_$2.list"
|
|
68
|
+
}
|
|
69
|
+
ssim() { ffmpeg -v info -i "$1" -i "$2" -lavfi "[0][1]ssim" -f null - 2>&1 | grep -oE 'All:[0-9.]+' | tail -1 | cut -d: -f2; }
|
|
70
|
+
|
|
71
|
+
FIRST_SHAPE=""
|
|
72
|
+
i=0
|
|
73
|
+
for clip in "${CLIPS[@]}"; do
|
|
74
|
+
i=$((i + 1))
|
|
75
|
+
name=$(basename "$clip")
|
|
76
|
+
if [ ! -f "$clip" ]; then warn "$name: file not found"; continue; fi
|
|
77
|
+
|
|
78
|
+
video=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,pix_fmt,width,height,r_frame_rate -of csv=p=0 "$clip")
|
|
79
|
+
if [ -z "$video" ]; then warn "$name: no video stream"; continue; fi
|
|
80
|
+
codec=$(echo "$video" | cut -d, -f1)
|
|
81
|
+
width=$(echo "$video" | cut -d, -f2)
|
|
82
|
+
height=$(echo "$video" | cut -d, -f3)
|
|
83
|
+
pixfmt=$(echo "$video" | cut -d, -f4)
|
|
84
|
+
rate=$(echo "$video" | cut -d, -f5)
|
|
85
|
+
|
|
86
|
+
[ "$codec" = "h264" ] && ok "$name: h264" || warn "$name: codec $codec (h264 decodes and seeks everywhere)"
|
|
87
|
+
[ "$pixfmt" = "yuv420p" ] && ok "$name: yuv420p" || warn "$name: pixel format $pixfmt (Safari wants yuv420p)"
|
|
88
|
+
|
|
89
|
+
audio=$(ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 "$clip")
|
|
90
|
+
[ -z "$audio" ] && ok "$name: no audio track" || warn "$name: has an audio track (dead weight on a muted scrub; encode with -an)"
|
|
91
|
+
|
|
92
|
+
first_atom=$(ffprobe -v trace "$clip" 2>&1 | grep -oE "type:'(moov|mdat)'" | head -1)
|
|
93
|
+
[ "$first_atom" = "type:'moov'" ] && ok "$name: moov before mdat (faststart)" \
|
|
94
|
+
|| warn "$name: index after the media (encode with -movflags +faststart)"
|
|
95
|
+
|
|
96
|
+
max_gap=$(ffprobe -v error -select_streams v:0 -show_entries packet=flags -of csv=p=0 "$clip" \
|
|
97
|
+
| awk '{ n++; if (substr($0,1,1) == "K") { if (last && n - last > gap) gap = n - last; last = n } }
|
|
98
|
+
END { if (n - last + 1 > gap) gap = n - last + 1; print gap + 0 }')
|
|
99
|
+
if [ "$max_gap" -le "$GOP" ]; then ok "$name: keyframe every <= $max_gap frames"
|
|
100
|
+
else warn "$name: $max_gap frames between keyframes (> $GOP); every scrub seek decodes that many"; fi
|
|
101
|
+
|
|
102
|
+
if [ "$PORTRAIT" -eq 1 ]; then
|
|
103
|
+
[ "$height" -gt "$width" ] && ok "$name: portrait ${width}x${height}" \
|
|
104
|
+
|| warn "$name: ${width}x${height} is not portrait (the phone chain is rendered at 9:16, not cropped)"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
shape="${width}x${height}@${rate}"
|
|
108
|
+
if [ -z "$FIRST_SHAPE" ]; then FIRST_SHAPE="$shape"
|
|
109
|
+
elif [ "$shape" != "$FIRST_SHAPE" ]; then
|
|
110
|
+
warn "$name: $shape differs from the first clip's $FIRST_SHAPE (one model and one encode for the whole chain)"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
edge_frames "$clip" "$i"
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
n=${#CLIPS[@]}
|
|
117
|
+
s=1
|
|
118
|
+
while [ "$s" -lt "$n" ]; do
|
|
119
|
+
next=$((s + 1))
|
|
120
|
+
label="seam $s→$next ($(basename "${CLIPS[$((s - 1))]}") → $(basename "${CLIPS[$s]}"))"
|
|
121
|
+
a_prev=""; a=""; b="$TMP/head_$next/0001.png"; b_next="$TMP/head_$next/0002.png"
|
|
122
|
+
if [ -s "$TMP/tail_$s.list" ]; then
|
|
123
|
+
a="$TMP/tail_$s/$(tail -1 "$TMP/tail_$s.list")"
|
|
124
|
+
[ "$(wc -l < "$TMP/tail_$s.list")" -ge 2 ] && a_prev="$TMP/tail_$s/$(head -1 "$TMP/tail_$s.list")"
|
|
125
|
+
fi
|
|
126
|
+
if [ -z "$a" ] || [ ! -f "$a" ] || [ ! -f "$b" ]; then
|
|
127
|
+
warn "$label: could not extract both frames"
|
|
128
|
+
else
|
|
129
|
+
score=$(ssim "$a" "$b")
|
|
130
|
+
step_out=""; step_in=""
|
|
131
|
+
[ -n "$a_prev" ] && step_out=$(ssim "$a_prev" "$a")
|
|
132
|
+
[ -f "$b_next" ] && step_in=$(ssim "$b" "$b_next")
|
|
133
|
+
step=$(echo "$step_out $step_in" | awk '{ m = ""; for (k = 1; k <= NF; k++) if (m == "" || $k < m) m = $k; print m }')
|
|
134
|
+
if [ -z "$score" ]; then
|
|
135
|
+
warn "$label: frames could not be compared (different sizes?)"
|
|
136
|
+
elif [ -z "$step" ]; then
|
|
137
|
+
warn "$label: SSIM $score, but the clips are too short to measure their own frame step; judge it by eye"
|
|
138
|
+
elif awk -v s="$score" -v t="$step" -v m="$MARGIN" 'BEGIN { exit !(s >= t - m) }'; then
|
|
139
|
+
ok "$label: SSIM $score (frame to frame inside the clips: $step)"
|
|
140
|
+
else
|
|
141
|
+
warn "$label: SSIM $score against $step frame to frame inside the clips; look at it. A different picture means the handoff used the wrong frame"
|
|
142
|
+
fi
|
|
143
|
+
if [ -n "$FRAMES_DIR" ]; then
|
|
144
|
+
ffmpeg -v error -y -i "$a" -i "$b" -filter_complex hstack "$FRAMES_DIR/seam_$(printf '%02d' "$s").png" \
|
|
145
|
+
&& echo " side by side: $FRAMES_DIR/seam_$(printf '%02d' "$s").png"
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
s=$next
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
echo
|
|
152
|
+
if [ "$WARNINGS" -eq 0 ]; then
|
|
153
|
+
echo "RESULT: PASS ($n clip(s), $((n - 1)) seam(s))"
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
echo "RESULT: $WARNINGS warning(s)"
|
|
157
|
+
[ "$STRICT" -eq 1 ] && exit 1
|
|
158
|
+
exit 0
|
|
@@ -13,7 +13,7 @@ origin: risco
|
|
|
13
13
|
app lifecycle, Xcode, iOS APIs, App Store shipping. Language-level Swift — Swift 6 data-race safety
|
|
14
14
|
and approachable concurrency, actors and task groups, `some` vs `any`, ARC and performance, Swift
|
|
15
15
|
Testing, macros — is this skill's own ground. A concurrency error, a hang, a data race or a
|
|
16
|
-
retain cycle belongs here
|
|
16
|
+
retain cycle belongs here, and so do interface feel, springs and materials on iOS.
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
Native Apple-platform apps: the code is `.swift`, the UI is SwiftUI, and the compiler enforces actor isolation. If the answer is a `@MainActor`, `@Observable`, `async let`, `#expect`, `NavigationStack`, or `@Model`, you are in the right place.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: ui-engineering
|
|
3
3
|
description: "Use when the question is how interface should be BUILT — component boundaries, state that belongs in the URL vs the component, loading/empty/error states, forms, optimistic updates, build vs adopt. NOT the visual system (`design`), NOT framework mechanics (`react`/`nextjs`)."
|
|
4
4
|
tags: [interface, frontend, architecture, components, state, craft]
|
|
5
|
-
recommends: [design
|
|
5
|
+
recommends: [design]
|
|
6
6
|
profiles: [core, full]
|
|
7
7
|
origin: risco
|
|
8
8
|
---
|
|
@@ -108,7 +108,6 @@ The split is not about difficulty, it is about **who is the authority on the beh
|
|
|
108
108
|
|
|
109
109
|
## See Also
|
|
110
110
|
|
|
111
|
-
- `../design/SKILL.md` — the visual system these decisions serve
|
|
112
|
-
-
|
|
113
|
-
- `../design-loop/SKILL.md` — the graded critique that judges the result.
|
|
111
|
+
- `../design/SKILL.md` — the visual system these decisions serve, its motion layer, and the
|
|
112
|
+
browser-measured critique that judges the result.
|
|
114
113
|
- `../accessibility/SKILL.md` — the ground under focus, semantics and forms.
|
|
@@ -22,10 +22,10 @@ should_not_trigger:
|
|
|
22
22
|
route_to: "react"
|
|
23
23
|
why: "Framework mechanics. This skill decides what to build, not how the framework behaves."
|
|
24
24
|
- prompt: "The dropdown animation feels cheap, what duration should it use?"
|
|
25
|
-
route_to: "
|
|
25
|
+
route_to: "design"
|
|
26
26
|
why: "Once the component exists, how it moves is motion craft."
|
|
27
27
|
- prompt: "Grade this dashboard and tell me if it is actually good."
|
|
28
|
-
route_to: "design
|
|
28
|
+
route_to: "design"
|
|
29
29
|
why: "Judging a finished screen is the critique loop, not the build decisions."
|
|
30
30
|
- prompt: "Set up the Next.js App Router with server actions for this project."
|
|
31
31
|
route_to: "nextjs"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: variant-explorer
|
|
3
|
-
description: "Use when someone wants OPTIONS rather than an answer — 'show me a few versions', picking between approaches by seeing them. Genuinely different candidates, cheap to throw away, trade-off named. NOT the critique that judges one (`design
|
|
3
|
+
description: "Use when someone wants OPTIONS rather than an answer — 'show me a few versions', picking between approaches by seeing them. Genuinely different candidates, cheap to throw away, trade-off named. NOT the critique that judges one (`design`), NOT the spec (`specify`)."
|
|
4
4
|
tags: [prototyping, divergence, options, exploration, decision]
|
|
5
|
-
recommends: [design
|
|
5
|
+
recommends: [design]
|
|
6
6
|
profiles: [full]
|
|
7
7
|
origin: risco
|
|
8
8
|
---
|
|
@@ -84,10 +84,10 @@ handing the work back. Say which one you would pick and why, and make it easy to
|
|
|
84
84
|
| Present five or six to be thorough | Past three, they stop being compared and start being skimmed. |
|
|
85
85
|
| Give options with no recommendation | Standing back is not neutrality; it hands the work back to the person who asked. |
|
|
86
86
|
| Explore a decision that is cheap to reverse | Just ship one. Divergence is for what is expensive to unwind. |
|
|
87
|
-
| Keep iterating on the winner here | Once one is chosen, refining it is `../design
|
|
87
|
+
| Keep iterating on the winner here | Once one is chosen, refining it is `../design/SKILL.md` or the relevant build skill. |
|
|
88
88
|
|
|
89
89
|
## See Also
|
|
90
90
|
|
|
91
|
-
- `../design
|
|
91
|
+
- `../design/SKILL.md` — the graded critique that takes over once a direction is chosen.
|
|
92
92
|
- `../specify/SKILL.md` — when what is missing is an agreed WHAT/WHY, not options.
|
|
93
93
|
- `../design/SKILL.md` — building the chosen direction properly.
|
|
@@ -16,7 +16,7 @@ should_trigger:
|
|
|
16
16
|
|
|
17
17
|
should_not_trigger:
|
|
18
18
|
- prompt: "Review this checkout screen and grade it."
|
|
19
|
-
route_to: "design
|
|
19
|
+
route_to: "design"
|
|
20
20
|
why: "Judging one finished thing is critique, the opposite of divergence."
|
|
21
21
|
- prompt: "We need to agree what this feature is for before anyone builds it."
|
|
22
22
|
route_to: "specify"
|
|
@@ -95,7 +95,7 @@ export function readManifest(root) {
|
|
|
95
95
|
const ids = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && ID.test(x)) : []);
|
|
96
96
|
// Retired skills read as their successors. Canonical map: scripts/lib/retired-skills.js — inlined
|
|
97
97
|
// because this file cannot import anything, and tests/retired-skills.test.js holds the two equal.
|
|
98
|
-
const RETIRED = { eli5: 'orient', 'show-me': 'orient', bro: 'unslop' };
|
|
98
|
+
const RETIRED = { eli5: 'orient', 'show-me': 'orient', bro: 'unslop', 'design-loop': 'design', 'design-dna': 'design', 'motion-craft': 'design' };
|
|
99
99
|
const skills = [...new Set(ids(parsed.skills).map((id) => (Object.hasOwn(RETIRED, id) ? RETIRED[id] : id)))];
|
|
100
100
|
const own = ids(parsed.ownSkills);
|
|
101
101
|
// A manifest with nothing declared is not a clone waiting to be equipped; there is nothing to offer.
|