@xiaobuyu/nodesign 0.0.1 → 0.0.2
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 +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/engine/motion/inventory.js — 一页"靠什么在动"的静态清单(2026-08-21)
|
|
3
|
-
*
|
|
4
|
-
* 回答的是"这站用了哪些 CSS 滚动动画 / 特殊样式动画、靠什么库",不是"动起来好不好看"
|
|
5
|
-
* (那是 motion-lab 的胶片条和元素探针的活)。**对页面无知**:浏览通道看参考站、
|
|
6
|
-
* 产物会话查自己的站,都是同一份。
|
|
7
|
-
*
|
|
8
|
-
* 四路来源,各自独立 try(一路挂了不带走别的):
|
|
9
|
-
* 1. 样式表原文:CDP `CSS.getStyleSheetText` —— 跨域 CDN 的样式表也拿得到
|
|
10
|
-
* (浏览器自己的资源,不受 CORS 管;explain_style 走的同一条 CDP CSS 域)。
|
|
11
|
-
* 扫 @keyframes / animation / transition / animation-timeline (scroll()/view())
|
|
12
|
-
* / scroll-snap / sticky / will-change / prefers-reduced-motion。
|
|
13
|
-
* 2. 运行时:`document.getAnimations()` —— 浏览器自己的账本:此刻活着的 CSS 动画 /
|
|
14
|
-
* 过渡 / WAAPI,连 keyframes、时长、缓动都有。⚠️ 只列**此刻活着的**,所以排在
|
|
15
|
-
* 滚动对照之后跑(把 reveal 都触发出来再查)。GSAP 不走 WAAPI(它直接写 inline
|
|
16
|
-
* style),靠第 3、4 路。
|
|
17
|
-
* 3. 库指纹:全局对象 + script src + DOM 属性(data-aos / data-scroll …)。GSAP 的
|
|
18
|
-
* ScrollTrigger 挂在全局时能直接读出每个 trigger 的 start/end/scrub/pin。
|
|
19
|
-
* 4. 滚动对照(真滚轮):记一遍块元素的 opacity/transform,滚到中、滚到底、滚回顶
|
|
20
|
-
* 各记一遍 —— 回顶后仍变了的 = 滚动触发的入场(class 切换型 reveal);中途变了
|
|
21
|
-
* 回顶又复原的 = 随滚动位置走的(scrub / 视差)。顺带量出派发的滚轮像素 vs 页面
|
|
22
|
-
* 真滚的像素 —— 不一致就是滚动劫持(Lenis / Locomotive 那一族)。
|
|
23
|
-
*
|
|
24
|
-
* 看不见的部分(工具绕不过去,描述里要说):canvas/WebGL 里的动画 DOM 层零痕迹,只有
|
|
25
|
-
* 胶片条能看;GSAP 没挂全局时只看得到结果看不到时间轴;class 名压缩过的站报不出语义名。
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
const MAX_SHEET_BYTES = 900_000; // 单张样式表上限(minified 大包也够)
|
|
29
|
-
const MAX_TOTAL_BYTES = 3_000_000;
|
|
30
|
-
const SAMPLE_CAP = 24; // 每类最多留几条样本
|
|
31
|
-
const CANDIDATE_CAP = 400; // 滚动对照采的块元素上限
|
|
32
|
-
const SCROLL_BUDGET_MS = 6000;
|
|
33
|
-
|
|
34
|
-
// ── 纯函数:CSS 文本扫描(可单测)──────────────────────────────
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 把 CSS 文本拆成 {prelude, body, depthPath} 规则列表。自己写的小括号计数器,
|
|
38
|
-
* 不引 CSS 解析库(要的只是"谁声明了什么",不是完整 AST)。
|
|
39
|
-
*/
|
|
40
|
-
export function splitRules(css) {
|
|
41
|
-
const out = [];
|
|
42
|
-
const stack = []; // 外层 @media / @supports 的 prelude
|
|
43
|
-
let i = 0; const n = css.length;
|
|
44
|
-
let buf = '';
|
|
45
|
-
const skipComment = () => {
|
|
46
|
-
if (css[i] === '/' && css[i + 1] === '*') {
|
|
47
|
-
const e = css.indexOf('*/', i + 2);
|
|
48
|
-
i = e < 0 ? n : e + 2;
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
};
|
|
53
|
-
while (i < n) {
|
|
54
|
-
if (skipComment()) continue;
|
|
55
|
-
const ch = css[i];
|
|
56
|
-
if (ch === '{') {
|
|
57
|
-
const prelude = buf.trim(); buf = '';
|
|
58
|
-
// @media / @supports / @layer / @container:进一层;其余(含 @keyframes)整块取到配对 }
|
|
59
|
-
if (/^@(media|supports|layer|container|scope)\b/i.test(prelude)) {
|
|
60
|
-
stack.push(prelude); i += 1; continue;
|
|
61
|
-
}
|
|
62
|
-
let depth = 1; let j = i + 1; let body = '';
|
|
63
|
-
while (j < n && depth > 0) {
|
|
64
|
-
if (css[j] === '/' && css[j + 1] === '*') { const e = css.indexOf('*/', j + 2); j = e < 0 ? n : e + 2; continue; }
|
|
65
|
-
if (css[j] === '{') depth += 1;
|
|
66
|
-
else if (css[j] === '}') { depth -= 1; if (depth === 0) break; }
|
|
67
|
-
body += css[j]; j += 1;
|
|
68
|
-
}
|
|
69
|
-
out.push({ prelude, body, context: stack.slice() });
|
|
70
|
-
i = j + 1;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (ch === '}') { stack.pop(); buf = ''; i += 1; continue; }
|
|
74
|
-
buf += ch; i += 1;
|
|
75
|
-
}
|
|
76
|
-
return out;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 规则列表 → 分类计数 + 样本。sheetName 只用来标注来源。 */
|
|
80
|
-
export function scanCss(css, sheetName = '') {
|
|
81
|
-
const r = {
|
|
82
|
-
keyframes: [], animations: [], transitions: [], scrollTimeline: [], scrollSnap: [],
|
|
83
|
-
sticky: [], willChange: 0, reducedMotion: false, viewTransitions: 0,
|
|
84
|
-
};
|
|
85
|
-
const push = (arr, item) => { if (arr.length < SAMPLE_CAP) arr.push(item); arr.total = (arr.total || 0) + 1; };
|
|
86
|
-
const decl = (body, prop) => {
|
|
87
|
-
const m = body.match(new RegExp(`(?:^|[;\\s])${prop}\\s*:\\s*([^;]+)`, 'i'));
|
|
88
|
-
return m ? m[1].trim().slice(0, 120) : null;
|
|
89
|
-
};
|
|
90
|
-
for (const rule of splitRules(css)) {
|
|
91
|
-
const ctx = rule.context.join(' › ');
|
|
92
|
-
if (/prefers-reduced-motion/i.test(ctx) || /prefers-reduced-motion/i.test(rule.prelude)) r.reducedMotion = true;
|
|
93
|
-
if (/^@keyframes\s+/i.test(rule.prelude)) {
|
|
94
|
-
const name = rule.prelude.replace(/^@keyframes\s+/i, '').trim();
|
|
95
|
-
const steps = (rule.body.match(/(?:^|})\s*(?:from|to|[\d.]+%)(?:\s*,\s*(?:from|to|[\d.]+%))*\s*{/g) || []).length;
|
|
96
|
-
push(r.keyframes, { name, steps, sheet: sheetName });
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (/^@view-transition/i.test(rule.prelude)) { r.viewTransitions += 1; continue; }
|
|
100
|
-
if (rule.prelude.startsWith('@')) continue;
|
|
101
|
-
const b = rule.body;
|
|
102
|
-
const sel = rule.prelude.slice(0, 80);
|
|
103
|
-
const anim = decl(b, 'animation') || decl(b, 'animation-name');
|
|
104
|
-
if (anim) push(r.animations, { selector: sel, animation: anim, context: ctx || null, sheet: sheetName });
|
|
105
|
-
const tr = decl(b, 'transition') || decl(b, 'transition-property');
|
|
106
|
-
if (tr && !/^(none|all 0s)/i.test(tr)) push(r.transitions, { selector: sel, transition: tr, context: ctx || null, sheet: sheetName });
|
|
107
|
-
const tl = decl(b, 'animation-timeline') || decl(b, 'scroll-timeline') || decl(b, 'view-timeline') || decl(b, 'scroll-timeline-name') || decl(b, 'view-timeline-name');
|
|
108
|
-
if (tl) push(r.scrollTimeline, { selector: sel, timeline: tl, sheet: sheetName });
|
|
109
|
-
const snap = decl(b, 'scroll-snap-type');
|
|
110
|
-
if (snap && !/^none/i.test(snap)) push(r.scrollSnap, { selector: sel, value: snap, sheet: sheetName });
|
|
111
|
-
const pos = decl(b, 'position');
|
|
112
|
-
if (pos && /sticky/i.test(pos)) push(r.sticky, { selector: sel, sheet: sheetName });
|
|
113
|
-
if (decl(b, 'will-change')) r.willChange += 1;
|
|
114
|
-
}
|
|
115
|
-
return r;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** 多张表的扫描结果并起来 */
|
|
119
|
-
export function mergeScans(scans) {
|
|
120
|
-
const m = { keyframes: [], animations: [], transitions: [], scrollTimeline: [], scrollSnap: [], sticky: [], willChange: 0, reducedMotion: false, viewTransitions: 0 };
|
|
121
|
-
const cat = (k) => { for (const s of scans) { m[k].push(...s[k]); m[k].total = (m[k].total || 0) + (s[k].total || 0); } m[k] = Object.assign(m[k].slice(0, SAMPLE_CAP), { total: m[k].total || 0 }); };
|
|
122
|
-
for (const k of ['keyframes', 'animations', 'transitions', 'scrollTimeline', 'scrollSnap', 'sticky']) cat(k);
|
|
123
|
-
for (const s of scans) { m.willChange += s.willChange; m.reducedMotion = m.reducedMotion || s.reducedMotion; m.viewTransitions += s.viewTransitions; }
|
|
124
|
-
return m;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── 页面里跑的函数(⚠️ 浏览器上下文,不能引外面的东西)────────────
|
|
128
|
-
|
|
129
|
-
function pageRuntimeAnimations() {
|
|
130
|
-
const selOf = (el) => {
|
|
131
|
-
if (!el || !el.tagName) return String(el);
|
|
132
|
-
const cls = typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}` : '';
|
|
133
|
-
return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${cls}`;
|
|
134
|
-
};
|
|
135
|
-
const list = typeof document.getAnimations === 'function' ? document.getAnimations() : [];
|
|
136
|
-
const out = [];
|
|
137
|
-
for (const a of list.slice(0, 60)) {
|
|
138
|
-
try {
|
|
139
|
-
const t = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};
|
|
140
|
-
const kf = a.effect && a.effect.getKeyframes ? a.effect.getKeyframes() : [];
|
|
141
|
-
const strip = (k) => { const o = {}; for (const p in k) if (!['offset', 'computedOffset', 'easing', 'composite'].includes(p)) o[p] = String(k[p]).slice(0, 40); return o; };
|
|
142
|
-
out.push({
|
|
143
|
-
kind: a.constructor && a.constructor.name,
|
|
144
|
-
name: a.animationName || a.transitionProperty || null,
|
|
145
|
-
target: selOf(a.effect && a.effect.target),
|
|
146
|
-
duration: t.duration, delay: t.delay, easing: t.easing, iterations: t.iterations, direction: t.direction,
|
|
147
|
-
state: a.playState,
|
|
148
|
-
from: kf.length ? strip(kf[0]) : null, to: kf.length > 1 ? strip(kf[kf.length - 1]) : null,
|
|
149
|
-
timeline: a.timeline && a.timeline.constructor ? a.timeline.constructor.name : null,
|
|
150
|
-
});
|
|
151
|
-
} catch { /* 某条读不出来就跳过 */ }
|
|
152
|
-
}
|
|
153
|
-
return { count: list.length, items: out };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function pageLibs() {
|
|
157
|
-
const w = window;
|
|
158
|
-
const libs = [];
|
|
159
|
-
const has = (name, cond, ev) => { try { if (cond()) libs.push({ name, evidence: ev }); } catch { /* */ } };
|
|
160
|
-
has('gsap', () => w.gsap, `gsap ${w.gsap && w.gsap.version}`);
|
|
161
|
-
has('ScrollTrigger', () => w.ScrollTrigger, 'window.ScrollTrigger');
|
|
162
|
-
has('Lenis', () => w.Lenis || w.lenis || document.documentElement.classList.contains('lenis'), 'window.Lenis / html.lenis');
|
|
163
|
-
has('LocomotiveScroll', () => w.LocomotiveScroll || document.querySelector('[data-scroll-container],[data-scroll-section]'), 'LocomotiveScroll / data-scroll-*');
|
|
164
|
-
has('AOS', () => w.AOS || document.querySelector('[data-aos]'), 'AOS / data-aos');
|
|
165
|
-
has('Swiper', () => w.Swiper || document.querySelector('.swiper'), 'Swiper / .swiper');
|
|
166
|
-
has('three.js', () => w.THREE || document.querySelector('canvas[data-engine*="three"]'), 'window.THREE');
|
|
167
|
-
has('framer-motion', () => document.querySelector('[data-framer-component-type],[data-framer-name],[data-projection-id]'), 'data-framer-* / data-projection-id');
|
|
168
|
-
has('anime.js', () => w.anime, 'window.anime');
|
|
169
|
-
has('Motion One', () => w.Motion, 'window.Motion');
|
|
170
|
-
has('barba/swup(页面转场)', () => w.barba || w.swup || document.querySelector('[data-barba]'), 'barba / swup');
|
|
171
|
-
has('lottie', () => w.lottie || w.bodymovin || document.querySelector('lottie-player,[data-lottie]'), 'lottie');
|
|
172
|
-
has('Spline/rive/WebGL canvas', () => document.querySelector('canvas[data-engine],canvas.webgl,canvas[data-rive]'), 'canvas');
|
|
173
|
-
const srcs = [...document.scripts].map(s => s.src).filter(Boolean).map(s => { try { return new URL(s).pathname.split('/').pop(); } catch { return s; } });
|
|
174
|
-
const hint = srcs.filter(s => /gsap|scrolltrigger|lenis|locomotive|aos|swiper|three|framer|anime|lottie|splide|flickity|barba|swup|motion/i.test(s)).slice(0, 12);
|
|
175
|
-
const st = [];
|
|
176
|
-
try {
|
|
177
|
-
if (w.ScrollTrigger && typeof w.ScrollTrigger.getAll === 'function') {
|
|
178
|
-
for (const t of w.ScrollTrigger.getAll().slice(0, 24)) {
|
|
179
|
-
const v = t.vars || {};
|
|
180
|
-
const tsel = (el) => el && el.tagName ? `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/)[0]}` : ''}` : String(el || '');
|
|
181
|
-
st.push({ trigger: tsel(t.trigger), start: String(v.start ?? '').slice(0, 40), end: String(v.end ?? '').slice(0, 40), scrub: v.scrub ?? false, pin: !!v.pin, snap: !!v.snap, markers: !!v.markers });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
} catch { /* */ }
|
|
185
|
-
let gsapTweens = null;
|
|
186
|
-
try { if (w.gsap && w.gsap.globalTimeline) gsapTweens = w.gsap.globalTimeline.getChildren(true, true, true).length; } catch { /* */ }
|
|
187
|
-
return { libs, scriptHints: hint, scrollTriggers: st, gsapTweens,
|
|
188
|
-
canvases: document.querySelectorAll('canvas').length,
|
|
189
|
-
videos: document.querySelectorAll('video[autoplay], video').length,
|
|
190
|
-
reducedMotionPref: w.matchMedia && w.matchMedia('(prefers-reduced-motion: reduce)').matches };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** 滚动对照·第一步:登记候选块元素,记快照 A */
|
|
194
|
-
function pageSnapshotInit(cap) {
|
|
195
|
-
const cand = [];
|
|
196
|
-
const SEL = 'section, article, header, footer, main > *, h1, h2, h3, figure, img, li, [class*="reveal"], [class*="fade"], [class*="anim"], [data-aos], [data-scroll], div';
|
|
197
|
-
for (const el of document.querySelectorAll(SEL)) {
|
|
198
|
-
if (cand.length >= cap) break;
|
|
199
|
-
const r = el.getBoundingClientRect();
|
|
200
|
-
if (r.width < 120 || r.height < 40) continue;
|
|
201
|
-
cand.push(el);
|
|
202
|
-
}
|
|
203
|
-
const snap = (el) => { const cs = getComputedStyle(el); const r = el.getBoundingClientRect(); return { o: cs.opacity, t: cs.transform, f: cs.filter, cp: cs.clipPath, y: Math.round(r.top + window.scrollY), tr: cs.transitionProperty !== 'all' || cs.transitionDuration !== '0s' ? `${cs.transitionProperty} ${cs.transitionDuration} ${cs.transitionTimingFunction}`.slice(0, 80) : null, an: cs.animationName !== 'none' ? cs.animationName : null }; };
|
|
204
|
-
window.__ndInv = { els: cand, A: cand.map(snap), snap, sel: (el) => { const cls = typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}` : ''; return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${cls}`; } };
|
|
205
|
-
return { count: cand.length, docH: document.documentElement.scrollHeight, vh: window.innerHeight, y0: window.scrollY };
|
|
206
|
-
}
|
|
207
|
-
function pageSnapshotTake(label) {
|
|
208
|
-
const st = window.__ndInv; if (!st) return null;
|
|
209
|
-
st[label] = st.els.map(st.snap);
|
|
210
|
-
return window.scrollY;
|
|
211
|
-
}
|
|
212
|
-
function pageSnapshotReport() {
|
|
213
|
-
const st = window.__ndInv; if (!st) return null;
|
|
214
|
-
// identity matrix 和 none 是同一件事;透明度抖 0.02 不算变(真跑逮到 0.979→1 这种噪音)
|
|
215
|
-
const normT = (t) => (!t || t === 'none' || /^matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)$/.test(t) || /^matrix3d\(1,\s*0,\s*0,\s*0,\s*0,\s*1,\s*0,\s*0,\s*0,\s*0,\s*1,\s*0,\s*0,\s*0,\s*0,\s*1\)$/.test(t)) ? 'none' : t;
|
|
216
|
-
const changed = (a, b) => Math.abs(Number(a.o) - Number(b.o)) > 0.05 || normT(a.t) !== normT(b.t) || a.f !== b.f || a.cp !== b.cp;
|
|
217
|
-
const reveals = []; const scrubs = [];
|
|
218
|
-
for (let i = 0; i < st.els.length; i += 1) {
|
|
219
|
-
const A = st.A[i]; const B = st.B && st.B[i]; const C = st.C && st.C[i]; const D = st.D && st.D[i];
|
|
220
|
-
if (!D) continue;
|
|
221
|
-
const el = st.els[i];
|
|
222
|
-
if (changed(A, D)) {
|
|
223
|
-
if (reveals.length < 30) reveals.push({ target: st.sel(el), docY: A.y, from: { opacity: A.o, transform: A.t === 'none' ? 'none' : A.t.slice(0, 60) }, to: { opacity: D.o, transform: D.t === 'none' ? 'none' : D.t.slice(0, 60) }, transition: D.tr, animation: D.an });
|
|
224
|
-
reveals.total = (reveals.total || 0) + 1;
|
|
225
|
-
} else if ((B && changed(A, B)) || (C && changed(A, C))) {
|
|
226
|
-
if (scrubs.length < 30) scrubs.push({ target: st.sel(el), docY: A.y, midTransform: B ? B.t.slice(0, 60) : null, midOpacity: B ? B.o : null });
|
|
227
|
-
scrubs.total = (scrubs.total || 0) + 1;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const out = { candidates: st.els.length, reveals, revealsTotal: reveals.total || 0, scrubs, scrubsTotal: scrubs.total || 0 };
|
|
231
|
-
delete window.__ndInv;
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ── 主流程 ───────────────────────────────────────────────────
|
|
236
|
-
|
|
237
|
-
async function readStylesheets(page) {
|
|
238
|
-
const cdp = await page.context().newCDPSession(page);
|
|
239
|
-
const headers = new Map();
|
|
240
|
-
cdp.on('CSS.styleSheetAdded', (ev) => { if (ev?.header?.styleSheetId) headers.set(ev.header.styleSheetId, ev.header); });
|
|
241
|
-
await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
|
|
242
|
-
await page.waitForTimeout(120);
|
|
243
|
-
const sheets = []; let total = 0; let skipped = 0;
|
|
244
|
-
for (const [id, h] of headers) {
|
|
245
|
-
if (total > MAX_TOTAL_BYTES) { skipped += 1; continue; }
|
|
246
|
-
try {
|
|
247
|
-
const { text } = await cdp.send('CSS.getStyleSheetText', { styleSheetId: id });
|
|
248
|
-
if (!text) continue;
|
|
249
|
-
const name = h.isInline ? '<style>' : (h.sourceURL ? decodeURIComponent((h.sourceURL.split('?')[0]).split('/').pop() || h.sourceURL) : '(anonymous)');
|
|
250
|
-
sheets.push({ name, bytes: text.length, text: text.slice(0, MAX_SHEET_BYTES) });
|
|
251
|
-
total += text.length;
|
|
252
|
-
} catch { skipped += 1; }
|
|
253
|
-
}
|
|
254
|
-
await cdp.detach().catch(() => {});
|
|
255
|
-
return { sheets, total, skipped };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function scrollProbe(page) {
|
|
259
|
-
const init = await page.evaluate(pageSnapshotInit, CANDIDATE_CAP);
|
|
260
|
-
const vw = page.viewportSize() || { width: 1280, height: 800 };
|
|
261
|
-
await page.mouse.move(Math.floor(vw.width / 2), Math.floor(vw.height / 2));
|
|
262
|
-
const maxY = Math.max(0, init.docH - init.vh);
|
|
263
|
-
const step = Math.max(120, Math.round(init.vh * 0.35));
|
|
264
|
-
const t0 = Date.now();
|
|
265
|
-
let dispatched = 0; let midTaken = false;
|
|
266
|
-
// 真滚轮:跟用户一样的输入路径,Lenis 那族劫持会在这里现形
|
|
267
|
-
while (Date.now() - t0 < SCROLL_BUDGET_MS) {
|
|
268
|
-
await page.mouse.wheel(0, step); dispatched += step;
|
|
269
|
-
await page.waitForTimeout(70);
|
|
270
|
-
const y = await page.evaluate(() => window.scrollY);
|
|
271
|
-
if (!midTaken && y >= maxY / 2) { await page.evaluate(pageSnapshotTake, 'B'); midTaken = true; }
|
|
272
|
-
if (y >= maxY - 2 || dispatched > maxY + init.vh * 2) break;
|
|
273
|
-
}
|
|
274
|
-
await page.waitForTimeout(300);
|
|
275
|
-
const yBottom = await page.evaluate(pageSnapshotTake, 'C');
|
|
276
|
-
const hijack = dispatched > 0 && maxY > 200 && yBottom < Math.min(maxY, dispatched) * 0.6;
|
|
277
|
-
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
|
|
278
|
-
await page.waitForTimeout(400);
|
|
279
|
-
await page.evaluate(pageSnapshotTake, 'D');
|
|
280
|
-
const rep = await page.evaluate(pageSnapshotReport);
|
|
281
|
-
return { ...rep, docHeight: init.docH, viewportHeight: init.vh, dispatchedPx: dispatched, reachedY: yBottom, hijackSuspected: hijack, tookMs: Date.now() - t0 };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @param {import('playwright').Page} page
|
|
286
|
-
* @param {{scrollProbe?: boolean}} [opts]
|
|
287
|
-
*/
|
|
288
|
-
export async function collectMotionInventory(page, { scrollProbe: doScroll = true } = {}) {
|
|
289
|
-
const inv = { url: page.url(), errors: [] };
|
|
290
|
-
try {
|
|
291
|
-
const { sheets, total, skipped } = await readStylesheets(page);
|
|
292
|
-
inv.stylesheets = { count: sheets.length, bytes: total, skipped, names: sheets.map(s => s.name).slice(0, 20) };
|
|
293
|
-
inv.css = mergeScans(sheets.map(s => scanCss(s.text, s.name)));
|
|
294
|
-
} catch (err) { inv.errors.push(`stylesheets: ${err.message.split('\n')[0]}`); }
|
|
295
|
-
try { inv.libs = await page.evaluate(pageLibs); } catch (err) { inv.errors.push(`libs: ${err.message.split('\n')[0]}`); }
|
|
296
|
-
if (doScroll) {
|
|
297
|
-
try { inv.scroll = await scrollProbe(page); } catch (err) { inv.errors.push(`scroll probe: ${err.message.split('\n')[0]}`); }
|
|
298
|
-
}
|
|
299
|
-
// 滚动对照把 reveal 都触发过了,这时再读运行时账本最全
|
|
300
|
-
try { inv.runtime = await page.evaluate(pageRuntimeAnimations); } catch (err) { inv.errors.push(`getAnimations: ${err.message.split('\n')[0]}`); }
|
|
301
|
-
return inv;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ── 给 agent 的人话 ──────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
export function formatMotionInventory(inv) {
|
|
307
|
-
const L = [];
|
|
308
|
-
const c = inv.css; const l = inv.libs; const s = inv.scroll; const r = inv.runtime;
|
|
309
|
-
if (l) {
|
|
310
|
-
const names = l.libs.map(x => x.name);
|
|
311
|
-
L.push(`动效库:${names.length ? names.join('、') : '没探到已知库(原生 CSS / 自写 JS)'}`
|
|
312
|
-
+ (l.gsapTweens != null ? `;GSAP 时间轴上 ${l.gsapTweens} 个 tween` : '')
|
|
313
|
-
+ (l.canvases ? `;${l.canvases} 个 <canvas>(里面的动画 DOM 层看不见,要看用胶片条)` : '')
|
|
314
|
-
+ (l.scriptHints.length ? `;脚本名线索:${l.scriptHints.join(' ')}` : ''));
|
|
315
|
-
if (l.scrollTriggers.length) {
|
|
316
|
-
L.push(`ScrollTrigger ×${l.scrollTriggers.length}:` + l.scrollTriggers.slice(0, 6).map(t => `${t.trigger}[${t.start}→${t.end}${t.scrub ? ` scrub=${t.scrub}` : ''}${t.pin ? ' pin' : ''}]`).join(' · '));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (c) {
|
|
320
|
-
const bits = [];
|
|
321
|
-
if (c.keyframes.total) bits.push(`@keyframes ×${c.keyframes.total}(${c.keyframes.slice(0, 8).map(k => k.name).join(' ')})`);
|
|
322
|
-
if (c.animations.total) bits.push(`animation 声明 ×${c.animations.total}`);
|
|
323
|
-
if (c.transitions.total) bits.push(`transition 声明 ×${c.transitions.total}`);
|
|
324
|
-
if (c.scrollTimeline.total) bits.push(`⭐ CSS 滚动驱动动画(animation-timeline/scroll()/view())×${c.scrollTimeline.total}`);
|
|
325
|
-
if (c.scrollSnap.total) bits.push(`scroll-snap ×${c.scrollSnap.total}(${c.scrollSnap.slice(0, 3).map(x => `${x.selector}: ${x.value}`).join('; ')})`);
|
|
326
|
-
if (c.sticky.total) bits.push(`sticky ×${c.sticky.total}`);
|
|
327
|
-
if (c.willChange) bits.push(`will-change ×${c.willChange}`);
|
|
328
|
-
if (c.viewTransitions) bits.push(`@view-transition ×${c.viewTransitions}`);
|
|
329
|
-
L.push(`CSS(${inv.stylesheets?.count ?? 0} 张表,${((inv.stylesheets?.bytes ?? 0) / 1024).toFixed(0)}KB):${bits.length ? bits.join(';') : '没有动画/过渡声明'}`
|
|
330
|
-
+ (c.reducedMotion ? ';有 prefers-reduced-motion 分支' : ';没做 prefers-reduced-motion'));
|
|
331
|
-
if (c.transitions.length) L.push(' 典型过渡:' + c.transitions.slice(0, 4).map(t => `${t.selector} {${t.transition}}`).join(' · '));
|
|
332
|
-
if (c.animations.length) L.push(' 典型动画:' + c.animations.slice(0, 4).map(a => `${a.selector} {${a.animation}}`).join(' · '));
|
|
333
|
-
if (c.scrollTimeline.length) L.push(' 滚动驱动:' + c.scrollTimeline.slice(0, 4).map(a => `${a.selector} {${a.timeline}}`).join(' · '));
|
|
334
|
-
}
|
|
335
|
-
if (s) {
|
|
336
|
-
L.push(`滚动对照(真滚轮滚了 ${s.dispatchedPx}px,页面到 ${s.reachedY}/${Math.max(0, s.docHeight - s.viewportHeight)}):`
|
|
337
|
-
+ `入场 reveal ${s.revealsTotal} 个 · 随滚动位置走的(scrub/视差)${s.scrubsTotal} 个`
|
|
338
|
-
+ (s.hijackSuspected ? ' · ⚠ 疑似滚动劫持(派发的滚轮像素远多于页面真滚的)' : ''));
|
|
339
|
-
// 滚完被拆掉/display:none 的元素(典型:开场幕布 lift 后 remove)computed 全空 —— 说"没了",别打印空串
|
|
340
|
-
const st8 = (v) => (v.opacity === '' || v.opacity == null) && (!v.transform || v.transform === '') ? '(removed/hidden)' : `${v.opacity}/${v.transform}`;
|
|
341
|
-
if (s.reveals.length) L.push(' reveal 样本:' + s.reveals.slice(0, 5).map(x => `${x.target} ${st8(x.from)}→${st8(x.to)}${x.transition ? ` [${x.transition}]` : ''}${x.animation ? ` [anim ${x.animation}]` : ''}`).join(' · '));
|
|
342
|
-
if (s.scrubs.length) L.push(' scrub/视差样本:' + s.scrubs.slice(0, 5).map(x => `${x.target}(滚到一半时 ${x.midTransform})`).join(' · '));
|
|
343
|
-
}
|
|
344
|
-
if (r) {
|
|
345
|
-
L.push(`运行时(getAnimations,滚过一遍后):${r.count} 条活着的` + (r.items.length ? ':' + r.items.slice(0, 6).map(a => `${a.target} ${a.name || a.kind} ${a.duration}ms ${a.easing}${a.iterations === Infinity ? ' ∞' : ''}`).join(' · ') : ''));
|
|
346
|
-
}
|
|
347
|
-
if (inv.errors?.length) L.push(`(这几路没采到:${inv.errors.join(';')})`);
|
|
348
|
-
return L;
|
|
349
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/motion/inventory.js — 一页"靠什么在动"的静态清单(2026-08-21)
|
|
3
|
+
*
|
|
4
|
+
* 回答的是"这站用了哪些 CSS 滚动动画 / 特殊样式动画、靠什么库",不是"动起来好不好看"
|
|
5
|
+
* (那是 motion-lab 的胶片条和元素探针的活)。**对页面无知**:浏览通道看参考站、
|
|
6
|
+
* 产物会话查自己的站,都是同一份。
|
|
7
|
+
*
|
|
8
|
+
* 四路来源,各自独立 try(一路挂了不带走别的):
|
|
9
|
+
* 1. 样式表原文:CDP `CSS.getStyleSheetText` —— 跨域 CDN 的样式表也拿得到
|
|
10
|
+
* (浏览器自己的资源,不受 CORS 管;explain_style 走的同一条 CDP CSS 域)。
|
|
11
|
+
* 扫 @keyframes / animation / transition / animation-timeline (scroll()/view())
|
|
12
|
+
* / scroll-snap / sticky / will-change / prefers-reduced-motion。
|
|
13
|
+
* 2. 运行时:`document.getAnimations()` —— 浏览器自己的账本:此刻活着的 CSS 动画 /
|
|
14
|
+
* 过渡 / WAAPI,连 keyframes、时长、缓动都有。⚠️ 只列**此刻活着的**,所以排在
|
|
15
|
+
* 滚动对照之后跑(把 reveal 都触发出来再查)。GSAP 不走 WAAPI(它直接写 inline
|
|
16
|
+
* style),靠第 3、4 路。
|
|
17
|
+
* 3. 库指纹:全局对象 + script src + DOM 属性(data-aos / data-scroll …)。GSAP 的
|
|
18
|
+
* ScrollTrigger 挂在全局时能直接读出每个 trigger 的 start/end/scrub/pin。
|
|
19
|
+
* 4. 滚动对照(真滚轮):记一遍块元素的 opacity/transform,滚到中、滚到底、滚回顶
|
|
20
|
+
* 各记一遍 —— 回顶后仍变了的 = 滚动触发的入场(class 切换型 reveal);中途变了
|
|
21
|
+
* 回顶又复原的 = 随滚动位置走的(scrub / 视差)。顺带量出派发的滚轮像素 vs 页面
|
|
22
|
+
* 真滚的像素 —— 不一致就是滚动劫持(Lenis / Locomotive 那一族)。
|
|
23
|
+
*
|
|
24
|
+
* 看不见的部分(工具绕不过去,描述里要说):canvas/WebGL 里的动画 DOM 层零痕迹,只有
|
|
25
|
+
* 胶片条能看;GSAP 没挂全局时只看得到结果看不到时间轴;class 名压缩过的站报不出语义名。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const MAX_SHEET_BYTES = 900_000; // 单张样式表上限(minified 大包也够)
|
|
29
|
+
const MAX_TOTAL_BYTES = 3_000_000;
|
|
30
|
+
const SAMPLE_CAP = 24; // 每类最多留几条样本
|
|
31
|
+
const CANDIDATE_CAP = 400; // 滚动对照采的块元素上限
|
|
32
|
+
const SCROLL_BUDGET_MS = 6000;
|
|
33
|
+
|
|
34
|
+
// ── 纯函数:CSS 文本扫描(可单测)──────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 把 CSS 文本拆成 {prelude, body, depthPath} 规则列表。自己写的小括号计数器,
|
|
38
|
+
* 不引 CSS 解析库(要的只是"谁声明了什么",不是完整 AST)。
|
|
39
|
+
*/
|
|
40
|
+
export function splitRules(css) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const stack = []; // 外层 @media / @supports 的 prelude
|
|
43
|
+
let i = 0; const n = css.length;
|
|
44
|
+
let buf = '';
|
|
45
|
+
const skipComment = () => {
|
|
46
|
+
if (css[i] === '/' && css[i + 1] === '*') {
|
|
47
|
+
const e = css.indexOf('*/', i + 2);
|
|
48
|
+
i = e < 0 ? n : e + 2;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
while (i < n) {
|
|
54
|
+
if (skipComment()) continue;
|
|
55
|
+
const ch = css[i];
|
|
56
|
+
if (ch === '{') {
|
|
57
|
+
const prelude = buf.trim(); buf = '';
|
|
58
|
+
// @media / @supports / @layer / @container:进一层;其余(含 @keyframes)整块取到配对 }
|
|
59
|
+
if (/^@(media|supports|layer|container|scope)\b/i.test(prelude)) {
|
|
60
|
+
stack.push(prelude); i += 1; continue;
|
|
61
|
+
}
|
|
62
|
+
let depth = 1; let j = i + 1; let body = '';
|
|
63
|
+
while (j < n && depth > 0) {
|
|
64
|
+
if (css[j] === '/' && css[j + 1] === '*') { const e = css.indexOf('*/', j + 2); j = e < 0 ? n : e + 2; continue; }
|
|
65
|
+
if (css[j] === '{') depth += 1;
|
|
66
|
+
else if (css[j] === '}') { depth -= 1; if (depth === 0) break; }
|
|
67
|
+
body += css[j]; j += 1;
|
|
68
|
+
}
|
|
69
|
+
out.push({ prelude, body, context: stack.slice() });
|
|
70
|
+
i = j + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '}') { stack.pop(); buf = ''; i += 1; continue; }
|
|
74
|
+
buf += ch; i += 1;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 规则列表 → 分类计数 + 样本。sheetName 只用来标注来源。 */
|
|
80
|
+
export function scanCss(css, sheetName = '') {
|
|
81
|
+
const r = {
|
|
82
|
+
keyframes: [], animations: [], transitions: [], scrollTimeline: [], scrollSnap: [],
|
|
83
|
+
sticky: [], willChange: 0, reducedMotion: false, viewTransitions: 0,
|
|
84
|
+
};
|
|
85
|
+
const push = (arr, item) => { if (arr.length < SAMPLE_CAP) arr.push(item); arr.total = (arr.total || 0) + 1; };
|
|
86
|
+
const decl = (body, prop) => {
|
|
87
|
+
const m = body.match(new RegExp(`(?:^|[;\\s])${prop}\\s*:\\s*([^;]+)`, 'i'));
|
|
88
|
+
return m ? m[1].trim().slice(0, 120) : null;
|
|
89
|
+
};
|
|
90
|
+
for (const rule of splitRules(css)) {
|
|
91
|
+
const ctx = rule.context.join(' › ');
|
|
92
|
+
if (/prefers-reduced-motion/i.test(ctx) || /prefers-reduced-motion/i.test(rule.prelude)) r.reducedMotion = true;
|
|
93
|
+
if (/^@keyframes\s+/i.test(rule.prelude)) {
|
|
94
|
+
const name = rule.prelude.replace(/^@keyframes\s+/i, '').trim();
|
|
95
|
+
const steps = (rule.body.match(/(?:^|})\s*(?:from|to|[\d.]+%)(?:\s*,\s*(?:from|to|[\d.]+%))*\s*{/g) || []).length;
|
|
96
|
+
push(r.keyframes, { name, steps, sheet: sheetName });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (/^@view-transition/i.test(rule.prelude)) { r.viewTransitions += 1; continue; }
|
|
100
|
+
if (rule.prelude.startsWith('@')) continue;
|
|
101
|
+
const b = rule.body;
|
|
102
|
+
const sel = rule.prelude.slice(0, 80);
|
|
103
|
+
const anim = decl(b, 'animation') || decl(b, 'animation-name');
|
|
104
|
+
if (anim) push(r.animations, { selector: sel, animation: anim, context: ctx || null, sheet: sheetName });
|
|
105
|
+
const tr = decl(b, 'transition') || decl(b, 'transition-property');
|
|
106
|
+
if (tr && !/^(none|all 0s)/i.test(tr)) push(r.transitions, { selector: sel, transition: tr, context: ctx || null, sheet: sheetName });
|
|
107
|
+
const tl = decl(b, 'animation-timeline') || decl(b, 'scroll-timeline') || decl(b, 'view-timeline') || decl(b, 'scroll-timeline-name') || decl(b, 'view-timeline-name');
|
|
108
|
+
if (tl) push(r.scrollTimeline, { selector: sel, timeline: tl, sheet: sheetName });
|
|
109
|
+
const snap = decl(b, 'scroll-snap-type');
|
|
110
|
+
if (snap && !/^none/i.test(snap)) push(r.scrollSnap, { selector: sel, value: snap, sheet: sheetName });
|
|
111
|
+
const pos = decl(b, 'position');
|
|
112
|
+
if (pos && /sticky/i.test(pos)) push(r.sticky, { selector: sel, sheet: sheetName });
|
|
113
|
+
if (decl(b, 'will-change')) r.willChange += 1;
|
|
114
|
+
}
|
|
115
|
+
return r;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 多张表的扫描结果并起来 */
|
|
119
|
+
export function mergeScans(scans) {
|
|
120
|
+
const m = { keyframes: [], animations: [], transitions: [], scrollTimeline: [], scrollSnap: [], sticky: [], willChange: 0, reducedMotion: false, viewTransitions: 0 };
|
|
121
|
+
const cat = (k) => { for (const s of scans) { m[k].push(...s[k]); m[k].total = (m[k].total || 0) + (s[k].total || 0); } m[k] = Object.assign(m[k].slice(0, SAMPLE_CAP), { total: m[k].total || 0 }); };
|
|
122
|
+
for (const k of ['keyframes', 'animations', 'transitions', 'scrollTimeline', 'scrollSnap', 'sticky']) cat(k);
|
|
123
|
+
for (const s of scans) { m.willChange += s.willChange; m.reducedMotion = m.reducedMotion || s.reducedMotion; m.viewTransitions += s.viewTransitions; }
|
|
124
|
+
return m;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 页面里跑的函数(⚠️ 浏览器上下文,不能引外面的东西)────────────
|
|
128
|
+
|
|
129
|
+
function pageRuntimeAnimations() {
|
|
130
|
+
const selOf = (el) => {
|
|
131
|
+
if (!el || !el.tagName) return String(el);
|
|
132
|
+
const cls = typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}` : '';
|
|
133
|
+
return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${cls}`;
|
|
134
|
+
};
|
|
135
|
+
const list = typeof document.getAnimations === 'function' ? document.getAnimations() : [];
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const a of list.slice(0, 60)) {
|
|
138
|
+
try {
|
|
139
|
+
const t = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};
|
|
140
|
+
const kf = a.effect && a.effect.getKeyframes ? a.effect.getKeyframes() : [];
|
|
141
|
+
const strip = (k) => { const o = {}; for (const p in k) if (!['offset', 'computedOffset', 'easing', 'composite'].includes(p)) o[p] = String(k[p]).slice(0, 40); return o; };
|
|
142
|
+
out.push({
|
|
143
|
+
kind: a.constructor && a.constructor.name,
|
|
144
|
+
name: a.animationName || a.transitionProperty || null,
|
|
145
|
+
target: selOf(a.effect && a.effect.target),
|
|
146
|
+
duration: t.duration, delay: t.delay, easing: t.easing, iterations: t.iterations, direction: t.direction,
|
|
147
|
+
state: a.playState,
|
|
148
|
+
from: kf.length ? strip(kf[0]) : null, to: kf.length > 1 ? strip(kf[kf.length - 1]) : null,
|
|
149
|
+
timeline: a.timeline && a.timeline.constructor ? a.timeline.constructor.name : null,
|
|
150
|
+
});
|
|
151
|
+
} catch { /* 某条读不出来就跳过 */ }
|
|
152
|
+
}
|
|
153
|
+
return { count: list.length, items: out };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pageLibs() {
|
|
157
|
+
const w = window;
|
|
158
|
+
const libs = [];
|
|
159
|
+
const has = (name, cond, ev) => { try { if (cond()) libs.push({ name, evidence: ev }); } catch { /* */ } };
|
|
160
|
+
has('gsap', () => w.gsap, `gsap ${w.gsap && w.gsap.version}`);
|
|
161
|
+
has('ScrollTrigger', () => w.ScrollTrigger, 'window.ScrollTrigger');
|
|
162
|
+
has('Lenis', () => w.Lenis || w.lenis || document.documentElement.classList.contains('lenis'), 'window.Lenis / html.lenis');
|
|
163
|
+
has('LocomotiveScroll', () => w.LocomotiveScroll || document.querySelector('[data-scroll-container],[data-scroll-section]'), 'LocomotiveScroll / data-scroll-*');
|
|
164
|
+
has('AOS', () => w.AOS || document.querySelector('[data-aos]'), 'AOS / data-aos');
|
|
165
|
+
has('Swiper', () => w.Swiper || document.querySelector('.swiper'), 'Swiper / .swiper');
|
|
166
|
+
has('three.js', () => w.THREE || document.querySelector('canvas[data-engine*="three"]'), 'window.THREE');
|
|
167
|
+
has('framer-motion', () => document.querySelector('[data-framer-component-type],[data-framer-name],[data-projection-id]'), 'data-framer-* / data-projection-id');
|
|
168
|
+
has('anime.js', () => w.anime, 'window.anime');
|
|
169
|
+
has('Motion One', () => w.Motion, 'window.Motion');
|
|
170
|
+
has('barba/swup(页面转场)', () => w.barba || w.swup || document.querySelector('[data-barba]'), 'barba / swup');
|
|
171
|
+
has('lottie', () => w.lottie || w.bodymovin || document.querySelector('lottie-player,[data-lottie]'), 'lottie');
|
|
172
|
+
has('Spline/rive/WebGL canvas', () => document.querySelector('canvas[data-engine],canvas.webgl,canvas[data-rive]'), 'canvas');
|
|
173
|
+
const srcs = [...document.scripts].map(s => s.src).filter(Boolean).map(s => { try { return new URL(s).pathname.split('/').pop(); } catch { return s; } });
|
|
174
|
+
const hint = srcs.filter(s => /gsap|scrolltrigger|lenis|locomotive|aos|swiper|three|framer|anime|lottie|splide|flickity|barba|swup|motion/i.test(s)).slice(0, 12);
|
|
175
|
+
const st = [];
|
|
176
|
+
try {
|
|
177
|
+
if (w.ScrollTrigger && typeof w.ScrollTrigger.getAll === 'function') {
|
|
178
|
+
for (const t of w.ScrollTrigger.getAll().slice(0, 24)) {
|
|
179
|
+
const v = t.vars || {};
|
|
180
|
+
const tsel = (el) => el && el.tagName ? `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/)[0]}` : ''}` : String(el || '');
|
|
181
|
+
st.push({ trigger: tsel(t.trigger), start: String(v.start ?? '').slice(0, 40), end: String(v.end ?? '').slice(0, 40), scrub: v.scrub ?? false, pin: !!v.pin, snap: !!v.snap, markers: !!v.markers });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch { /* */ }
|
|
185
|
+
let gsapTweens = null;
|
|
186
|
+
try { if (w.gsap && w.gsap.globalTimeline) gsapTweens = w.gsap.globalTimeline.getChildren(true, true, true).length; } catch { /* */ }
|
|
187
|
+
return { libs, scriptHints: hint, scrollTriggers: st, gsapTweens,
|
|
188
|
+
canvases: document.querySelectorAll('canvas').length,
|
|
189
|
+
videos: document.querySelectorAll('video[autoplay], video').length,
|
|
190
|
+
reducedMotionPref: w.matchMedia && w.matchMedia('(prefers-reduced-motion: reduce)').matches };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 滚动对照·第一步:登记候选块元素,记快照 A */
|
|
194
|
+
function pageSnapshotInit(cap) {
|
|
195
|
+
const cand = [];
|
|
196
|
+
const SEL = 'section, article, header, footer, main > *, h1, h2, h3, figure, img, li, [class*="reveal"], [class*="fade"], [class*="anim"], [data-aos], [data-scroll], div';
|
|
197
|
+
for (const el of document.querySelectorAll(SEL)) {
|
|
198
|
+
if (cand.length >= cap) break;
|
|
199
|
+
const r = el.getBoundingClientRect();
|
|
200
|
+
if (r.width < 120 || r.height < 40) continue;
|
|
201
|
+
cand.push(el);
|
|
202
|
+
}
|
|
203
|
+
const snap = (el) => { const cs = getComputedStyle(el); const r = el.getBoundingClientRect(); return { o: cs.opacity, t: cs.transform, f: cs.filter, cp: cs.clipPath, y: Math.round(r.top + window.scrollY), tr: cs.transitionProperty !== 'all' || cs.transitionDuration !== '0s' ? `${cs.transitionProperty} ${cs.transitionDuration} ${cs.transitionTimingFunction}`.slice(0, 80) : null, an: cs.animationName !== 'none' ? cs.animationName : null }; };
|
|
204
|
+
window.__ndInv = { els: cand, A: cand.map(snap), snap, sel: (el) => { const cls = typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}` : ''; return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${cls}`; } };
|
|
205
|
+
return { count: cand.length, docH: document.documentElement.scrollHeight, vh: window.innerHeight, y0: window.scrollY };
|
|
206
|
+
}
|
|
207
|
+
function pageSnapshotTake(label) {
|
|
208
|
+
const st = window.__ndInv; if (!st) return null;
|
|
209
|
+
st[label] = st.els.map(st.snap);
|
|
210
|
+
return window.scrollY;
|
|
211
|
+
}
|
|
212
|
+
function pageSnapshotReport() {
|
|
213
|
+
const st = window.__ndInv; if (!st) return null;
|
|
214
|
+
// identity matrix 和 none 是同一件事;透明度抖 0.02 不算变(真跑逮到 0.979→1 这种噪音)
|
|
215
|
+
const normT = (t) => (!t || t === 'none' || /^matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)$/.test(t) || /^matrix3d\(1,\s*0,\s*0,\s*0,\s*0,\s*1,\s*0,\s*0,\s*0,\s*0,\s*1,\s*0,\s*0,\s*0,\s*0,\s*1\)$/.test(t)) ? 'none' : t;
|
|
216
|
+
const changed = (a, b) => Math.abs(Number(a.o) - Number(b.o)) > 0.05 || normT(a.t) !== normT(b.t) || a.f !== b.f || a.cp !== b.cp;
|
|
217
|
+
const reveals = []; const scrubs = [];
|
|
218
|
+
for (let i = 0; i < st.els.length; i += 1) {
|
|
219
|
+
const A = st.A[i]; const B = st.B && st.B[i]; const C = st.C && st.C[i]; const D = st.D && st.D[i];
|
|
220
|
+
if (!D) continue;
|
|
221
|
+
const el = st.els[i];
|
|
222
|
+
if (changed(A, D)) {
|
|
223
|
+
if (reveals.length < 30) reveals.push({ target: st.sel(el), docY: A.y, from: { opacity: A.o, transform: A.t === 'none' ? 'none' : A.t.slice(0, 60) }, to: { opacity: D.o, transform: D.t === 'none' ? 'none' : D.t.slice(0, 60) }, transition: D.tr, animation: D.an });
|
|
224
|
+
reveals.total = (reveals.total || 0) + 1;
|
|
225
|
+
} else if ((B && changed(A, B)) || (C && changed(A, C))) {
|
|
226
|
+
if (scrubs.length < 30) scrubs.push({ target: st.sel(el), docY: A.y, midTransform: B ? B.t.slice(0, 60) : null, midOpacity: B ? B.o : null });
|
|
227
|
+
scrubs.total = (scrubs.total || 0) + 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const out = { candidates: st.els.length, reveals, revealsTotal: reveals.total || 0, scrubs, scrubsTotal: scrubs.total || 0 };
|
|
231
|
+
delete window.__ndInv;
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── 主流程 ───────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
async function readStylesheets(page) {
|
|
238
|
+
const cdp = await page.context().newCDPSession(page);
|
|
239
|
+
const headers = new Map();
|
|
240
|
+
cdp.on('CSS.styleSheetAdded', (ev) => { if (ev?.header?.styleSheetId) headers.set(ev.header.styleSheetId, ev.header); });
|
|
241
|
+
await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
|
|
242
|
+
await page.waitForTimeout(120);
|
|
243
|
+
const sheets = []; let total = 0; let skipped = 0;
|
|
244
|
+
for (const [id, h] of headers) {
|
|
245
|
+
if (total > MAX_TOTAL_BYTES) { skipped += 1; continue; }
|
|
246
|
+
try {
|
|
247
|
+
const { text } = await cdp.send('CSS.getStyleSheetText', { styleSheetId: id });
|
|
248
|
+
if (!text) continue;
|
|
249
|
+
const name = h.isInline ? '<style>' : (h.sourceURL ? decodeURIComponent((h.sourceURL.split('?')[0]).split('/').pop() || h.sourceURL) : '(anonymous)');
|
|
250
|
+
sheets.push({ name, bytes: text.length, text: text.slice(0, MAX_SHEET_BYTES) });
|
|
251
|
+
total += text.length;
|
|
252
|
+
} catch { skipped += 1; }
|
|
253
|
+
}
|
|
254
|
+
await cdp.detach().catch(() => {});
|
|
255
|
+
return { sheets, total, skipped };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function scrollProbe(page) {
|
|
259
|
+
const init = await page.evaluate(pageSnapshotInit, CANDIDATE_CAP);
|
|
260
|
+
const vw = page.viewportSize() || { width: 1280, height: 800 };
|
|
261
|
+
await page.mouse.move(Math.floor(vw.width / 2), Math.floor(vw.height / 2));
|
|
262
|
+
const maxY = Math.max(0, init.docH - init.vh);
|
|
263
|
+
const step = Math.max(120, Math.round(init.vh * 0.35));
|
|
264
|
+
const t0 = Date.now();
|
|
265
|
+
let dispatched = 0; let midTaken = false;
|
|
266
|
+
// 真滚轮:跟用户一样的输入路径,Lenis 那族劫持会在这里现形
|
|
267
|
+
while (Date.now() - t0 < SCROLL_BUDGET_MS) {
|
|
268
|
+
await page.mouse.wheel(0, step); dispatched += step;
|
|
269
|
+
await page.waitForTimeout(70);
|
|
270
|
+
const y = await page.evaluate(() => window.scrollY);
|
|
271
|
+
if (!midTaken && y >= maxY / 2) { await page.evaluate(pageSnapshotTake, 'B'); midTaken = true; }
|
|
272
|
+
if (y >= maxY - 2 || dispatched > maxY + init.vh * 2) break;
|
|
273
|
+
}
|
|
274
|
+
await page.waitForTimeout(300);
|
|
275
|
+
const yBottom = await page.evaluate(pageSnapshotTake, 'C');
|
|
276
|
+
const hijack = dispatched > 0 && maxY > 200 && yBottom < Math.min(maxY, dispatched) * 0.6;
|
|
277
|
+
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
|
|
278
|
+
await page.waitForTimeout(400);
|
|
279
|
+
await page.evaluate(pageSnapshotTake, 'D');
|
|
280
|
+
const rep = await page.evaluate(pageSnapshotReport);
|
|
281
|
+
return { ...rep, docHeight: init.docH, viewportHeight: init.vh, dispatchedPx: dispatched, reachedY: yBottom, hijackSuspected: hijack, tookMs: Date.now() - t0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {import('playwright').Page} page
|
|
286
|
+
* @param {{scrollProbe?: boolean}} [opts]
|
|
287
|
+
*/
|
|
288
|
+
export async function collectMotionInventory(page, { scrollProbe: doScroll = true } = {}) {
|
|
289
|
+
const inv = { url: page.url(), errors: [] };
|
|
290
|
+
try {
|
|
291
|
+
const { sheets, total, skipped } = await readStylesheets(page);
|
|
292
|
+
inv.stylesheets = { count: sheets.length, bytes: total, skipped, names: sheets.map(s => s.name).slice(0, 20) };
|
|
293
|
+
inv.css = mergeScans(sheets.map(s => scanCss(s.text, s.name)));
|
|
294
|
+
} catch (err) { inv.errors.push(`stylesheets: ${err.message.split('\n')[0]}`); }
|
|
295
|
+
try { inv.libs = await page.evaluate(pageLibs); } catch (err) { inv.errors.push(`libs: ${err.message.split('\n')[0]}`); }
|
|
296
|
+
if (doScroll) {
|
|
297
|
+
try { inv.scroll = await scrollProbe(page); } catch (err) { inv.errors.push(`scroll probe: ${err.message.split('\n')[0]}`); }
|
|
298
|
+
}
|
|
299
|
+
// 滚动对照把 reveal 都触发过了,这时再读运行时账本最全
|
|
300
|
+
try { inv.runtime = await page.evaluate(pageRuntimeAnimations); } catch (err) { inv.errors.push(`getAnimations: ${err.message.split('\n')[0]}`); }
|
|
301
|
+
return inv;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── 给 agent 的人话 ──────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
export function formatMotionInventory(inv) {
|
|
307
|
+
const L = [];
|
|
308
|
+
const c = inv.css; const l = inv.libs; const s = inv.scroll; const r = inv.runtime;
|
|
309
|
+
if (l) {
|
|
310
|
+
const names = l.libs.map(x => x.name);
|
|
311
|
+
L.push(`动效库:${names.length ? names.join('、') : '没探到已知库(原生 CSS / 自写 JS)'}`
|
|
312
|
+
+ (l.gsapTweens != null ? `;GSAP 时间轴上 ${l.gsapTweens} 个 tween` : '')
|
|
313
|
+
+ (l.canvases ? `;${l.canvases} 个 <canvas>(里面的动画 DOM 层看不见,要看用胶片条)` : '')
|
|
314
|
+
+ (l.scriptHints.length ? `;脚本名线索:${l.scriptHints.join(' ')}` : ''));
|
|
315
|
+
if (l.scrollTriggers.length) {
|
|
316
|
+
L.push(`ScrollTrigger ×${l.scrollTriggers.length}:` + l.scrollTriggers.slice(0, 6).map(t => `${t.trigger}[${t.start}→${t.end}${t.scrub ? ` scrub=${t.scrub}` : ''}${t.pin ? ' pin' : ''}]`).join(' · '));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (c) {
|
|
320
|
+
const bits = [];
|
|
321
|
+
if (c.keyframes.total) bits.push(`@keyframes ×${c.keyframes.total}(${c.keyframes.slice(0, 8).map(k => k.name).join(' ')})`);
|
|
322
|
+
if (c.animations.total) bits.push(`animation 声明 ×${c.animations.total}`);
|
|
323
|
+
if (c.transitions.total) bits.push(`transition 声明 ×${c.transitions.total}`);
|
|
324
|
+
if (c.scrollTimeline.total) bits.push(`⭐ CSS 滚动驱动动画(animation-timeline/scroll()/view())×${c.scrollTimeline.total}`);
|
|
325
|
+
if (c.scrollSnap.total) bits.push(`scroll-snap ×${c.scrollSnap.total}(${c.scrollSnap.slice(0, 3).map(x => `${x.selector}: ${x.value}`).join('; ')})`);
|
|
326
|
+
if (c.sticky.total) bits.push(`sticky ×${c.sticky.total}`);
|
|
327
|
+
if (c.willChange) bits.push(`will-change ×${c.willChange}`);
|
|
328
|
+
if (c.viewTransitions) bits.push(`@view-transition ×${c.viewTransitions}`);
|
|
329
|
+
L.push(`CSS(${inv.stylesheets?.count ?? 0} 张表,${((inv.stylesheets?.bytes ?? 0) / 1024).toFixed(0)}KB):${bits.length ? bits.join(';') : '没有动画/过渡声明'}`
|
|
330
|
+
+ (c.reducedMotion ? ';有 prefers-reduced-motion 分支' : ';没做 prefers-reduced-motion'));
|
|
331
|
+
if (c.transitions.length) L.push(' 典型过渡:' + c.transitions.slice(0, 4).map(t => `${t.selector} {${t.transition}}`).join(' · '));
|
|
332
|
+
if (c.animations.length) L.push(' 典型动画:' + c.animations.slice(0, 4).map(a => `${a.selector} {${a.animation}}`).join(' · '));
|
|
333
|
+
if (c.scrollTimeline.length) L.push(' 滚动驱动:' + c.scrollTimeline.slice(0, 4).map(a => `${a.selector} {${a.timeline}}`).join(' · '));
|
|
334
|
+
}
|
|
335
|
+
if (s) {
|
|
336
|
+
L.push(`滚动对照(真滚轮滚了 ${s.dispatchedPx}px,页面到 ${s.reachedY}/${Math.max(0, s.docHeight - s.viewportHeight)}):`
|
|
337
|
+
+ `入场 reveal ${s.revealsTotal} 个 · 随滚动位置走的(scrub/视差)${s.scrubsTotal} 个`
|
|
338
|
+
+ (s.hijackSuspected ? ' · ⚠ 疑似滚动劫持(派发的滚轮像素远多于页面真滚的)' : ''));
|
|
339
|
+
// 滚完被拆掉/display:none 的元素(典型:开场幕布 lift 后 remove)computed 全空 —— 说"没了",别打印空串
|
|
340
|
+
const st8 = (v) => (v.opacity === '' || v.opacity == null) && (!v.transform || v.transform === '') ? '(removed/hidden)' : `${v.opacity}/${v.transform}`;
|
|
341
|
+
if (s.reveals.length) L.push(' reveal 样本:' + s.reveals.slice(0, 5).map(x => `${x.target} ${st8(x.from)}→${st8(x.to)}${x.transition ? ` [${x.transition}]` : ''}${x.animation ? ` [anim ${x.animation}]` : ''}`).join(' · '));
|
|
342
|
+
if (s.scrubs.length) L.push(' scrub/视差样本:' + s.scrubs.slice(0, 5).map(x => `${x.target}(滚到一半时 ${x.midTransform})`).join(' · '));
|
|
343
|
+
}
|
|
344
|
+
if (r) {
|
|
345
|
+
L.push(`运行时(getAnimations,滚过一遍后):${r.count} 条活着的` + (r.items.length ? ':' + r.items.slice(0, 6).map(a => `${a.target} ${a.name || a.kind} ${a.duration}ms ${a.easing}${a.iterations === Infinity ? ' ∞' : ''}`).join(' · ') : ''));
|
|
346
|
+
}
|
|
347
|
+
if (inv.errors?.length) L.push(`(这几路没采到:${inv.errors.join(';')})`);
|
|
348
|
+
return L;
|
|
349
|
+
}
|