howone 0.1.39 → 0.1.50
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-clone/LICENSE +21 -0
- package/templates/vite/.howone/skills/web-clone/README.md +179 -0
- package/templates/vite/.howone/skills/web-clone/SKILL.md +243 -0
- package/templates/vite/.howone/skills/web-clone/references/assessment.md +77 -0
- package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +46 -0
- package/templates/vite/.howone/skills/web-clone/references/deliverables.md +144 -0
- package/templates/vite/.howone/skills/web-clone/references/design-dna.md +125 -0
- package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +73 -0
- package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +31 -0
- package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +34 -0
- package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +72 -0
- package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +101 -0
- package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +151 -0
- package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +265 -0
- package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +214 -0
- package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +136 -0
- package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +314 -0
- package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +39 -0
- package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +121 -0
- package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +127 -0
- package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +235 -0
- package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +228 -0
- package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +112 -0
- package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +161 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# 产物规范与模板
|
|
2
|
+
|
|
3
|
+
每个复刻子项目(`~/projects/website-clones/<站名>-clone/`)的标准产物。
|
|
4
|
+
|
|
5
|
+
## NOTES.md(必须)
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# <站名> · 克隆笔记
|
|
9
|
+
|
|
10
|
+
## 源信息
|
|
11
|
+
- 原站 URL:
|
|
12
|
+
- 源码仓库:(如有)
|
|
13
|
+
- 原作者:
|
|
14
|
+
- 许可证: MIT / Apache / NONE / 专有 ← 必须核实,见 SKILL.md 许可表
|
|
15
|
+
- 致谢要求:
|
|
16
|
+
|
|
17
|
+
## 技术栈
|
|
18
|
+
- 框架 / 关键库 / Node 版本:
|
|
19
|
+
|
|
20
|
+
## 复刻前预判
|
|
21
|
+
- 复杂度等级: L1 / L2 / L3 / L4 / L5 / L6(见 web-clone skill 的 references/assessment.md)
|
|
22
|
+
- 推荐模式: 忠实复刻 / 视觉复刻 / 内容爆改 / 技术拆解
|
|
23
|
+
- 可高保真的部分:
|
|
24
|
+
- 需要近似或替代的部分:
|
|
25
|
+
- 不克隆的部分:
|
|
26
|
+
- 主要风险:
|
|
27
|
+
|
|
28
|
+
## 跑起来
|
|
29
|
+
\`\`\`bash
|
|
30
|
+
cd ~/projects/website-clones/<站名>-clone
|
|
31
|
+
# 单文件静态站: python3 -m http.server 8123
|
|
32
|
+
# 框架站: nvm use <ver> && npm install && npm run dev
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
## 改了什么(对照原版)
|
|
36
|
+
- 删了追踪脚本: ...(GA/gtag 行号)
|
|
37
|
+
- ...
|
|
38
|
+
|
|
39
|
+
## 原站 vs 克隆站
|
|
40
|
+
| 模块 | 原站表现 | 克隆实现 | 差异 / 取舍 | 证据 |
|
|
41
|
+
|---|---|---|---|---|
|
|
42
|
+
| 首屏 | | | | screenshot / file:line |
|
|
43
|
+
| 导航 | | | | |
|
|
44
|
+
| 核心动效 | | | | |
|
|
45
|
+
| 内容区块 | | | | |
|
|
46
|
+
| 移动端 | | | | |
|
|
47
|
+
|
|
48
|
+
## 复刻评分
|
|
49
|
+
- 源证据: /5
|
|
50
|
+
- 结构保真: /5
|
|
51
|
+
- 视觉保真: /5
|
|
52
|
+
- 动效/交互: /5
|
|
53
|
+
- 响应式: /5
|
|
54
|
+
- 功能完整: /5
|
|
55
|
+
- 内容替换: /5
|
|
56
|
+
- 法务/部署风险: /5
|
|
57
|
+
- 总评:
|
|
58
|
+
|
|
59
|
+
## 替换地图(要换什么改哪)
|
|
60
|
+
- 文字 → 文件 行
|
|
61
|
+
- 图片/媒体 → 目录
|
|
62
|
+
- 配色 → CSS 变量 / theme
|
|
63
|
+
- 3D 模型 / 字体 → ...
|
|
64
|
+
|
|
65
|
+
## 验证
|
|
66
|
+
- [ ] 本地跑通、console 0 error
|
|
67
|
+
- [ ] 截图对照原站(RECON/screenshots/)
|
|
68
|
+
- [ ] `route-crawl.mjs` 输出原站/克隆站路由地图(多页面站必做)
|
|
69
|
+
- [ ] `interaction-probe.mjs` 输出 hover/click/scroll/canvas 状态证据(交互站必做)
|
|
70
|
+
- [ ] `visual-diff.mjs` 输出截图差异报告(能做时)
|
|
71
|
+
- [ ] `audit-clone.mjs` 输出残留审计报告
|
|
72
|
+
- 验证不了的点(如实记,别伪造): ...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## TEARDOWN.md(复杂交互站追加)
|
|
76
|
+
|
|
77
|
+
技术拆解文档,**所有结论标真源码行号**。结构:
|
|
78
|
+
- **0. 一句话本质**
|
|
79
|
+
- **A. 真实技术拆解**(按支柱:渲染/合成/物理/交互/音频,每条标行号 **+ 证据级别 `SOURCE`/`PARTIAL`/`GUESS`**,见 `effect-extraction.md`)
|
|
80
|
+
- **B. 二手分析校验表**(若有 AI 分析文档:`声称 | 真源码 | 准确度✅/⚠️/❌ | 说明`,重点揪实质性错误)
|
|
81
|
+
- **C. 可迁移方法论**(通用套路 / 本站特有 / 复刻路径)
|
|
82
|
+
|
|
83
|
+
范例:`~/projects/website-clones/marbles-clone/TEARDOWN.md`。
|
|
84
|
+
|
|
85
|
+
## RECON/(侦察产物)
|
|
86
|
+
- `screenshots/original-{1440,768,390}.png` 原站三档
|
|
87
|
+
- `screenshots/clone-1440.png` 克隆图(对照)
|
|
88
|
+
- `screenshots/visual-diff-1440.png` 截图差异图(能做时)
|
|
89
|
+
- `global-recon.json` 侦察探针输出(框架/canvas/滚动库/字体等)
|
|
90
|
+
- `routes/original-route-map.json` / `.md` 原站内部路由地图和逐路由截图
|
|
91
|
+
- `routes-clone/clone-route-map.json` / `.md` 克隆站内部路由地图和逐路由截图
|
|
92
|
+
- `interactions/original-interactions.json` / `.md` 原站交互状态、网络、console、截图证据
|
|
93
|
+
- `interactions-clone/clone-interactions.json` / `.md` 克隆站交互状态、网络、console、截图证据
|
|
94
|
+
- `asset-manifest.json` 原站素材清单和下载状态(能做时)
|
|
95
|
+
- `network/original-network.json` API/XHR 捕获清单(SPA/SaaS 必做)
|
|
96
|
+
- `network/fixtures/` 保存下来的 JSON/text 响应
|
|
97
|
+
- `sourcemaps/sourcemap-manifest.json` source map 搜索结果(复杂前端优先做)
|
|
98
|
+
- `visual-diff-1440.json` 像素差异指标
|
|
99
|
+
- `design-dna.json` 结构化设计身份(视觉复刻/内容爆改模式产出,由 `dna-scaffold.mjs` 起手,人工补全;schema 见 `design-dna.md`)
|
|
100
|
+
- `baseline/` WebGL/特效逆向时"最小原样可复现"产物 + 证据包(见 `effect-extraction.md` 的 baseline-first 闸门)
|
|
101
|
+
|
|
102
|
+
## CLONE_REPORT.md(需要对外汇报或评估 skill 效果时追加)
|
|
103
|
+
|
|
104
|
+
```markdown
|
|
105
|
+
# <站名> · 原站 vs 克隆站评估报告
|
|
106
|
+
|
|
107
|
+
## 结论
|
|
108
|
+
- 复杂度等级:
|
|
109
|
+
- 复刻模式:
|
|
110
|
+
- 总体还原度:
|
|
111
|
+
- 最适合怎么用: 本地学习 / 继续爆改 / 可部署展示 / 仅技术拆解
|
|
112
|
+
|
|
113
|
+
## 对比
|
|
114
|
+
| 维度 | 原站 | 克隆站 | 结论 |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| 信息架构 | | | |
|
|
117
|
+
| 视觉语言 | | | |
|
|
118
|
+
| 动效交互 | | | |
|
|
119
|
+
| 响应式 | | | |
|
|
120
|
+
| 内容替换 | | | |
|
|
121
|
+
| 功能边界 | | | |
|
|
122
|
+
|
|
123
|
+
## 评分
|
|
124
|
+
按 web-clone skill 的 `references/assessment.md` 8 项维度打分。
|
|
125
|
+
|
|
126
|
+
## 已知缺口
|
|
127
|
+
-
|
|
128
|
+
|
|
129
|
+
## 下一步升级建议
|
|
130
|
+
-
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## CLONE_AUDIT.md(上线前追加)
|
|
134
|
+
|
|
135
|
+
由 `scripts/audit-clone.mjs` 生成。重点看:
|
|
136
|
+
- 追踪脚本 / 统计像素是否已删
|
|
137
|
+
- 原站品牌名、日文、TODO 是否残留
|
|
138
|
+
- 外部 URL 是否仍指向原站或第三方不可控资源
|
|
139
|
+
- 素材和 license 风险是否已记录
|
|
140
|
+
|
|
141
|
+
## 收尾
|
|
142
|
+
- 更新中枢 `~/projects/website-clones/README.md` 索引行(状态 emoji:🟡侦察/🟢跑通/🔵改造/✅上线/🔴卡住/🗂️归档)
|
|
143
|
+
- 原始源码留只读基准 `index-original.html`,不要在它上面改
|
|
144
|
+
- 跑完关掉本地服务器进程
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Design DNA · 结构化设计身份层(视觉复刻 / 内容爆改专用)
|
|
2
|
+
|
|
3
|
+
把"做成那个站的感觉"从模糊感觉变成**可版本化、可复用、可对照的 JSON 规范**。
|
|
4
|
+
用在「视觉复刻」和「内容爆改」两个模式——侦察之后、搭工程之前,多产出一个 `design-dna.json`,
|
|
5
|
+
让 Step 6「替换成 Jane 自己的东西」有据可依:**DNA 留着、内容换掉**。
|
|
6
|
+
|
|
7
|
+
> 方法与 schema 改编自 [zanwei/design-dna](https://github.com/zanwei/design-dna)(MIT),按 web-clone 的口径裁剪。
|
|
8
|
+
|
|
9
|
+
## ⚠️ 适用边界(必须先读)
|
|
10
|
+
|
|
11
|
+
Design DNA 的生成哲学是**"近似风格"**——它产出的是"风格一致的新站",不是逐字节复制。
|
|
12
|
+
这和 web-clone 的头号铁律「真源码至上 / 逐字节忠实」是**相反方向**。所以:
|
|
13
|
+
|
|
14
|
+
| 模式 | 用不用 DNA |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 忠实复刻(拿到真源码 / 单文件原生站 / WebGL 逐字节还原) | **不用**。真源码就是真相,别让 DNA 把它稀释成"近似" |
|
|
17
|
+
| 视觉复刻(还原观感但简化实现) | **用**。DNA 是这条路的主产物 |
|
|
18
|
+
| 内容爆改(保留信息架构+节奏+视觉语法,换成 Jane 的内容) | **用**。DNA 定义"要保留什么",内容随便换 |
|
|
19
|
+
|
|
20
|
+
一句话:**DNA 是给"做我自己的站"用的,不是给"搬一模一样"用的。**
|
|
21
|
+
|
|
22
|
+
## 三维结构
|
|
23
|
+
|
|
24
|
+
DNA JSON 分三层,对应"可量化 / 可感知 / 特殊渲染":
|
|
25
|
+
|
|
26
|
+
1. **`design_system`** — 可测量的 token:color / typography / spacing / layout / shape / elevation / iconography / motion / components
|
|
27
|
+
2. **`design_style`** — 主观感知:aesthetic(mood/genre/era)/ visual_language / composition / imagery / interaction_feel / brand_voice_in_ui
|
|
28
|
+
3. **`visual_effects`** — 超出普通 CSS 的渲染:background / particles / 3d / shader / scroll / text / cursor / image / glass-neu / canvas / svg
|
|
29
|
+
|
|
30
|
+
`design_system` 直接落成 CSS 变量;`design_style` 指导主观取舍;`visual_effects` 决定要不要上 Canvas/WebGL/GSAP,并和本 skill 的「WebGL 逆向分支」对接。
|
|
31
|
+
|
|
32
|
+
## 三步工作流
|
|
33
|
+
|
|
34
|
+
1. **Structure** — 先看 schema(下方 + `dna-scaffold.mjs` 生成的骨架),确认要填哪些维度,可裁掉不相关的。
|
|
35
|
+
2. **Analyze** — 从侦察产物里抽:
|
|
36
|
+
- 颜色:`<label>-recon.json` 的 `cssVariables`(`--` 开头的色值)+ `sections[].style` 的 `backgroundColor` / `color`,按面积定 primary、按 CTA 定 accent。
|
|
37
|
+
- 字体:`fonts` 数组 + `sections[].style.fontFamily`,分 heading / body / mono。
|
|
38
|
+
- 间距/布局:截图 + `sections[].rect` 量节奏与最大宽度。
|
|
39
|
+
- 特效:`frameworks.three/gsap/lenis` + `canvases` + `counts.canvas` → 填 `visual_effects.overview.primary_technology` 和各 enabled 标志。
|
|
40
|
+
- 风格/感知:看三档截图(1440/768/390)人工判 mood、genre、composition、whitespace。
|
|
41
|
+
- **每个字段都要填实,不要留空字符串**;填不出的标 `TODO` 并说明缺什么证据。
|
|
42
|
+
3. **Generate** — 解析 DNA → 生成 CSS 自定义属性 → 按 `design_style` 做主观决策 → 按 `effect_intensity` 选实现层级(lightweight=CSS/SVG/vanilla;medium=Canvas2D/GSAP/Lottie;heavy=Three.js/GLSL/Pixi)→ 输出页面 → 灌 Jane 自己的内容。**素材优先从原站取真图(用 `asset-harvest.mjs`),不要 AI 重绘近似。**
|
|
43
|
+
|
|
44
|
+
## 脚手架
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node /Users/work/create-x-studio/.agents/skills/web-clone/scripts/dna-scaffold.mjs \
|
|
48
|
+
--recon ~/projects/website-clones/<站名>-clone/RECON/original-recon.json \
|
|
49
|
+
--out ~/projects/website-clones/<站名>-clone/RECON/design-dna.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
脚本会输出完整 DNA 骨架,并把侦察到的字体、CSS 色变量、框架/特效信号**best-effort 预填**,
|
|
53
|
+
其余字段留 `""`(待人工 Analyze 补全)。没有 `--recon` 也能跑,纯出空骨架。
|
|
54
|
+
|
|
55
|
+
## DNA JSON 完整字段(改编自 design-dna,MIT)
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"meta": { "name": "", "description": "", "source_references": "", "created_at": "" },
|
|
60
|
+
|
|
61
|
+
"design_system": {
|
|
62
|
+
"color": {
|
|
63
|
+
"palette_type": "monochromatic | complementary | analogous | triadic | split-complementary",
|
|
64
|
+
"primary": { "hex": "", "role": "" },
|
|
65
|
+
"secondary": { "hex": "", "role": "" },
|
|
66
|
+
"accent": { "hex": "", "role": "" },
|
|
67
|
+
"neutral": { "scale": "", "usage": "" },
|
|
68
|
+
"semantic": { "success": "", "warning": "", "error": "", "info": "" },
|
|
69
|
+
"surface": { "background": "", "card": "", "elevated": "" },
|
|
70
|
+
"contrast_strategy": "high contrast | subtle layers | dark-on-light dominant"
|
|
71
|
+
},
|
|
72
|
+
"typography": {
|
|
73
|
+
"type_scale": {
|
|
74
|
+
"display": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
75
|
+
"heading_1": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
76
|
+
"heading_2": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
77
|
+
"heading_3": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
78
|
+
"body": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
79
|
+
"body_small": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
80
|
+
"caption": { "size": "", "weight": "", "line_height": "", "tracking": "" },
|
|
81
|
+
"overline": { "size": "", "weight": "", "line_height": "", "tracking": "" }
|
|
82
|
+
},
|
|
83
|
+
"font_families": { "heading": "", "body": "", "mono": "" },
|
|
84
|
+
"font_style_notes": ""
|
|
85
|
+
},
|
|
86
|
+
"spacing": { "base_unit": "", "scale": "", "content_density": "compact | comfortable | spacious", "section_rhythm": "" },
|
|
87
|
+
"layout": { "grid_system": "", "max_content_width": "", "columns": "", "gutter": "", "breakpoints": "", "alignment_tendency": "strict grid | centered | asymmetric | mixed" },
|
|
88
|
+
"shape": { "border_radius": { "small": "", "medium": "", "large": "", "pill": "" }, "border_usage": "none | subtle 1px | bold borders | only on inputs", "divider_style": "" },
|
|
89
|
+
"elevation": { "shadow_style": "none | soft diffused | hard drop | layered", "levels": { "low": "", "medium": "", "high": "" }, "depth_cues": "shadows | overlapping layers | blur/glass | color intensity" },
|
|
90
|
+
"iconography": { "style": "", "stroke_weight": "", "size_scale": "", "preferred_set": "" },
|
|
91
|
+
"motion": { "easing": "", "duration_scale": { "micro": "", "normal": "", "macro": "" }, "entrance_pattern": "", "exit_pattern": "", "philosophy": "minimal functional | playful bouncy | cinematic | none" },
|
|
92
|
+
"components": { "button_style": "", "input_style": "", "card_style": "", "navigation_pattern": "", "modal_style": "", "list_style": "", "component_notes": "" }
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
"design_style": {
|
|
96
|
+
"aesthetic": { "mood": [], "visual_metaphor": "", "era_influence": "", "genre": "", "personality_traits": [], "adjectives": [] },
|
|
97
|
+
"visual_language": { "complexity": "minimal | moderate | rich | maximal", "ornamentation": "none | subtle accents | decorative | heavily ornamented", "whitespace_usage": "", "visual_weight_distribution": "", "focal_strategy": "single hero element | distributed interest | progressive reveal", "contrast_level": "", "texture_usage": "" },
|
|
98
|
+
"composition": { "hierarchy_method": "scale contrast | color weight | spatial isolation | typographic hierarchy", "balance_type": "symmetric | asymmetric | radial | mosaic", "flow_direction": "", "grouping_strategy": "", "negative_space_role": "" },
|
|
99
|
+
"imagery": { "photo_treatment": "", "illustration_style": "", "graphic_elements": "", "pattern_usage": "", "image_shape": "" },
|
|
100
|
+
"interaction_feel": { "feedback_style": "", "hover_behavior": "", "transition_personality": "snappy | smooth glide | bouncy elastic | fade-subtle", "loading_style": "", "microinteraction_density": "" },
|
|
101
|
+
"brand_voice_in_ui": { "tone": "", "formality": "", "cta_style": "direct imperative | friendly invitation | urgent scarcity | subtle suggestion", "empty_state_approach": "", "error_tone": "" }
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
"visual_effects": {
|
|
105
|
+
"overview": { "effect_intensity": "none | subtle-accent | moderate | heavy-immersive", "performance_tier": "lightweight | medium | heavy", "fallback_strategy": "", "primary_technology": "CSS only | Canvas 2D | WebGL/Three.js | GSAP | Lottie | SVG SMIL | Pixi.js" },
|
|
106
|
+
"background_effects": { "type": "gradient-animation | noise-field | mesh-gradient | video-bg | generative-art | none", "description": "", "technology": "", "params": { "color_palette": "", "speed": "", "density": "", "opacity": "", "blend_mode": "" } },
|
|
107
|
+
"particle_systems": { "enabled": false, "type": "floating-dots | confetti | snow | fireflies | connected-nodes | custom", "description": "", "technology": "", "params": { "count": "", "shape": "", "size_range": "", "movement_pattern": "", "color_behavior": "", "interaction": "mouse-repel | mouse-attract | click-burst | none", "spawn_area": "" } },
|
|
108
|
+
"3d_elements": { "enabled": false, "type": "hero-model | product-viewer | scene-bg | text-extrusion | abstract-geometry", "description": "", "technology": "", "params": { "renderer": "", "lighting": "", "camera": "", "materials": "", "geometry": "", "post_processing": [], "interaction_model": "" } },
|
|
109
|
+
"shader_effects": { "enabled": false, "type": "noise-distortion | wave | morph | color-shift | custom-GLSL", "description": "", "technology": "", "params": { "uniforms": "", "vertex_manipulation": "", "fragment_output": "", "noise_type": "perlin | simplex | worley | fbm", "distortion": "" } },
|
|
110
|
+
"scroll_effects": { "parallax": { "enabled": false, "layers": "", "depth_range": "", "speed_curve": "" }, "scroll_triggered_animations": { "enabled": false, "trigger_points": "", "animation_type": "fade-up | scale-in | clip-reveal | counter | draw-SVG", "scrub_behavior": "" }, "scroll_morphing": { "enabled": false, "description": "" } },
|
|
111
|
+
"text_effects": { "type": "split-letter-animate | typewriter | glitch | gradient-fill | 3d-extrude | none", "description": "", "technology": "", "params": { "split_strategy": "by-char | by-word | by-line", "animation_per_unit": "", "stagger": "", "effect_style": "" } },
|
|
112
|
+
"cursor_effects": { "enabled": false, "type": "custom-cursor | magnetic-buttons | spotlight | trail | none", "description": "", "params": { "shape": "", "size": "", "blend_mode": "", "trail": "", "interaction_zone": "" } },
|
|
113
|
+
"image_effects": { "type": "hover-distortion | reveal-clip | parallax-tilt | rgb-shift | none", "description": "", "technology": "", "params": { "filter_pipeline": "", "hover_transform": "", "reveal_animation": "", "distortion_type": "barrel | wave | liquid | glitch" } },
|
|
114
|
+
"glassmorphism_neumorphism": { "enabled": false, "style": "glass | neumorphic-light | neumorphic-dark | frosted-layers | none", "params": { "blur_radius": "", "transparency": "", "border_treatment": "", "shadow_type": "", "light_source_angle": "" } },
|
|
115
|
+
"canvas_drawings": { "enabled": false, "type": "generative-lines | interactive-blobs | data-visualization | pattern-fill | none", "description": "", "technology": "", "params": { "draw_method": "", "animation_loop": "", "color_scheme": "", "responsiveness": "", "interaction": "" } },
|
|
116
|
+
"svg_animations": { "enabled": false, "type": "path-draw | morph-shapes | logo-reveal | decorative-loop | none", "description": "", "params": { "animation_method": "", "path_morphing": "", "stroke_animation": "", "filter_effects": "" } },
|
|
117
|
+
"composite_notes": ""
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 和 WebGL 逆向分支的分工
|
|
123
|
+
|
|
124
|
+
- `visual_effects` 里标了 `heavy-immersive` / `WebGL/Three.js` / `shader_effects.enabled=true` 的站,**别用 DNA 去"近似"那个特效**——那是 `effect-extraction.md` + `reverse-engineering.md` 的活,要逆向真实现。
|
|
125
|
+
- DNA 在这种站里只负责"特效**之外**"的设计层(配色/排版/布局/普通动效),特效本身走逆向或委托 web-shader-extractor。
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 特效提取的证据纪律 + Baseline 闸门(WebGL/Canvas 逆向分支强化)
|
|
2
|
+
|
|
3
|
+
`reverse-engineering.md` 讲"怎么读懂渲染架构";本篇讲**逆向特效时怎么不骗自己**,
|
|
4
|
+
以及找不到真源码时的兜底路线。三件套:**证据分级 → no-compensation → baseline-first 闸门**。
|
|
5
|
+
|
|
6
|
+
> 纪律范式受 [lixiaolin94/skills · web-shader-extractor](https://github.com/lixiaolin94/skills) 启发(该仓库无 LICENSE,
|
|
7
|
+
> 默认保留所有权利——这里**只借方法概念、全部用本 skill 自己的话重写,未复制其代码或原文**)。
|
|
8
|
+
> 它和 web-clone 头号铁律「真源码至上」同魂,只是把"标行号"升级成了系统化的证据制度。
|
|
9
|
+
|
|
10
|
+
## 一、证据分级(每条结论都打标签)
|
|
11
|
+
|
|
12
|
+
写 TEARDOWN 时,渲染管线的每个关键事实都标一个级别,**默认按最低级算**:
|
|
13
|
+
|
|
14
|
+
| 标签 | 含义 | 例子 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `SOURCE` | 直接、绑定到目标的硬证据 | 公开真源码行、source-map 还原的模块、运行时对象 dump、抓到的 shader/WGSL 文本、帧捕获、带 hash 的网络响应体 |
|
|
17
|
+
| `PARTIAL` | 下一步探针的把手,还不够定论 | 类/函数/字段名、minified bundle 切片、框架对象、拿到 shader 但缺 uniform/pass/输入状态 |
|
|
18
|
+
| `GUESS` | 没有直接证据的重建值 | 视觉拟合、命名推断、套用默认值、手调魔数、任何"看起来对"的行为复原 |
|
|
19
|
+
|
|
20
|
+
- **未标 = 当 GUESS。** 别让没证据的东西混进"已知"。
|
|
21
|
+
- 这是把 marbles 教训("AI 把解析求交臆造成 ray-marching")制度化:**凡是 GUESS 级的实现,照抄前必须先升级到 SOURCE。**
|
|
22
|
+
|
|
23
|
+
## 二、no-compensation(不靠调参掩盖不懂)
|
|
24
|
+
|
|
25
|
+
> **严禁**为了让画面"看起来对",去改亮度 / 速度 / 位置 / 噪声值,来掩盖时序、颜色、FBO、资源、坐标系或状态模型上的真错误。
|
|
26
|
+
|
|
27
|
+
- 拟合出来的某个常数让输出更像了 → **它仍是 GUESS**,并写明"要拿到什么证据才能升级"。
|
|
28
|
+
- 接线类事实(pass 顺序、坐标变换、时间单位、输入耦合)**不会因为画面像了就算对**,必须独立追到证据为止。
|
|
29
|
+
- 对应 web-clone 老规矩:验证不了的如实写,**别伪造"拖动成功"**。
|
|
30
|
+
|
|
31
|
+
## 三、baseline-first 闸门(先复现,再重构)
|
|
32
|
+
|
|
33
|
+
逆向特效时**最容易犯的错**是边抠边重写边美化,最后既不像原站也说不清哪步错了。改成分闸:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
定位渲染面 → 捕获最小真相 → RAW REPLAY(原样最小复现)→ ✅BASELINE 逐帧比对通过
|
|
37
|
+
↓ 通过后才允许
|
|
38
|
+
PROJECTIZE(重构成可编辑工程)→ PACKAGE
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **RAW REPLAY**:用抓到的真实 draw call / shader / uniform / 顶点数据,做一个**尽量小、尽量原样**的可运行复现,不优化、不换框架、不改参数。
|
|
42
|
+
- **BASELINE 闸门**:RAW REPLAY 必须和原站逐帧(或多帧采样)视觉一致,才算过。**没过这道闸,不准进重构。**
|
|
43
|
+
- 过闸后再 PROJECTIZE:换成可维护写法(raw WebGL / Three.js TSL / Babylon 等),每处仍标证据级别。
|
|
44
|
+
- 收尾三态,如实标注:`DONE_BASELINE_VERIFIED`(复现且验证)/ `DONE_PROJECTIZED`(已工程化)/ `DONE_BASELINE_WITH_GAPS`(复现但有记录在案的缺口)。
|
|
45
|
+
|
|
46
|
+
## 四、找不到真源码时——运行时捕获兜底
|
|
47
|
+
|
|
48
|
+
web-clone 第一动作永远是"去 GitHub / source-map 找真源码"。但**特效站经常无源、minified 到底**。
|
|
49
|
+
这时不要退回"看着像就照着写"(那是 GUESS),而是去**渲染边界抓运行时真相**:
|
|
50
|
+
|
|
51
|
+
- 在 WebGL/WebGPU 上下文拦截:实际的 draw call、绑定的 program、编译过的 shader 源、uniform 值、FBO/纹理尺寸、blend/depth 状态。
|
|
52
|
+
- 工具方向:spector.js 式的帧捕获、`WebGLRenderingContext` 原型打补丁记录调用、`getShaderSource` 拿编译后的 shader、preload script 在页面脚本前注入钩子。
|
|
53
|
+
- 抓到的这些算 `SOURCE` 级——它们就是新的"真源码",喂给 baseline-first 流程。
|
|
54
|
+
|
|
55
|
+
## 五、什么时候委托 web-shader-extractor
|
|
56
|
+
|
|
57
|
+
如果你已经装了 `web-shader-extractor`(`npx skills add lixiaolin94/skills` 里的那个 skill),
|
|
58
|
+
**遇到下面情况直接把特效那一段委托给它,web-clone 只当总入口**:
|
|
59
|
+
|
|
60
|
+
- 站是 WebGL/WebGPU/重 Canvas 特效,且 GitHub + source-map 都找不到真源码;
|
|
61
|
+
- 需要运行时帧捕获、逐帧比对、shader/uniform 级别的抠取;
|
|
62
|
+
- 想要"先 baseline 复现、再独立工程化"的完整带闸流程。
|
|
63
|
+
|
|
64
|
+
委托产物(最小可复现的 baseline + 证据包)拿回来后,**再并入 `~/projects/website-clones/<站名>-clone/`**,
|
|
65
|
+
按 web-clone 的产物规范补 NOTES/TEARDOWN,并继续走 Step 5 验证 + Step 6 替换。
|
|
66
|
+
|
|
67
|
+
没装它也不影响:上面四节的纪律本身就能照着做,web-shader-extractor 只是把第四节的捕获机器做成了现成工具。
|
|
68
|
+
|
|
69
|
+
## 与既有产物的衔接
|
|
70
|
+
|
|
71
|
+
- TEARDOWN.md 的"A. 真实技术拆解"每条加证据标签(`SOURCE`/`PARTIAL`/`GUESS`)。
|
|
72
|
+
- "B. 二手分析校验表"本来就是在抓 GUESS 冒充 SOURCE,口径一致。
|
|
73
|
+
- baseline 复现物建议留在 `<站名>-clone/RECON/baseline/`,和原始截图一起作为"验证过"的铁证。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 旗舰案例 · Glass Marbles(真架构 vs AI 臆造)
|
|
2
|
+
|
|
3
|
+
源站 https://chiuhans111.github.io/marbles/ ·作者 Hans Chiu ·单文件 1067 行 ·许可证 **NONE**。
|
|
4
|
+
完整逐行拆解见 `~/projects/website-clones/marbles-clone/TEARDOWN.md`,这里是给 skill 用的精华版。
|
|
5
|
+
|
|
6
|
+
## 一句话真架构
|
|
7
|
+
|
|
8
|
+
一个全屏 WebGL 片段着色器,用**解析法(二次方程 `b*b-c`)**求光线与球的交点、做最多 4 次折射/反射迭代,把光学结果编码成一张**位移图 PNG**(RG=像素位移、B=菲涅尔值);再用一个 SVG `<filter>` 的 `feDisplacementMap(scale=200)` 拿这张图去扭曲**真实的、活的、可交互的网页 DOM**(背景色块 + 标题文字)。
|
|
9
|
+
|
|
10
|
+
> 你拖动的玻璃球本质是个透镜,透镜后面就是这页 HTML。WebGL 全程不碰 DOM 像素——折射这件事是 SVG filter 干的。物理、音频均为纯手写、零库。
|
|
11
|
+
|
|
12
|
+
## 三大支柱(真实实现要点)
|
|
13
|
+
|
|
14
|
+
1. **WebGL 光学**:解析球求交(非 ray-marching);折射率 N=1.3、迭代 4 次;菲涅尔 `0.05+0.95*pow(1-cosθ,2.0)`(指数 2,非 Schlick 的 5);内部 2 气泡 + 抛物面彩色核 + Beer-Lambert 体积吸收;**一套 shader 靠 `u_mode`(0折射/1反射/2前景高光/3阴影) 复用**;位移编码 `DISPLACEMENT_SCALE=200` 与 SVG 端严格对齐。
|
|
15
|
+
2. **SVG Filter 合成**:4 个 canvas(1 主 + 3 离屏),离屏图每帧 `toDataURL` 喂给 `<feImage>`。真实链:阴影 `feGaussianBlur(8)` → `feBlend multiply` 乘到 DOM → 折射 `feDisplacementMap` → 反射 `feDisplacementMap` → `feGaussianBlur(2)` → 用反射图 B 通道(菲涅尔)经 `feColorMatrix` 做 alpha mask → `feComposite` 两步合成。被折射的 `SourceGraphic` 就是挂了 `filter:url(#marble-filter)` 的 `#container`。
|
|
16
|
+
3. **物理**:纯手写,`mass=r³`、重力 0.8、3D 弹性碰撞(恢复 0.8、只在靠近时解算)、地面恢复 0.55 + 微弹跳归零、四元数滚动、拖拽抬升 targetZ=200;`settleFrames` 静止时完全停渲染。
|
|
17
|
+
4. **音频**:Web Audio 程序化合成(零文件),基频 `800+(60-r)*20` + 5 泛音,音量随碰撞速度。
|
|
18
|
+
|
|
19
|
+
## ⚠️ AI 分析文档错在哪(反面教材,记住这些坑型)
|
|
20
|
+
|
|
21
|
+
那份 `弹珠网站复刻分析.md` 正文的**概念骨架基本对**(8 步、三支柱方向都对),但**附带的"复刻代码块"几乎全是臆造**:
|
|
22
|
+
|
|
23
|
+
| 臆造 | 真相 | 坑型(通用警示) |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| ray-marching + SDF + `MAX_STEPS=100` + 6 次差分求法线 | 解析求交,法线 `normalize(rp-center)` | **别凭直觉假设折射 demo 用 ray-marching**——球有闭式解,很多 demo 用解析法,又快又准 |
|
|
26
|
+
| `sampler2D uBackground` 把 DOM 当纹理采样 | 着色器不读背景;折射靠位移图交给 SVG | **搞反了 GPU↔DOM 的层级**——最核心的架构创意被丢 |
|
|
27
|
+
| `feBlend screen` + 单个 displacement + `feComposite over` 合阴影 | 双 displacement + 菲涅尔 mask + multiply 阴影 | **二手分析的 filter 链不可信,以真源码逐节点核对** |
|
|
28
|
+
| `MARBLE_COUNT=5`、数组开 10 | 硬编码 2 | 连常量都靠猜 |
|
|
29
|
+
| 屏幕中心 NDC 坐标 | 左上角像素 + Y 翻转 | 坐标系约定全凭臆测 |
|
|
30
|
+
|
|
31
|
+
**教训**:AI 写的"复刻施工图"= 参考其思路骨架,**代码块一行都别直接抄**,必须拿真源码核对。这就是本 skill 头号铁律的来源。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 逆向拆解 WebGL/Canvas 重前端站
|
|
2
|
+
|
|
3
|
+
当侦察判定一个站是 WebGL/Canvas/Three.js 重前端(`window.THREE` 存在、或有多个 `<canvas>`、或有 `<script type="x-shader/*">`),走这套。
|
|
4
|
+
|
|
5
|
+
> 本篇讲**怎么读懂渲染架构**;逆向时的**证据纪律**(SOURCE/PARTIAL/GUESS 分级、no-compensation、baseline-first 闸门)和**找不到源码时的运行时捕获兜底** → `effect-extraction.md`。两篇配合用。
|
|
6
|
+
|
|
7
|
+
## 通用拆解步骤
|
|
8
|
+
|
|
9
|
+
1. **先拿单文件源码**。很多 demo 站全自包含在一个 HTML 里(GitHub raw / 浏览器查看源代码),别急着上 Playwright 抓站。
|
|
10
|
+
2. **数 canvas、读 SVG defs、grep shader**:几个 `<canvas>`?有没有 `<filter>`?着色器脚本在哪?——这三样决定渲染架构。
|
|
11
|
+
3. **判断"WebGL 在算什么"**(关键,决定要不要相信二手分析):
|
|
12
|
+
- grep `texture2D` / `sampler2D` → 在采样纹理 / 帧缓冲
|
|
13
|
+
- grep `for` 循环里 `+= dS` / `map(` / `MAX_STEPS` → ray-marching(步进式)
|
|
14
|
+
- grep `b*b` / 判别式 / `sqrt(` + 二次方程 → **解析求交**(闭式解,球/平面常用)
|
|
15
|
+
- ⚠️ 别凭直觉假设 ray-marching——折射玻璃 demo 经常是解析球求交。
|
|
16
|
+
4. **找 GPU↔DOM 的桥**:若 WebGL canvas 不直接显示,而是 `toDataURL` / 喂 `<feImage>` / `feDisplacementMap`,说明它在生成**数据图**(位移/法线/深度)给别的层用。这是高级前端的标志手法,也是最容易被二手分析搞反的层级。
|
|
17
|
+
5. **物理/音频单独读**:通常是与渲染解耦的纯 JS 模块,可单独验证。
|
|
18
|
+
|
|
19
|
+
## 值得攒着的可迁移高级范式
|
|
20
|
+
|
|
21
|
+
- **位移图折射 DOM**:离屏 WebGL 算出 RG=位移、B=辅助量的 PNG,SVG `<feDisplacementMap scale=N>` 拿它扭曲真实 HTML。GPU 端与 SVG 端的 `scale` 必须对齐。能做液态玻璃、放大镜、水波等任意"透镜罩在网页上"的效果,且**被折射的是活的可交互 DOM**——这是 Three.js `MeshPhysicalMaterial(transmission)` 做不到的(它只能做"玻璃球外观",做不出"折射整个网页")。
|
|
22
|
+
- **一套 shader + mode uniform 多用途**:折射/反射/阴影/前景共用同一份 fragment shader,靠 `u_mode` 分支。省代码省编译。
|
|
23
|
+
- **辅助数据降分辨率 + 静止跳帧**:位移/反射/阴影图肉眼对分辨率不敏感 → 降到 1/2、1/4。物体静止 → 完全停渲染(settleFrames)。重前端站标配性能套路。
|
|
24
|
+
- **全屏大三角替代 quad**:3 顶点 `[-1,-1, 3,-1, -1,3]` 覆盖全屏,省一条对角线。
|
|
25
|
+
|
|
26
|
+
## 复刻路径决策
|
|
27
|
+
|
|
28
|
+
- **能 1:1 还原 + 许可允许** → 直接拿真源码改文案/配色/参数(单文件原生站逐字节保留 = 最忠实)。
|
|
29
|
+
- **想要近似效果、不在意一致** → 找同类开源模板换内容(如 awesome-threejs)。但注意:**换实现路线常常丢掉原站的灵魂机制**(如本案的"折射真实 DOM"),先确认那个机制是不是用户要的卖点。
|
|
30
|
+
- **本站特有的数学(求交公式/魔数)迁移时要重写**:解析"圆球"求交换成别的形状就得换公式(或才真需要 SDF/ray-marching);折射率、菲涅尔系数、吸收等手调魔数换材质要重调。
|
|
31
|
+
|
|
32
|
+
## 验证(硬要求)
|
|
33
|
+
起本地服务器 → 浏览器打开 → 抓 console(不能有 JS/WebGL 编译错误)→ 截图对照原站。
|
|
34
|
+
诚实记录验证不了的部分:例如 Playwright 合成 PointerEvent `isTrusted=false` 触发不了拖拽命中逻辑——**如实写进 NOTES,别伪造"拖动成功"**。物理类站可用"两次加载初始状态不同"间接证明引擎在跑。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 静态构建站的 1:1 忠实复刻:全量资产镜像
|
|
2
|
+
|
|
3
|
+
> 适用:**Astro / Vite SSG / Hugo / Eleventy / 任何把客户端运行时输出成可下载静态资产的站**——哪怕它是 WebGL/Canvas/高斯泼溅重前端。
|
|
4
|
+
> 不适用:真·服务端渲染 / 数据驱动 SPA(业务数据在 API 后面)→ 走 `network-capture.mjs` 做 API 替身,不是这条。
|
|
5
|
+
|
|
6
|
+
## 核心认知(为什么这能做到 1:1)
|
|
7
|
+
这类站的"**真源码不在 GitHub**",但**部署出来的静态资产就是真相**:HTML + 打包 bundle + CSS + 运行时 fetch 的二进制(`.sog`/`.buf`/`.wasm`/`.riv`/字体/图/视频)。把这些**原样**镜像下来、从 web 根目录服务,跑的就是**真代码 + 真资产**,不是重建——所以能逐字节 1:1,连原站的 bug/怪癖都一并还原。
|
|
8
|
+
|
|
9
|
+
这是"真源码至上"铁律在静态站上的延伸:**对静态站,"拿到真源码" = "镜像部署资产整套"**。
|
|
10
|
+
|
|
11
|
+
> ⚠️ 决策树坑:别看到 `astro:true` 就甩去"主题市场找源主题"——那只对**用现成开源主题**的站成立。**定制 Astro 站(如 Lusion oryzo.ai)没有可买的主题**,正解是本条的全量镜像。
|
|
12
|
+
|
|
13
|
+
## 为什么必须"真浏览器全程滚动捕获",不能只 grep / wget
|
|
14
|
+
- `.buf`/`.sog`/`.riv` 等二进制是 **JS 运行时按滚动进度 fetch** 的,URL 常是代码里**动态拼接**的 → grep bundle 抓不全,`wget --mirror` 也发现不了(它只跟 HTML 静态链接)。
|
|
15
|
+
- 唯一可靠:**真浏览器加载 + 从头滚到尾**,把每一个**真实发生的网络请求**记下来,再按这份"实际请求清单"镜像。
|
|
16
|
+
|
|
17
|
+
## 一键脚本
|
|
18
|
+
```bash
|
|
19
|
+
node /Users/work/create-x-studio/.agents/skills/web-clone/scripts/mirror-site.mjs \
|
|
20
|
+
--url https://<站>/ \
|
|
21
|
+
--out ~/projects/website-clones/<站名>-clone
|
|
22
|
+
```
|
|
23
|
+
产物:
|
|
24
|
+
- `<out>/site/…`:镜像的**同源**资产(保留路径;目录 URL 存 `index.html`)
|
|
25
|
+
- `<out>/own-asset-urls.txt`:同源资产清单
|
|
26
|
+
- `<out>/third-party.json`:第三方 host + **需自托管的 webfont CSS**(Typekit/Google)提示
|
|
27
|
+
- `<out>/mirror-manifest.json`:全部请求 + 状态
|
|
28
|
+
|
|
29
|
+
脚本用真浏览器全程滚动捕获 + 走浏览器网络栈下载(cookie/TUN/代理与页面一致)。
|
|
30
|
+
|
|
31
|
+
## 镜像后的手工收尾(让它离线 1:1 跑)
|
|
32
|
+
脚本只搬同源资产、不自动改写——**第三方按 `third-party.json` 人工处理**:
|
|
33
|
+
|
|
34
|
+
1. **自托管锁域名字体(最常见=Adobe Typekit)**
|
|
35
|
+
Typekit kit 锁授权域名,远程 `@import` 换域名后可能不渲染 → 自托管:
|
|
36
|
+
```bash
|
|
37
|
+
# ① 下 kit CSS(直连,typekit 常被代理挡 → 不要走 proxy)
|
|
38
|
+
curl -sL -A "Mozilla/5.0 …Chrome…" -e "https://<站>/" "https://use.typekit.net/<kit>.css" -o site/typekit/kit.css
|
|
39
|
+
# ② 从 kit.css 的 @font-face src 抠出 use.typekit.net/af/... 字体 URL,逐个下到 site/typekit/fonts/
|
|
40
|
+
# 同一字体 3 个后缀: /l=woff2 /d=woff /a=otf(以文件魔数 wOF2/wOFF/0x00010000 为准,别信文件名)
|
|
41
|
+
# ③ 写本地 @font-face(相对 url + 保留 format 提示),见下
|
|
42
|
+
```
|
|
43
|
+
本地 `@font-face`:
|
|
44
|
+
```css
|
|
45
|
+
@font-face{ font-family:"<同名>"; src:url("./fonts/x.woff2") format("woff2"),
|
|
46
|
+
url("./fonts/x.woff") format("woff"), url("./fonts/x.otf") format("opentype");
|
|
47
|
+
font-display:swap; font-weight:<原范围>; }
|
|
48
|
+
```
|
|
49
|
+
然后把引用处改成本地——**注意 Typekit 常是主 CSS 第一行 `@import"https://use.typekit.net/<kit>.css"`,不是 HTML `<link>`**:
|
|
50
|
+
```bash
|
|
51
|
+
perl -0pi -e 's{\@import"https://use\.typekit\.net/<kit>\.css"}{\@import"/typekit/kit-local.css"}g' site/_astro/<主>.css
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. **删追踪**:cloudflare beacon / GA / 像素——精确切 `<script>`。
|
|
55
|
+
|
|
56
|
+
3. **公共 CDN(Rive wasm@unpkg / 第三方 player)**:公共 CDN 跨域可在线加载,本地服务+联网时正常 → 可留在线(离线则失效,NOTES 标注)。要彻底离线再镜像它们并改写注入点。
|
|
57
|
+
|
|
58
|
+
4. **Vimeo/YouTube 嵌入**:iframe 在线播,离线不可用 → 一般非核心首屏,NOTES 标注即可。
|
|
59
|
+
|
|
60
|
+
## 服务 + 验证
|
|
61
|
+
```bash
|
|
62
|
+
cd ~/projects/website-clones/<站名>-clone/site
|
|
63
|
+
python3 -m http.server 8124 # 必须从 site/ 作 web 根,根相对路径(/_astro /models …)才解析
|
|
64
|
+
```
|
|
65
|
+
然后按 SKILL.md Step 5:浏览器 0 console error + `visual-diff.mjs` 像素对照原站。重 WebGL 站记得**滚动到各段截图**对照(静态全页截图抓不到滚动触发的 GL 帧)。
|
|
66
|
+
|
|
67
|
+
## worked example:oryzo.ai(Lusion,L6)
|
|
68
|
+
- 135 个同源资产(HTML+bundle+CSS + 25×`.buf` 几何/相机动画 + 2×`.sog` 高斯泼溅 + 排序 wasm + `.riv` + MSDF + 字体 + 80+ 图)
|
|
69
|
+
- 唯一改写:Typekit `@import`→本地自托管 halyard + 删 cloudflare beacon
|
|
70
|
+
- 结果:`scrollHeight` 精确一致、**0 console error**、hero 像素 diff **36/1.3M(5/5)**
|
|
71
|
+
- 留在线:Vimeo 画廊视频 + unpkg Rive wasm(非核心)
|
|
72
|
+
- 完整记录:`~/projects/website-clones/oryzo-clone/`(NOTES.md + TEARDOWN.md)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
function usage() {
|
|
7
|
+
console.log(`Usage:
|
|
8
|
+
node scripts/asset-harvest.mjs --recon original-recon.json --out assets/original [--manifest asset-manifest.json] [--all-external]
|
|
9
|
+
|
|
10
|
+
Reads recon JSON, downloads discovered images/scripts/stylesheets, and writes a manifest.
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const out = { recon: "", outDir: "assets/original", manifest: "", allExternal: false };
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (arg === "--help" || arg === "-h") out.help = true;
|
|
19
|
+
else if (arg === "--recon") out.recon = argv[++i] || "";
|
|
20
|
+
else if (arg === "--out") out.outDir = argv[++i] || "assets/original";
|
|
21
|
+
else if (arg === "--manifest") out.manifest = argv[++i] || "";
|
|
22
|
+
else if (arg === "--all-external") out.allExternal = true;
|
|
23
|
+
else throw new Error(`Unexpected argument: ${arg}`);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeName(url) {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
const ext = path.extname(parsed.pathname).slice(0, 12);
|
|
31
|
+
const base = path.basename(parsed.pathname, ext).replace(/[^a-z0-9._-]+/gi, "-").slice(0, 80) || "asset";
|
|
32
|
+
const hash = crypto.createHash("sha1").update(url).digest("hex").slice(0, 10);
|
|
33
|
+
return `${base}-${hash}${ext || ".bin"}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectAssets(recon) {
|
|
37
|
+
const assets = new Map();
|
|
38
|
+
for (const capture of recon.captures || []) {
|
|
39
|
+
const signals = capture.signals || {};
|
|
40
|
+
for (const src of signals.scripts || []) assets.set(src, { type: "script", url: src });
|
|
41
|
+
for (const href of signals.stylesheets || []) assets.set(href, { type: "stylesheet", url: href });
|
|
42
|
+
for (const image of signals.images || []) {
|
|
43
|
+
if (image.src) assets.set(image.src, { type: "image", url: image.src, alt: image.alt, width: image.width, height: image.height });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return Array.from(assets.values()).filter((asset) => /^https?:\/\//i.test(asset.url));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function download(asset, outDir) {
|
|
50
|
+
const response = await fetch(asset.url, {
|
|
51
|
+
headers: {
|
|
52
|
+
"user-agent": "web-clone-skill/1.0 asset-harvest",
|
|
53
|
+
"accept": "*/*",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
57
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
58
|
+
const hostDir = path.join(outDir, new URL(asset.url).hostname);
|
|
59
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
60
|
+
const file = path.join(hostDir, safeName(asset.url));
|
|
61
|
+
fs.writeFileSync(file, buffer);
|
|
62
|
+
return { ...asset, status: "ok", bytes: buffer.length, file };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const args = parseArgs(process.argv.slice(2));
|
|
67
|
+
if (args.help || !args.recon) {
|
|
68
|
+
usage();
|
|
69
|
+
process.exit(args.help ? 0 : 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const recon = JSON.parse(fs.readFileSync(args.recon, "utf8"));
|
|
73
|
+
const outDir = path.resolve(args.outDir);
|
|
74
|
+
const manifestFile = path.resolve(args.manifest || path.join(outDir, "asset-manifest.json"));
|
|
75
|
+
const originHost = recon.url ? new URL(recon.url).hostname : "";
|
|
76
|
+
const assets = collectAssets(recon).filter((asset) => args.allExternal || new URL(asset.url).hostname === originHost);
|
|
77
|
+
const results = [];
|
|
78
|
+
|
|
79
|
+
for (const asset of assets) {
|
|
80
|
+
try {
|
|
81
|
+
results.push(await download(asset, outDir));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
results.push({ ...asset, status: "error", error: error.message });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(path.dirname(manifestFile), { recursive: true });
|
|
88
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify({
|
|
89
|
+
source: args.recon,
|
|
90
|
+
url: recon.url,
|
|
91
|
+
allExternal: args.allExternal,
|
|
92
|
+
total: results.length,
|
|
93
|
+
ok: results.filter((item) => item.status === "ok").length,
|
|
94
|
+
error: results.filter((item) => item.status === "error").length,
|
|
95
|
+
assets: results,
|
|
96
|
+
}, null, 2)}\n`);
|
|
97
|
+
console.log(manifestFile);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`asset-harvest failed: ${error.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|