cozyclay 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +22 -56
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
- package/dist/assets/app-B2BpfCl2.js +5088 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-B887n77J.js +1 -0
- package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
- package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
- package/dist/assets/task-board-BKaM7d-9.js +14 -0
- package/dist/assets/workflow-B8Dhzexj.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/package.json +3 -3
- package/src/App.jsx +50 -281
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-task-scopes.js +69 -0
- package/src/bus-task-targets.js +48 -0
- package/src/bus-tasks.js +181 -0
- package/src/command-bus.js +15 -40
- package/src/command-history.js +36 -0
- package/src/commands/cast.js +10 -3
- package/src/commands/motion.js +2 -1
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +13 -7
- package/src/commands/project.js +1 -1
- package/src/commands/shared.js +9 -0
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/anchors.js +33 -5
- package/src/motion3/compiler/compile.js +27 -4
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/grip.js +9 -3
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/socket-grip.js +34 -10
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/fix/index.js +7 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +5 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +21 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +11 -10
- package/src/motion3/schema/types.js +5 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/basic.js +61 -13
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +13 -4
- package/src/motion3/skills/info.js +14 -1
- package/src/motion3/skills/reach-human.js +139 -0
- package/src/motion3/skills/stance.js +92 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/motion3/sockets.js +21 -4
- package/src/motion3/views.js +28 -5
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +4 -7
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +3 -7
- package/src/scenes.js +4 -5
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +1 -1
- package/src/studio-error-codes.js +1 -1
- package/src/studio-motion-visual.js +4 -4
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +75 -42
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +11 -25
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +7 -14
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-DbKG0NgD.js +0 -6
- package/dist/assets/app-34i-iUy1.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DYpa2nvO.js +0 -5
- package/dist/assets/first-shot-handoff-CrGZ3n_O.js +0 -1
- package/dist/assets/landing-mq5bnqFE.js +0 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-dUhurYUc.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Archive GitHub repository traffic into .metrics/.
|
|
3
|
-
//
|
|
4
|
-
// GitHub keeps traffic for 14 days. That is shorter than the time it takes to
|
|
5
|
-
// tell whether a launch worked, so this snapshots the numbers and merges them
|
|
6
|
-
// into a permanent time series.
|
|
7
|
-
//
|
|
8
|
-
// The traffic endpoints need repository admin rights, which the Actions
|
|
9
|
-
// GITHUB_TOKEN does not have. So this runs wherever a real credential lives:
|
|
10
|
-
// pass one in GITHUB_TOKEN / GH_TOKEN, or let it fall back to the token git
|
|
11
|
-
// already stores for github.com.
|
|
12
|
-
|
|
13
|
-
import { writeFile, readFile, mkdir } from "node:fs/promises";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
|
|
18
|
-
const REPO = process.env.TRAFFIC_REPO ?? "NomaDamas/CozyClay";
|
|
19
|
-
const OUT = path.resolve(process.env.TRAFFIC_OUT ?? ".metrics");
|
|
20
|
-
|
|
21
|
-
async function resolveToken() {
|
|
22
|
-
const fromEnv = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
23
|
-
if (fromEnv) return fromEnv;
|
|
24
|
-
|
|
25
|
-
// git credential fill speaks a key=value protocol on stdin/stdout, and it
|
|
26
|
-
// blocks until stdin is closed — so the request has to be written and the
|
|
27
|
-
// pipe ended explicitly.
|
|
28
|
-
const stdout = await new Promise((resolve) => {
|
|
29
|
-
const child = spawn("git", ["credential", "fill"], { stdio: ["pipe", "pipe", "ignore"] });
|
|
30
|
-
let out = "";
|
|
31
|
-
const done = (value) => resolve(value);
|
|
32
|
-
const timer = setTimeout(() => {
|
|
33
|
-
child.kill();
|
|
34
|
-
done("");
|
|
35
|
-
}, 10_000);
|
|
36
|
-
child.stdout.on("data", (chunk) => (out += chunk));
|
|
37
|
-
child.on("error", () => {
|
|
38
|
-
clearTimeout(timer);
|
|
39
|
-
done("");
|
|
40
|
-
});
|
|
41
|
-
child.on("close", () => {
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
done(out);
|
|
44
|
-
});
|
|
45
|
-
child.stdin.end(`protocol=https\nhost=github.com\npath=${REPO}.git\n\n`);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const line = stdout.split("\n").find((l) => l.startsWith("password="));
|
|
49
|
-
if (!line) throw new Error("no GitHub token: set GITHUB_TOKEN or sign in to git");
|
|
50
|
-
return line.slice("password=".length);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function api(token, endpoint) {
|
|
54
|
-
const res = await fetch(`https://api.github.com/repos/${REPO}/traffic/${endpoint}`, {
|
|
55
|
-
headers: {
|
|
56
|
-
authorization: `Bearer ${token}`,
|
|
57
|
-
accept: "application/vnd.github+json",
|
|
58
|
-
"x-github-api-version": "2022-11-28",
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
if (!res.ok) {
|
|
62
|
-
const body = await res.text();
|
|
63
|
-
throw new Error(`${endpoint} -> ${res.status}: ${body.slice(0, 200)}`);
|
|
64
|
-
}
|
|
65
|
-
return res.json();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function readJson(file, fallback) {
|
|
69
|
-
const p = path.join(OUT, file);
|
|
70
|
-
if (!existsSync(p)) return fallback;
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(await readFile(p, "utf8"));
|
|
73
|
-
} catch {
|
|
74
|
-
return fallback;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const sortKeys = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
79
|
-
|
|
80
|
-
async function writeJson(file, data) {
|
|
81
|
-
await writeFile(path.join(OUT, file), JSON.stringify(sortKeys(data), null, 2) + "\n");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const token = await resolveToken();
|
|
85
|
-
await mkdir(OUT, { recursive: true });
|
|
86
|
-
|
|
87
|
-
const [views, clones, referrers, paths] = await Promise.all([
|
|
88
|
-
api(token, "views"),
|
|
89
|
-
api(token, "clones"),
|
|
90
|
-
api(token, "popular/referrers"),
|
|
91
|
-
api(token, "popular/paths"),
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
|
-
// Daily counts are keyed by date, so re-running the same day corrects the row
|
|
95
|
-
// instead of appending a duplicate.
|
|
96
|
-
const daily = await readJson("daily.json", {});
|
|
97
|
-
for (const [kind, payload] of [
|
|
98
|
-
["views", views],
|
|
99
|
-
["clones", clones],
|
|
100
|
-
]) {
|
|
101
|
-
for (const row of payload[kind] ?? []) {
|
|
102
|
-
const day = row.timestamp.slice(0, 10);
|
|
103
|
-
daily[day] ??= {};
|
|
104
|
-
daily[day][kind] = row.count;
|
|
105
|
-
daily[day][`${kind}_uniques`] = row.uniques;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
await writeJson("daily.json", daily);
|
|
109
|
-
|
|
110
|
-
// Referrers and paths are 14-day rollups on GitHub's side, not per-day values,
|
|
111
|
-
// so they are kept as dated snapshots rather than merged.
|
|
112
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
113
|
-
for (const [file, payload] of [
|
|
114
|
-
["referrers.json", referrers],
|
|
115
|
-
["paths.json", paths],
|
|
116
|
-
]) {
|
|
117
|
-
const history = await readJson(file, {});
|
|
118
|
-
history[today] = payload;
|
|
119
|
-
await writeJson(file, history);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const rows = Object.entries(daily).sort(([a], [b]) => a.localeCompare(b));
|
|
123
|
-
await writeFile(
|
|
124
|
-
path.join(OUT, "daily.csv"),
|
|
125
|
-
["date,views,views_uniques,clones,clones_uniques"]
|
|
126
|
-
.concat(
|
|
127
|
-
rows.map(([day, v]) =>
|
|
128
|
-
[day, v.views ?? "", v.views_uniques ?? "", v.clones ?? "", v.clones_uniques ?? ""].join(","),
|
|
129
|
-
),
|
|
130
|
-
)
|
|
131
|
-
.join("\n") + "\n",
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const totalViews = rows.reduce((sum, [, v]) => sum + (v.views ?? 0), 0);
|
|
135
|
-
console.log(
|
|
136
|
-
`archived ${rows.length} days (${totalViews} views total) · top referrer: ` +
|
|
137
|
-
`${referrers[0]?.referrer ?? "none"} (${referrers[0]?.uniques ?? 0} uniques)`,
|
|
138
|
-
);
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Real-surface IK camera-drag frame-time probe. The browser is supplied by
|
|
4
|
-
* tools/qa-browser.mjs; this script only drives the page over CDP.
|
|
5
|
-
*/
|
|
6
|
-
import { writeFileSync } from "node:fs";
|
|
7
|
-
|
|
8
|
-
const port = Number(process.env.CDP_PORT || 9284);
|
|
9
|
-
const baseUrl = process.env.QA_URL || "http://127.0.0.1:5284/app/";
|
|
10
|
-
const perfDpr = Number(process.env.PERF_DPR || 1);
|
|
11
|
-
if (!(perfDpr > 0)) throw new Error(`PERF_DPR must be positive, got ${process.env.PERF_DPR}`);
|
|
12
|
-
const perfWidth = 1920;
|
|
13
|
-
const perfHeight = 1200;
|
|
14
|
-
const target = (await (await fetch(`http://127.0.0.1:${port}/json`)).json()).find((item) => item.type === "page" && item.webSocketDebuggerUrl);
|
|
15
|
-
if (!target) throw new Error(`no page target on CDP port ${port}`);
|
|
16
|
-
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
17
|
-
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
|
18
|
-
let nextId = 1;
|
|
19
|
-
const pending = new Map();
|
|
20
|
-
ws.onmessage = (event) => {
|
|
21
|
-
const message = JSON.parse(event.data);
|
|
22
|
-
if (!message.id || !pending.has(message.id)) return;
|
|
23
|
-
const request = pending.get(message.id);
|
|
24
|
-
pending.delete(message.id);
|
|
25
|
-
if (message.error) request.reject(new Error(JSON.stringify(message.error)));
|
|
26
|
-
else request.resolve(message.result);
|
|
27
|
-
};
|
|
28
|
-
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
29
|
-
const id = nextId++;
|
|
30
|
-
pending.set(id, { resolve, reject });
|
|
31
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
32
|
-
});
|
|
33
|
-
const evaluate = async (expression) => {
|
|
34
|
-
const result = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
35
|
-
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || "browser evaluation failed");
|
|
36
|
-
return result.result.value;
|
|
37
|
-
};
|
|
38
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
-
const waitFor = async (expression, timeoutMs = 15000) => {
|
|
40
|
-
const deadline = Date.now() + timeoutMs;
|
|
41
|
-
while (Date.now() < deadline) {
|
|
42
|
-
if (await evaluate(expression)) return;
|
|
43
|
-
await sleep(50);
|
|
44
|
-
}
|
|
45
|
-
throw new Error(`timed out waiting for ${expression}`);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await send("Runtime.enable");
|
|
49
|
-
await send("Page.enable");
|
|
50
|
-
// Match the reporter's high-resolution surface before the page creates its
|
|
51
|
-
// renderer; setting this after navigation leaves canvas allocation at DPR 1.
|
|
52
|
-
await send("Emulation.setDeviceMetricsOverride", {
|
|
53
|
-
width: perfWidth,
|
|
54
|
-
height: perfHeight,
|
|
55
|
-
deviceScaleFactor: perfDpr,
|
|
56
|
-
mobile: false,
|
|
57
|
-
});
|
|
58
|
-
await send("Profiler.enable");
|
|
59
|
-
await send("Page.navigate", { url: `${baseUrl}?motion=/demo/walk-then-stop.npz` });
|
|
60
|
-
await waitFor("!!window.__cozyclay?.rigA && !!window.__cozyclay?.motion");
|
|
61
|
-
await evaluate(`(() => { const button = [...document.querySelectorAll("button")].find((item) => item.textContent.trim() === "IK off"); if (!button) throw new Error("IK toggle missing"); button.click(); return true; })()`);
|
|
62
|
-
await waitFor("window.__cozyclay?.ikMode === true && !!window.__ikVisibilityPerformance");
|
|
63
|
-
await sleep(500);
|
|
64
|
-
const renderer = await evaluate(`(() => {
|
|
65
|
-
const canvas = document.querySelector("canvas");
|
|
66
|
-
const context = canvas?.getContext("webgl2") || canvas?.getContext("webgl");
|
|
67
|
-
const debug = context?.getExtension("WEBGL_debug_renderer_info");
|
|
68
|
-
return {
|
|
69
|
-
userAgent: navigator.userAgent,
|
|
70
|
-
requestedDevicePixelRatio: ${perfDpr},
|
|
71
|
-
effectiveDevicePixelRatio: devicePixelRatio,
|
|
72
|
-
viewportWidth: innerWidth,
|
|
73
|
-
viewportHeight: innerHeight,
|
|
74
|
-
canvasWidth: canvas?.width,
|
|
75
|
-
canvasHeight: canvas?.height,
|
|
76
|
-
webgl: context?.constructor?.name || null,
|
|
77
|
-
renderer: debug ? context.getParameter(debug.UNMASKED_RENDERER_WEBGL) : "unavailable",
|
|
78
|
-
vendor: debug ? context.getParameter(debug.UNMASKED_VENDOR_WEBGL) : "unavailable",
|
|
79
|
-
};
|
|
80
|
-
})()`);
|
|
81
|
-
const stage = await evaluate(`(() => { const rect = document.querySelector(".stage")?.getBoundingClientRect(); if (!rect) throw new Error("stage missing"); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height }; })()`);
|
|
82
|
-
const mouse = (type, x, y, button = "right", buttons = button === "right" ? 2 : 0) => send("Input.dispatchMouseEvent", {
|
|
83
|
-
type, x: Math.round(x), y: Math.round(y), button, buttons, clickCount: 1,
|
|
84
|
-
});
|
|
85
|
-
const profileStartedAt = new Date().toISOString();
|
|
86
|
-
await send("Profiler.start");
|
|
87
|
-
const sample = evaluate(`new Promise((resolve) => {
|
|
88
|
-
const frames = [];
|
|
89
|
-
let previous = performance.now();
|
|
90
|
-
const started = previous;
|
|
91
|
-
const tick = (now) => {
|
|
92
|
-
frames.push(now - previous);
|
|
93
|
-
previous = now;
|
|
94
|
-
if (now - started >= 2100) resolve(frames.slice(1));
|
|
95
|
-
else requestAnimationFrame(tick);
|
|
96
|
-
};
|
|
97
|
-
requestAnimationFrame(tick);
|
|
98
|
-
})`);
|
|
99
|
-
await mouse("mousePressed", stage.x, stage.y);
|
|
100
|
-
for (let index = 1; index <= 120; index += 1) {
|
|
101
|
-
const angle = (index / 120) * Math.PI * 2;
|
|
102
|
-
await mouse("mouseMoved", stage.x + Math.cos(angle) * stage.width * 0.12, stage.y + Math.sin(angle) * stage.height * 0.08);
|
|
103
|
-
await sleep(2000 / 120);
|
|
104
|
-
}
|
|
105
|
-
await mouse("mouseReleased", stage.x + stage.width * 0.12, stage.y, "right", 0);
|
|
106
|
-
const frameTimes = await sample;
|
|
107
|
-
const profileResult = await send("Profiler.stop");
|
|
108
|
-
const sorted = [...frameTimes].sort((a, b) => a - b);
|
|
109
|
-
const percentile = (values, p) => values[Math.min(values.length - 1, Math.floor(values.length * p))] ?? null;
|
|
110
|
-
const duration = frameTimes.reduce((sum, value) => sum + value, 0);
|
|
111
|
-
const samples = profileResult.profile?.samples ?? [];
|
|
112
|
-
const deltas = profileResult.profile?.timeDeltas ?? [];
|
|
113
|
-
const nodes = new Map((profileResult.profile?.nodes ?? []).map((node) => [node.id, node]));
|
|
114
|
-
const selfTime = new Map();
|
|
115
|
-
for (let index = 0; index < samples.length; index += 1) selfTime.set(samples[index], (selfTime.get(samples[index]) || 0) + (deltas[index] || 0));
|
|
116
|
-
const topSelfTime = [...selfTime.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([id, microseconds]) => {
|
|
117
|
-
const node = nodes.get(id);
|
|
118
|
-
return { function: node?.callFrame?.functionName || "(anonymous)", url: node?.callFrame?.url || "", microseconds };
|
|
119
|
-
});
|
|
120
|
-
const result = {
|
|
121
|
-
capturedAt: profileStartedAt,
|
|
122
|
-
baseUrl,
|
|
123
|
-
renderer,
|
|
124
|
-
ikVisibility: await evaluate("window.__ikVisibilityPerformance()"),
|
|
125
|
-
run: {
|
|
126
|
-
frameCount: frameTimes.length,
|
|
127
|
-
medianFrameMs: percentile(sorted, 0.5),
|
|
128
|
-
p95FrameMs: percentile(sorted, 0.95),
|
|
129
|
-
fps: duration > 0 ? frameTimes.length / (duration / 1000) : 0,
|
|
130
|
-
minFrameMs: sorted[0] ?? null,
|
|
131
|
-
maxFrameMs: sorted.at(-1) ?? null,
|
|
132
|
-
},
|
|
133
|
-
topSelfTime,
|
|
134
|
-
};
|
|
135
|
-
const outputPath = process.env.PERF_OUTPUT;
|
|
136
|
-
if (outputPath) writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
137
|
-
console.log(JSON.stringify(result, null, 2));
|
|
138
|
-
ws.close();
|