howone 0.1.38 → 0.1.39
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 +1 -1
- package/templates/vite/.howone/skills/web-shader-extractor/SKILL.md +123 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/capture-backends.md +201 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/evidence-policy.md +93 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/operating-contract.md +82 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/qa-failure-policy.md +100 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/recon-kernel.md +206 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/replay-policy.md +145 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/shaders-com.md +212 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/source-analysis.md +112 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/surface-discovery.md +113 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/target-lock.md +205 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/three-shader-reconstruction.md +155 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/tool-capability-matrix.md +57 -0
- package/templates/vite/.howone/skills/web-shader-extractor/references/unicorn-studio.md +387 -0
- package/templates/vite/.howone/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs +178 -0
- package/templates/vite/.howone/skills/web-shader-extractor/scripts/scan-bundle.sh +80 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/extraction-report.md +41 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/known-gaps.md +14 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/qa-report.md +74 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/replay-manifest.json +147 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/run-state.json +75 -0
- package/templates/vite/.howone/skills/web-shader-extractor/templates/scout-card.json +106 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Unicorn Studio Adapter
|
|
2
|
+
|
|
3
|
+
Use this adapter only after the target surface group has been attributed to a Unicorn Studio embed, remix, or runtime renderer.
|
|
4
|
+
Public APIs and bundle details change.
|
|
5
|
+
Verify keys, shader variable names, and initialization routes against the current target.
|
|
6
|
+
|
|
7
|
+
Stability: volatile. Treat endpoint names, variable names, keys, shader templates, and build-specific mappings as hints until verified on the current target.
|
|
8
|
+
|
|
9
|
+
Unicorn Studio is a no-code WebGL design tool. Observed runtimes use curtains.js-style multi-pass WebGL rendering and may load scene definitions from Firestore, GCS, or CDN embed data.
|
|
10
|
+
|
|
11
|
+
## Adapter Interface
|
|
12
|
+
|
|
13
|
+
- `detect(context)`: target surface is created by Unicorn runtime, target embed/project ID matches the canvas, or public definition maps to the target visual.
|
|
14
|
+
- `preferredEvidence(context)`: published embed JSON or public remix/version definition, then target-bound runtime/source route.
|
|
15
|
+
- `capture(context)`: scene/history data, compiled shaders or shader templates, layer graph, FBO chain, assets/fonts/textures, timing rules, output composite.
|
|
16
|
+
- `replay(context)`: `SOURCE_REPLAY` from embed/version data when possible; `PIPELINE_REPLAY` if only compiled shaders and runtime graph are available.
|
|
17
|
+
- `validationHints(context)`: element/child effect FBO chains, transparent `showBg=0`, frame-based `uTime`, glyph atlas compatibility, WebGL environment issues.
|
|
18
|
+
- `fallback(context)`: dynamic bundle slice around target effects, then frame capture.
|
|
19
|
+
|
|
20
|
+
## Recognition Signals
|
|
21
|
+
|
|
22
|
+
- URL patterns such as `unicorn.studio/remix/{remixId}` or `unicorn.studio/edit/{designId}`
|
|
23
|
+
- meta tags or runtime clues referencing Unicorn Studio
|
|
24
|
+
- embed SDK paths such as `unicornStudio-*.js`
|
|
25
|
+
- application bundle paths such as `index-*.js` or the current dynamic chunk graph
|
|
26
|
+
- data attributes such as `data-us-project` or `data-us-project-src`
|
|
27
|
+
|
|
28
|
+
## Data Acquisition Routes
|
|
29
|
+
|
|
30
|
+
### Route 1: Firestore REST API
|
|
31
|
+
|
|
32
|
+
Use this route for remix URLs when public Firestore access is available. Extract Firebase/public API config dynamically from the current frontend bundle.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Example discovery pattern; validate against the current site.
|
|
36
|
+
curl -s https://www.unicorn.studio/ | grep -oP 'apiKey:"[^"]+"' | head -1
|
|
37
|
+
|
|
38
|
+
API_KEY="<extract dynamically from current public source>"
|
|
39
|
+
PROJECT="unicorn-studio"
|
|
40
|
+
|
|
41
|
+
# Step 1: Fetch remix metadata, including versionId, designId, and creator metadata.
|
|
42
|
+
curl -s "https://firestore.googleapis.com/v1/projects/$PROJECT/databases/(default)/documents/remixes/{REMIX_ID}?key=$API_KEY"
|
|
43
|
+
|
|
44
|
+
# Step 2: Fetch version data with layers, parameters, and texture references.
|
|
45
|
+
curl -s "https://firestore.googleapis.com/v1/projects/$PROJECT/databases/(default)/documents/versions/{VERSION_ID}?key=$API_KEY"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Route 2: GCS/CDN Embed Data
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Non-Pro projects observed in older samples.
|
|
52
|
+
curl -s "https://storage.googleapis.com/unicornstudio-production/embeds/{DESIGN_ID}"
|
|
53
|
+
|
|
54
|
+
# Pro projects observed in older samples.
|
|
55
|
+
curl -s "https://assets.unicorn.studio/embeds/{DESIGN_ID}"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Embed JSON may look like `{ options: {...}, layers/history: [...], modules: [...] }` and may include `compiledFragmentShaders[]` and `compiledVertexShaders[]`.
|
|
59
|
+
|
|
60
|
+
### Route 3: Inline Page JSON
|
|
61
|
+
|
|
62
|
+
Unicorn Studio embeds may use `data-us-project` or `data-us-project-src` HTML attributes. The SDK `init()` path scans these attributes and loads the corresponding project.
|
|
63
|
+
|
|
64
|
+
## Identify The Data Shape First
|
|
65
|
+
|
|
66
|
+
Unicorn Studio data commonly appears in at least two shapes.
|
|
67
|
+
|
|
68
|
+
### 1. Embed/export scene
|
|
69
|
+
|
|
70
|
+
- Usually the final scene JSON passed to `addScene()`.
|
|
71
|
+
- Often already includes `compiledFragmentShaders[]` and `compiledVertexShaders[]`.
|
|
72
|
+
- Can often be replayed through the embed runtime.
|
|
73
|
+
|
|
74
|
+
### 2. Editor/version history
|
|
75
|
+
|
|
76
|
+
- Commonly from Firestore `versions/{id}` `history`.
|
|
77
|
+
- Represents editor source layer data.
|
|
78
|
+
- Do not pass it directly to `addScene()`.
|
|
79
|
+
|
|
80
|
+
If `history` is misused as an embed scene, common symptoms include:
|
|
81
|
+
|
|
82
|
+
- `Plane: No fragment shader provided, will use a default one`
|
|
83
|
+
- `Plane: No vertex shader provided, will use a default one`
|
|
84
|
+
- `No composite shader data for element`
|
|
85
|
+
- the canvas exists but renders black or default layers only
|
|
86
|
+
|
|
87
|
+
## Firestore Collections
|
|
88
|
+
|
|
89
|
+
| Collection | Purpose | Key fields |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `designs` | design metadata | creatorId, name, versionId, hasEmbed |
|
|
92
|
+
| `versions` | core version data | history[], options |
|
|
93
|
+
| `remixes` | remixable design metadata | designId, versionId, creatorId, thumbnail |
|
|
94
|
+
|
|
95
|
+
Firestore REST wraps values as `{stringValue, integerValue, arrayValue, mapValue, ...}`. Parse recursively before implementation.
|
|
96
|
+
|
|
97
|
+
Each `history` entry is usually one layer:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
layerType: "effect" | "text" | "image" | "model" | "shape"
|
|
101
|
+
type: effect type such as gradient, noiseFill, sdf_shape, glyphDither, bloomFast
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Common layer parameters:
|
|
105
|
+
|
|
106
|
+
- `pos`, `scale`, `speed`, `opacity`, `blendMode`
|
|
107
|
+
- `trackMouse`, `trackAxes`, `mouseMomentum`
|
|
108
|
+
- `parentLayer`: UUID or false
|
|
109
|
+
- `breakpoints[]` for responsive behavior
|
|
110
|
+
- `states` for appear, scroll, hover, and mousemove animation
|
|
111
|
+
- `customFragmentShaders[]`, `customVertexShaders[]`; often empty when built-in effects are used
|
|
112
|
+
|
|
113
|
+
## Initialization Strategy
|
|
114
|
+
|
|
115
|
+
If the source is Firestore `version/history`, prefer the site's own initialization chain instead of forcing public embed APIs.
|
|
116
|
+
|
|
117
|
+
Typical observed sequence:
|
|
118
|
+
|
|
119
|
+
1. `unpackageHistory()` or `unpackVersion()`
|
|
120
|
+
2. `createFontScript()`
|
|
121
|
+
3. `createCurtains()`
|
|
122
|
+
4. `handleItemPlanes()`
|
|
123
|
+
5. `fullRedraw()`
|
|
124
|
+
|
|
125
|
+
If the bundle contains a Remix/Preview component, trace that path before relying on UMD/SDK documentation.
|
|
126
|
+
|
|
127
|
+
## Resource Localization
|
|
128
|
+
|
|
129
|
+
- Download image, font, and texture resources locally when permitted.
|
|
130
|
+
- Rewrite `history` `src` and `fontCSS.src` to local paths for local replay.
|
|
131
|
+
- Normalize numeric-key maps into arrays when needed before writing local JSON.
|
|
132
|
+
|
|
133
|
+
## Effect-Type Parameters
|
|
134
|
+
|
|
135
|
+
| Effect | Key parameters |
|
|
136
|
+
|---|---|
|
|
137
|
+
| gradient | fill[], stops[], gradientType, gradientAngle, wrap |
|
|
138
|
+
| noiseFill | noiseType, turbulence, color1, color2, colorPhase, chroma, direction |
|
|
139
|
+
| sdf_shape | shape 0-22, refraction, extrude, smoothing, axis, animationDirection, lightPosition |
|
|
140
|
+
| glyphDither | characters, glyphSet, scale, gamma, monochrome, sprite atlas texture |
|
|
141
|
+
| bloomFast | amount, intensity, exposure, tint |
|
|
142
|
+
|
|
143
|
+
## Shader Extraction
|
|
144
|
+
|
|
145
|
+
Some embed SDKs do not contain GLSL shader code.
|
|
146
|
+
Shader templates may live in the main application bundle or a dynamic chunk.
|
|
147
|
+
They may be compiled into embed JSON through a processing pipeline.
|
|
148
|
+
Use the current target's network and source evidence.
|
|
149
|
+
|
|
150
|
+
### Shader Template Locations
|
|
151
|
+
|
|
152
|
+
Observed older bundles used string literals identified by minified variables:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
Effect name observed variable
|
|
156
|
+
glyphDither X$ fragment
|
|
157
|
+
noiseFill WY fragment
|
|
158
|
+
sdf_shape XY fragment
|
|
159
|
+
gradient eX fragment
|
|
160
|
+
bloomFast Hj fragment
|
|
161
|
+
generic vertex ye
|
|
162
|
+
gradient vertex ko
|
|
163
|
+
composite frag Uz
|
|
164
|
+
composite vertex Nz
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Variable names change with builds. Search by semantic shader features rather than fixed names.
|
|
168
|
+
|
|
169
|
+
### Template Variables
|
|
170
|
+
|
|
171
|
+
Shader templates may include `${variable}` placeholders replaced at compile time:
|
|
172
|
+
|
|
173
|
+
| Placeholder | Observed meaning |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `${fe}` | mask-related uniform declarations |
|
|
176
|
+
| `${Vt}` | layer blending helpers such as `applyLayerMix`, `applyLayerMixAlpha`, `applyLayerMixClip` |
|
|
177
|
+
| `${gt}` | PCG hash / random helpers such as `pcg2d`, `randFibo` |
|
|
178
|
+
| `${ht}` | blend mode helpers |
|
|
179
|
+
| `${pe("var")}` | mask application plus `fragColor` output |
|
|
180
|
+
| `${wf}` | BCC noise derivatives / OpenSimplex2S |
|
|
181
|
+
| `${Aa}` | Perlin noise helpers |
|
|
182
|
+
| `${yr}` | debanding dither |
|
|
183
|
+
| `${cm}` | gradient color/stop uniform declarations |
|
|
184
|
+
| `${xz}` | Gaussian weights for bloom blur |
|
|
185
|
+
|
|
186
|
+
### Observed Compile Pipeline
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
1. Fz(): replace uniform values with constants
|
|
190
|
+
2. Dz(): handle gradient color counts and switch-case pruning
|
|
191
|
+
3. Mz(): evaluate constant switches / dead code elimination
|
|
192
|
+
4. Rz(): handle #ifelseopen / #ifelseclose blocks
|
|
193
|
+
5. Iz(): remove unused functions
|
|
194
|
+
6. Cz(): remove unused uniform declarations
|
|
195
|
+
7. Bp(): strip comments and normalize whitespace
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Render Pipeline
|
|
199
|
+
|
|
200
|
+
Replay must restore the pipeline, not only individual shaders.
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
curtains.js-style WebGL2 renderer
|
|
204
|
+
|- each effect layer = one Plane + independent FBO
|
|
205
|
+
|- layers render linearly by renderOrder, each plane reading previous FBO as uTexture
|
|
206
|
+
|- Element layers (shape/text/image) plus child effects form a render group:
|
|
207
|
+
| 1. Element plane renders first -> FBO_elem
|
|
208
|
+
| 2. Child effects render in effects[] order -> FBO_child1, FBO_child2, ...
|
|
209
|
+
| 3. Composite plane alpha-blends child output back into the background scene
|
|
210
|
+
|- standalone post effects with parentLayer=false process the global scene
|
|
211
|
+
`- final plane outputs directly to canvas without an FBO
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Element + Child Effect FBO Chain
|
|
215
|
+
|
|
216
|
+
Shape group example:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
FBO_before ----------------------------------------------------.
|
|
220
|
+
|
|
|
221
|
+
Shape plane -> FBO_shape |
|
|
222
|
+
| |
|
|
223
|
+
Child noiseFill -> FBO_noise (uBgTexture = FBO_shape) |
|
|
224
|
+
| |
|
|
225
|
+
Child sdf_shape -> FBO_sdf (uTexture = FBO_noise) |
|
|
226
|
+
| showBg=0 means outside shape = vec4(0) transparent |
|
|
227
|
+
| |
|
|
228
|
+
Composite plane -> FBO_result |
|
|
229
|
+
uTexture = FBO_sdf |
|
|
230
|
+
uBgTexture = FBO_before ---------------------------------'
|
|
231
|
+
output = alpha_blend(fg, bg) = fg + bg * (1.0 - fg.a)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Child Effect Association
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
// Element effects[] lists child-effect parentLayer UUIDs.
|
|
238
|
+
shape.effects = ["e270a7cd-...", "fb591190-..."];
|
|
239
|
+
|
|
240
|
+
// Each child effect references one parent element UUID.
|
|
241
|
+
noiseFill.parentLayer = "e270a7cd-..."; // effects[0]
|
|
242
|
+
sdf_shape.parentLayer = "fb591190-..."; // effects[1]
|
|
243
|
+
|
|
244
|
+
// Embed runtime lookup pattern:
|
|
245
|
+
getChildEffectItems() {
|
|
246
|
+
return this.effects.map(uuid =>
|
|
247
|
+
state.layers.find(l => l.parentLayer === uuid)
|
|
248
|
+
).filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Time Base
|
|
253
|
+
|
|
254
|
+
Observed embed SDKs may use frame accumulation for `uTime`, not seconds:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
// In setEffectPlaneUniforms():
|
|
258
|
+
t.uniforms.time.value += speed * 60 / this.fps;
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
At 60 fps, `uTime += speed` each frame. After one second, `uTime = speed * 60`.
|
|
262
|
+
|
|
263
|
+
| Effect layer | speed | uTime after one second |
|
|
264
|
+
|---|---:|---:|
|
|
265
|
+
| noiseFill | 0.25 | 15 |
|
|
266
|
+
| sdf_shape | 0.5 | 30 |
|
|
267
|
+
| gradient | 0.25 | 15 |
|
|
268
|
+
|
|
269
|
+
Replay must match the target time model:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
// Correct for frame-accumulation samples:
|
|
273
|
+
uni1f(prog, 'uTime', elapsedSeconds * speed * 60);
|
|
274
|
+
|
|
275
|
+
// Wrong for those samples:
|
|
276
|
+
uni1f(prog, 'uTime', elapsedSeconds);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## `showBg`
|
|
280
|
+
|
|
281
|
+
- `showBg=0`: ray miss outputs `vec4(0)`, fully transparent rather than opaque black.
|
|
282
|
+
- `showBg=1`: ray miss samples `uTexture` / `uBgTexture`, showing background content.
|
|
283
|
+
|
|
284
|
+
If `showBg=0` is incorrectly replayed as `vec4(0,0,0,1)`, composite alpha blending is blocked and the lower layer disappears. Ensure `alpha=0`.
|
|
285
|
+
|
|
286
|
+
## Replay Strategy
|
|
287
|
+
|
|
288
|
+
1. Simple 2D post effects such as `glyphDither` or `bloomFast`: native WebGL2 fullscreen quad can be suitable.
|
|
289
|
+
2. Generative effects such as `noiseFill` or `gradient`: native WebGL2 can be suitable when facts are complete.
|
|
290
|
+
3. 3D SDF such as `sdf_shape`: native WebGL2 raymarching can be suitable.
|
|
291
|
+
4. Complex scenes: preserve a multi-pass FBO graph.
|
|
292
|
+
5. Text layers: render text with Canvas2D and upload it as a WebGL texture.
|
|
293
|
+
|
|
294
|
+
## Playwright Verification
|
|
295
|
+
|
|
296
|
+
Headless Playwright may not have a usable WebGL environment. Treat these symptoms as environment issues before changing extraction logic:
|
|
297
|
+
|
|
298
|
+
- `Renderer: WebGL context could not be created`
|
|
299
|
+
- `0 canvas(es) found`
|
|
300
|
+
- `Error creating Curtains instance`
|
|
301
|
+
- black screenshot
|
|
302
|
+
|
|
303
|
+
Try SwiftShader-backed validation when needed:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
--use-angle=swiftshader
|
|
307
|
+
--use-gl=angle
|
|
308
|
+
--enable-unsafe-swiftshader
|
|
309
|
+
--ignore-gpu-blocklist
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Suggested validation order:
|
|
313
|
+
|
|
314
|
+
1. Determine whether console output shows shader/runtime errors or WebGL context creation failure.
|
|
315
|
+
2. Confirm that the DOM actually creates a `canvas`.
|
|
316
|
+
3. Capture a SwiftShader screenshot.
|
|
317
|
+
4. Compare composition against the source thumbnail or first viewport screenshot.
|
|
318
|
+
|
|
319
|
+
## Glyph Atlas Generation
|
|
320
|
+
|
|
321
|
+
Original glyph atlases may be base64 PNGs with cross-browser compatibility issues. Canvas2D generation can be more portable:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
function createGlyphAtlas(chars, size = 40) {
|
|
325
|
+
const canvas = document.createElement('canvas');
|
|
326
|
+
canvas.width = size * chars.length;
|
|
327
|
+
canvas.height = size;
|
|
328
|
+
const ctx = canvas.getContext('2d');
|
|
329
|
+
ctx.fillStyle = '#000';
|
|
330
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
331
|
+
ctx.fillStyle = '#fff';
|
|
332
|
+
ctx.font = `bold ${size * 0.8}px monospace`;
|
|
333
|
+
ctx.textAlign = 'center';
|
|
334
|
+
ctx.textBaseline = 'middle';
|
|
335
|
+
for (let i = 0; i < chars.length; i++) {
|
|
336
|
+
ctx.fillText(chars[i], size * i + size / 2, size / 2);
|
|
337
|
+
}
|
|
338
|
+
return canvas;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Upload the generated atlas with:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Cloud Functions
|
|
349
|
+
|
|
350
|
+
Observed endpoint base:
|
|
351
|
+
|
|
352
|
+
```text
|
|
353
|
+
https://us-central1-unicorn-studio.cloudfunctions.net/
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Examples:
|
|
357
|
+
|
|
358
|
+
- `publishEmbedTest`: publish/update embed; requires authentication
|
|
359
|
+
- `getUserIdByUsername`: username to userId
|
|
360
|
+
- `handleVideos`, `handleModels`, `handleImages`: asset handling
|
|
361
|
+
- `generateImprovedMSDF`: MSDF text rendering
|
|
362
|
+
- `generateDepthMap`: depth map generation
|
|
363
|
+
- `copyRemixAssets`: remix asset copy
|
|
364
|
+
|
|
365
|
+
Do not call authenticated endpoints unless the user has explicitly authorized the session and the action is within scope.
|
|
366
|
+
|
|
367
|
+
## Example Extraction Skeleton
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# 1. Extract remix ID from URL.
|
|
371
|
+
REMIX_ID="QZxhNFb1X1OaUqaJLT9S"
|
|
372
|
+
|
|
373
|
+
# 2. Fetch remix metadata.
|
|
374
|
+
curl -s "https://firestore.googleapis.com/v1/projects/unicorn-studio/databases/(default)/documents/remixes/$REMIX_ID?key=$API_KEY" > remix.json
|
|
375
|
+
|
|
376
|
+
# 3. Extract versionId.
|
|
377
|
+
VERSION_ID=$(python3 -c "import json; print(json.load(open('remix.json'))['fields']['versionId']['stringValue'])")
|
|
378
|
+
|
|
379
|
+
# 4. Fetch version data.
|
|
380
|
+
curl -s "https://firestore.googleapis.com/v1/projects/unicorn-studio/databases/(default)/documents/versions/$VERSION_ID?key=$API_KEY" > version.json
|
|
381
|
+
|
|
382
|
+
# 5. Recursively parse Firestore REST wrappers into layer and parameter data.
|
|
383
|
+
|
|
384
|
+
# 6. Fetch the current target-bound app bundle or source slice and locate shader templates by effect type.
|
|
385
|
+
|
|
386
|
+
# 7. Combine parameters and shader templates into the chosen replay route.
|
|
387
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Optional inventory helper that uses an explicitly installed Playwright runtime
|
|
4
|
+
* to record DOM/canvas/network observations. It does not perform
|
|
5
|
+
* Surface Attribution, Target Lock, Replay Ready, or QA gates, and it does not
|
|
6
|
+
* install dependencies automatically.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node fetch-rendered-dom.mjs <URL> [outDir]
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* npm install playwright
|
|
13
|
+
* npx playwright install chromium
|
|
14
|
+
*
|
|
15
|
+
* Inventory outputs to outDir (default ./web-shader-extractor-scout):
|
|
16
|
+
* dom.html - full rendered HTML
|
|
17
|
+
* canvas-info.json - all canvas element metadata
|
|
18
|
+
* webgl-info.json - lightweight metadata for discovered canvases
|
|
19
|
+
* console.log - page console output
|
|
20
|
+
* screenshot.png - viewport screenshot
|
|
21
|
+
* network.json - runtime-loaded JS/resource/asset URLs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
25
|
+
import { join, resolve } from 'path';
|
|
26
|
+
|
|
27
|
+
const url = process.argv[2];
|
|
28
|
+
const outDir = resolve(process.argv[3] || 'web-shader-extractor-scout');
|
|
29
|
+
|
|
30
|
+
if (!url) {
|
|
31
|
+
console.error('Usage: node fetch-rendered-dom.mjs <URL> [outDir]');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let chromium;
|
|
36
|
+
try {
|
|
37
|
+
({ chromium } = await import('playwright'));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Playwright is not available. Install it explicitly before using this optional scout script:');
|
|
40
|
+
console.error(' npm install playwright');
|
|
41
|
+
console.error(' npx playwright install chromium');
|
|
42
|
+
console.error(`Original error: ${error.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
mkdirSync(outDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const consoleLogs = [];
|
|
49
|
+
const networkRequests = [];
|
|
50
|
+
|
|
51
|
+
const browser = await chromium.launch({ headless: true });
|
|
52
|
+
const context = await browser.newContext({
|
|
53
|
+
viewport: { width: 1920, height: 1080 },
|
|
54
|
+
deviceScaleFactor: 2,
|
|
55
|
+
});
|
|
56
|
+
const page = await context.newPage();
|
|
57
|
+
|
|
58
|
+
// Capture console
|
|
59
|
+
page.on('console', msg => {
|
|
60
|
+
consoleLogs.push(`[${msg.type()}] ${msg.text()}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Capture network (JS, WASM, shader, media, image, and likely data files)
|
|
64
|
+
page.on('response', async response => {
|
|
65
|
+
const reqUrl = response.url();
|
|
66
|
+
const type = response.headers()['content-type'] || '';
|
|
67
|
+
if (/\.(js|mjs|wasm|bin|glsl|frag|vert|svg|png|jpe?g|webp|avif|gif|mp4|webm|ktx2?)(\?|$)/i.test(reqUrl) || type.includes('javascript') || type.startsWith('image/') || type.startsWith('video/')) {
|
|
68
|
+
networkRequests.push({
|
|
69
|
+
url: reqUrl,
|
|
70
|
+
status: response.status(),
|
|
71
|
+
type: type.split(';')[0],
|
|
72
|
+
size: parseInt(response.headers()['content-length'] || '0'),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
console.log(`Navigating to ${url} ...`);
|
|
79
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
80
|
+
|
|
81
|
+
// Wait for common renderer initialization.
|
|
82
|
+
await page.waitForTimeout(3000);
|
|
83
|
+
|
|
84
|
+
// 1. Full rendered DOM
|
|
85
|
+
const html = await page.content();
|
|
86
|
+
writeFileSync(join(outDir, 'dom.html'), html);
|
|
87
|
+
console.log(`dom.html - ${html.length} bytes`);
|
|
88
|
+
|
|
89
|
+
// 2. Surface info. This is inventory evidence, not target attribution.
|
|
90
|
+
const canvasInfo = await page.evaluate(() => {
|
|
91
|
+
return Array.from(document.querySelectorAll('canvas')).map((c, i) => {
|
|
92
|
+
const style = getComputedStyle(c);
|
|
93
|
+
const rect = c.getBoundingClientRect();
|
|
94
|
+
const parentStyle = c.parentElement ? getComputedStyle(c.parentElement) : null;
|
|
95
|
+
return {
|
|
96
|
+
index: i,
|
|
97
|
+
outerHTML: c.outerHTML.slice(0, 500),
|
|
98
|
+
width: c.width,
|
|
99
|
+
height: c.height,
|
|
100
|
+
clientWidth: c.clientWidth,
|
|
101
|
+
clientHeight: c.clientHeight,
|
|
102
|
+
boundingRect: {
|
|
103
|
+
x: rect.x,
|
|
104
|
+
y: rect.y,
|
|
105
|
+
width: rect.width,
|
|
106
|
+
height: rect.height,
|
|
107
|
+
top: rect.top,
|
|
108
|
+
left: rect.left,
|
|
109
|
+
right: rect.right,
|
|
110
|
+
bottom: rect.bottom,
|
|
111
|
+
},
|
|
112
|
+
dataEngine: c.dataset.engine || null,
|
|
113
|
+
dataRenderer: c.dataset.renderer || null,
|
|
114
|
+
id: c.id || null,
|
|
115
|
+
className: c.className || null,
|
|
116
|
+
parentTag: c.parentElement?.tagName || null,
|
|
117
|
+
parentClass: c.parentElement?.className?.slice(0, 100) || null,
|
|
118
|
+
style: {
|
|
119
|
+
display: style.display,
|
|
120
|
+
position: style.position,
|
|
121
|
+
opacity: style.opacity,
|
|
122
|
+
visibility: style.visibility,
|
|
123
|
+
pointerEvents: style.pointerEvents,
|
|
124
|
+
transform: style.transform,
|
|
125
|
+
zIndex: style.zIndex,
|
|
126
|
+
clipPath: style.clipPath,
|
|
127
|
+
maskImage: style.maskImage,
|
|
128
|
+
mixBlendMode: style.mixBlendMode,
|
|
129
|
+
},
|
|
130
|
+
parentStyle: parentStyle ? {
|
|
131
|
+
position: parentStyle.position,
|
|
132
|
+
overflow: parentStyle.overflow,
|
|
133
|
+
clipPath: parentStyle.clipPath,
|
|
134
|
+
maskImage: parentStyle.maskImage,
|
|
135
|
+
zIndex: parentStyle.zIndex,
|
|
136
|
+
} : null,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
writeFileSync(join(outDir, 'canvas-info.json'), JSON.stringify(canvasInfo, null, 2));
|
|
141
|
+
console.log(`canvas-info.json - ${canvasInfo.length} canvas(es) found`);
|
|
142
|
+
|
|
143
|
+
// 3. Lightweight canvas metadata. Do not create new contexts here.
|
|
144
|
+
const webglInfo = await page.evaluate(() => {
|
|
145
|
+
const canvases = Array.from(document.querySelectorAll('canvas'));
|
|
146
|
+
if (!canvases.length) return { error: 'no canvas found', canvases: [] };
|
|
147
|
+
return {
|
|
148
|
+
found: true,
|
|
149
|
+
canvases: canvases.map((canvas, index) => ({
|
|
150
|
+
index,
|
|
151
|
+
width: canvas.width,
|
|
152
|
+
height: canvas.height,
|
|
153
|
+
dataEngine: canvas.dataset.engine || null,
|
|
154
|
+
dataRenderer: canvas.dataset.renderer || null,
|
|
155
|
+
id: canvas.id || null,
|
|
156
|
+
className: canvas.className || null,
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
writeFileSync(join(outDir, 'webgl-info.json'), JSON.stringify(webglInfo, null, 2));
|
|
161
|
+
console.log(`webgl-info.json - ${JSON.stringify(webglInfo).slice(0, 100)}`);
|
|
162
|
+
|
|
163
|
+
// 4. Console logs
|
|
164
|
+
writeFileSync(join(outDir, 'console.log'), consoleLogs.join('\n'));
|
|
165
|
+
console.log(`console.log - ${consoleLogs.length} entries`);
|
|
166
|
+
|
|
167
|
+
// 5. Screenshot
|
|
168
|
+
await page.screenshot({ path: join(outDir, 'screenshot.png'), fullPage: false });
|
|
169
|
+
console.log(`screenshot.png - saved`);
|
|
170
|
+
|
|
171
|
+
// 6. Network requests
|
|
172
|
+
writeFileSync(join(outDir, 'network.json'), JSON.stringify(networkRequests, null, 2));
|
|
173
|
+
console.log(`network.json - ${networkRequests.length} JS/resource requests captured`);
|
|
174
|
+
|
|
175
|
+
console.log(`\nDone. Files saved to ${outDir}`);
|
|
176
|
+
} finally {
|
|
177
|
+
await browser.close();
|
|
178
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Scan JS bundle slice(s) for WebGL/shader keywords and report hypotheses.
|
|
3
|
+
# Usage: scan-bundle.sh <file1.js> [file2.js ...]
|
|
4
|
+
# Output: keyword counts + tech stack hypotheses. These are not target-bound conclusions.
|
|
5
|
+
|
|
6
|
+
set -eu
|
|
7
|
+
|
|
8
|
+
if [ $# -eq 0 ]; then
|
|
9
|
+
echo "Usage: scan-bundle.sh <file1.js> [file2.js ...]"
|
|
10
|
+
echo "Scans JS files for WebGL/shader keywords and produces hypotheses."
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
FILES=("$@")
|
|
15
|
+
|
|
16
|
+
echo "=== SHADER/WEBGL KEYWORD SCAN ==="
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
# Core GLSL keywords
|
|
20
|
+
echo "--- GLSL Keywords ---"
|
|
21
|
+
for kw in "gl_FragColor" "gl_Position" "gl_PointSize" "gl_PointCoord" \
|
|
22
|
+
"precision" "uniform" "varying" "attribute" \
|
|
23
|
+
"FRAGMENT_SHADER" "VERTEX_SHADER" "createShader" \
|
|
24
|
+
"sampler2D" "texture2D" "smoothstep" "discard"; do
|
|
25
|
+
count=$(grep -o "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
|
26
|
+
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
echo ""
|
|
30
|
+
echo "--- WebGL/Canvas Keywords ---"
|
|
31
|
+
for kw in "canvas" "webgl" "webgl2" "getContext" "shader" "glsl" \
|
|
32
|
+
"framebuffer" "renderbuffer" "drawArrays" "drawElements" \
|
|
33
|
+
"bufferData" "texImage2D" "POINTS"; do
|
|
34
|
+
count=$(grep -oi "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
|
35
|
+
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
echo ""
|
|
39
|
+
echo "--- Noise/Math Keywords ---"
|
|
40
|
+
for kw in "snoise" "simplex" "perlin" "noise" "PoissonDisk" "Poisson"; do
|
|
41
|
+
count=$(grep -o "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
|
42
|
+
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
echo ""
|
|
46
|
+
echo "=== TECH STACK HYPOTHESES (NOT TARGET LOCK) ==="
|
|
47
|
+
|
|
48
|
+
detect() {
|
|
49
|
+
local label="$1" pattern="$2"
|
|
50
|
+
local count
|
|
51
|
+
count=$(grep -oE "$pattern" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
|
52
|
+
[ "$count" -gt 0 ] && printf " %-25s %s hits (hypothesis)\n" "$label" "$count" || true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Frameworks (patterns specific enough to avoid false positives)
|
|
56
|
+
detect "Three.js" "THREE\.|WebGLRenderer|ShaderMaterial|BufferGeometry|PerspectiveCamera|OrthographicCamera"
|
|
57
|
+
detect "Three.js (minified)" "setRenderTarget|DataTexture|setPixelRatio|setClearColor"
|
|
58
|
+
detect "PixiJS" "PIXI\.|pixi\.js|PixiJS"
|
|
59
|
+
detect "Babylon.js" "BABYLON\.|babylonjs"
|
|
60
|
+
detect "Raw WebGL" "gl\.bindBuffer|gl\.bindTexture|gl\.useProgram|gl\.attachShader|gl\.linkProgram"
|
|
61
|
+
detect "Regl" "regl\(|regl\.frame|regl\.texture"
|
|
62
|
+
detect "OGL" "ogl\.|ogl/"
|
|
63
|
+
|
|
64
|
+
# Patterns. Generic RenderTarget hits are common post-processing evidence, not GPGPU.
|
|
65
|
+
detect "GPGPU/simulation clues" "ping[-_.]?pong|GPUComputationRenderer|DataTexture|feedback|simulation|compute"
|
|
66
|
+
detect "Particles" "gl_PointSize|gl_PointCoord|PointSize|particl"
|
|
67
|
+
detect "Post-processing" "EffectComposer|RenderPass|ShaderPass|postprocess"
|
|
68
|
+
detect "Ray marching" "rayMarch|sdSphere|sdBox|sdRoundBox"
|
|
69
|
+
detect "Instancing" "InstancedMesh|InstancedBufferGeometry|instanceMatrix"
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
echo "=== FILE SIZES ==="
|
|
73
|
+
for f in "${FILES[@]}"; do
|
|
74
|
+
size=$(wc -c < "$f" | tr -d ' ')
|
|
75
|
+
echo " $(basename "$f"): ${size} bytes"
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
echo ""
|
|
79
|
+
echo "Reminder: bind any hypothesis to the target surface group before using it as implementation evidence."
|
|
80
|
+
echo "GPGPU requires combined evidence such as feedback loops, ping-pong pairs, data-texture simulation, or compute-like update passes."
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Extraction Report
|
|
2
|
+
|
|
3
|
+
## Source
|
|
4
|
+
|
|
5
|
+
- URL:
|
|
6
|
+
- Captured at:
|
|
7
|
+
- Permission boundary:
|
|
8
|
+
|
|
9
|
+
## Target Visual
|
|
10
|
+
|
|
11
|
+
## Target Surface Group
|
|
12
|
+
|
|
13
|
+
## Evidence Summary
|
|
14
|
+
|
|
15
|
+
## Replay Route
|
|
16
|
+
|
|
17
|
+
`SOURCE_REPLAY | PIPELINE_REPLAY | BEHAVIOR_REBUILD`
|
|
18
|
+
|
|
19
|
+
## Captured Facts
|
|
20
|
+
|
|
21
|
+
- Surface:
|
|
22
|
+
- Runtime/backend:
|
|
23
|
+
- Render graph:
|
|
24
|
+
- Resources:
|
|
25
|
+
- Timing:
|
|
26
|
+
- Inputs:
|
|
27
|
+
- Output/composite:
|
|
28
|
+
|
|
29
|
+
## Baseline
|
|
30
|
+
|
|
31
|
+
- Path: `capture-baseline/`
|
|
32
|
+
- Status:
|
|
33
|
+
|
|
34
|
+
## Editable Project
|
|
35
|
+
|
|
36
|
+
- Path: `editable-project/`
|
|
37
|
+
- Status:
|
|
38
|
+
|
|
39
|
+
## Known Gaps
|
|
40
|
+
|
|
41
|
+
## Deferred Work
|