cozyclay 2.1.1 → 3.0.1

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.
Files changed (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,128 @@
1
+ // The arm turns a held object, not the wrist alone (docs/motion-api-v3.1.md C14, lane K8). When a
2
+ // hand must take a world rotation (the object leads, hold.js), most of a held object's angle comes
3
+ // from the elbow's swivel about the shoulder-wrist axis and from the forearm's turn about its own
4
+ // axis (pronation and supination, the twist); the wrist adds the rest (flex and deviation).
5
+ // swivel the elbow on the circle twoBone can put it on for this wrist: POLES poles are scored
6
+ // and their soft minimum (a Boltzmann-weighted mean pole) taken, so two near-equal
7
+ // poles blend instead of flipping; ../hold-plan.js smooths it over the frames
8
+ // score each turn's squared share of its range, plus a steep price past the limits, plus a
9
+ // light pull toward the pole the channels gave (a keyed elbow_<side> is a preference)
10
+ // and the same steep price past SWING degrees from it (the upper arm's own roll limit:
11
+ // a straight arm swung half a turn would twist the shoulder, not hold the object)
12
+ // twist the forearm bone takes FOREARM_SHARE of the turn about the forearm and the hand the
13
+ // rest, so the skin twists along the forearm instead of at the wrist alone
14
+ // Everything here is analytic on the pose (no bone moves): twoBone's result does not depend on the
15
+ // arm it starts from, so the forearm's frame for a pole is its rest frame aligned onto it.
16
+ import { add, sub, scale, norm, dot, cross, len, mul, T, apply, align, clamp, quatFromMat, matFromQuat } from "./math.js";
17
+ import { qmul, qconj, qlog, qexp } from "./quat.js";
18
+ import { palmAxes, foldPoint, backOf } from "./limbs.js";
19
+
20
+ /** Wrist limits in degrees, relative to the hand at rest on the forearm (twist: the forearm's turn). */
21
+ export const WRIST_LIMITS = { twist: [-85, 85], flex: [-70, 70], dev: [-35, 20] };
22
+ /**
23
+ * The comfortable part of each range, what observations.wrist counts frames past (qa/wrist-obs.js):
24
+ * daily tasks keep the wrist within about 40 deg of flex and extension and 25 ulnar / 15 radial
25
+ * deviation (Ryu et al.; Gates et al. 2016) and the forearm within 50 deg of pronation and supination
26
+ * (Morrey 1981).
27
+ */
28
+ export const WRIST_COMFORT = { twist: [-50, 50], flex: [-40, 40], dev: [-25, 15] };
29
+ /** The share of the turn about the forearm the forearm bone takes (the hand takes the rest). */
30
+ export const FOREARM_SHARE = 0.5;
31
+ /** The direction an arm's elbow points at rest, carried by the bones (pose.js, twoBone). */
32
+ export const ELBOW_REST = [0, 0, -1];
33
+ const POLES = 72, TEMP = 0.02, OVER = 400, PULL = 0.5, SWING = 100 / (180 / Math.PI), DEG = 180 / Math.PI;
34
+ /**
35
+ * The shoulder's comfort (lane elbow): upper-arm elevation, degrees from hanging straight down (90 level, more
36
+ * the elbow over the shoulder). Daily handling (drinking, eating, a cup or a can at the mouth) keeps the upper
37
+ * arm low and forward, well below the level (Magermans et al. 2005); only overhead work lifts it. A pole pays
38
+ * SHOULDER_PRICE times the squared share of SHOULDER_SPAN it lifts the elbow past SHOULDER_COMFORT on the comfort
39
+ * landscape only (a carry whose aim keys may lean, key-aim.js): priced on every hold, it moved the bench's recompiled
40
+ * cup and can takes into wrist limits and QA (their keys were written for the elbow-up hold).
41
+ */
42
+ const SHOULDER_COMFORT = 70, SHOULDER_SPAN = 30, SHOULDER_PRICE = 2;
43
+ /** A wrist need's price against the comfortable range first and the limits second (the shape grip-choice.js prices a grip with): 1 at the comfortable edge, steep past it. */
44
+ export function comfortPrice(need, steep = 8, over = 400) {
45
+ let cost = 0;
46
+ for (const k of ["twist", "flex", "dev"]) {
47
+ const v = need[k], c = v >= 0 ? WRIST_COMFORT[k][1] : -WRIST_COMFORT[k][0], L = v >= 0 ? WRIST_LIMITS[k][1] : -WRIST_LIMITS[k][0], a = Math.abs(v);
48
+ cost += a <= c ? (a / c) ** 2 : 1 + steep * ((Math.min(a, L) - c) / Math.max(1e-6, L - c)) ** 2 + (a > L ? over * ((a - L) / L) ** 2 : 0);
49
+ }
50
+ return cost;
51
+ }
52
+
53
+ /** The shoulder's price for an elbow at E with the shoulder at S (SHOULDER_COMFORT above). */
54
+ export function shoulderPrice(S, E) {
55
+ const u = norm(sub(E, S)), el = Math.acos(clamp(-u[1], -1, 1)) * DEG;
56
+ return SHOULDER_PRICE * (Math.max(0, el - SHOULDER_COMFORT) / SHOULDER_SPAN) ** 2;
57
+ }
58
+
59
+ /** What the hand needs from neutral (its rest on the forearm) to goal, degrees { twist, flex, dev }; axT the forearm axis, l the palm's l axis at neutral. */
60
+ export function wristNeed(neutral, goal, axT, l) {
61
+ let q = quatFromMat(mul(goal, T(neutral)));
62
+ if (q[3] < 0) q = q.map(v => -v);
63
+ const twist = 2 * Math.atan2(dot([q[0], q[1], q[2]], axT), q[3]), spin = qexp(scale(axT, twist));
64
+ const swing = qlog(qmul(q, qconj(spin))), lTwisted = apply(matFromQuat(spin), l);
65
+ const axF = norm(sub(lTwisted, scale(axT, dot(lTwisted, axT)))), axD = cross(axT, axF);
66
+ return { twist: twist * DEG, flex: dot(swing, axF) * DEG, dev: dot(swing, axD) * DEG, axF, axD };
67
+ }
68
+
69
+ /**
70
+ * The strain of a wrist need: each turn's squared share of its half range, plus OVER times the
71
+ * squared share past a limit; `margin` (a share of the half range) moves the limits that far in.
72
+ */
73
+ function strainOf(need, margin = 0) {
74
+ let cost = 0;
75
+ for (const [k, [lo, hi]] of Object.entries(WRIST_LIMITS)) {
76
+ const z = (need[k] - (lo + hi) / 2) / ((hi - lo) / 2), over = Math.max(0, Math.abs(z) - 1 + margin);
77
+ cost += z * z + OVER * over * over;
78
+ }
79
+ return cost;
80
+ }
81
+
82
+ /**
83
+ * The arm's swivel landscape for hand rotation `goal` with the wrist at W, on pose s (the plain
84
+ * arm of `side`): at(th) -> { pv, need, elbow, cost } for the elbow th radians about the shoulder-wrist
85
+ * axis from `ref` (the bend direction the channels gave), angleOf(world direction) -> th,
86
+ * softmin() -> the th of the Boltzmann-weighted mean of POLES poles, miss the metres the arm is
87
+ * short of W (a wrist nearer the shoulder than the elbow folds is no miss here; hold.js holdCost
88
+ * prices it). `margin` tightens the wrist limits the strain prices (strainOf).
89
+ */
90
+ export function landscape(sk, s, side, goal, W, ref, restBend, margin = 0, comfort = false) {
91
+ const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`], S = s.P[a];
92
+ const A = len(sub(s.P[fa], S)), B = len(sub(s.P[h], s.P[fa]));
93
+ // the elbow where twoBone puts it: the wrist no further folded than limbs.js foldPoint lets it
94
+ const v = sub(foldPoint(S, W, A, B, backOf(sk, s)), S), d = len(v), u = scale(v, 1 / Math.max(d, 1e-9)), dc = clamp(d, Math.abs(A - B) + 1e-4, (A + B) * 0.9995);
95
+ const along = (A * A - B * B + dc * dc) / (2 * dc), rad = Math.sqrt(Math.max(A * A - along * along, 0));
96
+ const off = x => sub(x, scale(u, dot(x, u))), e1 = norm(len(off(ref)) > 1e-6 ? off(ref) : cross(u, Math.abs(u[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]));
97
+ const e2 = cross(u, e1), Wn = add(S, scale(u, dc));
98
+ const rw = sk.restWorldRot, faDir = apply(T(rw[fa]), sub(sk.restWorldPos[h], sk.restWorldPos[fa])), faBend = apply(T(rw[fa]), restBend);
99
+ const neutralRel = mul(T(rw[fa]), rw[h]), lLocal = apply(T(s.R[h]), palmAxes(sk, s, side).l);
100
+ const at = th => {
101
+ const pv = add(scale(e1, Math.cos(th)), scale(e2, Math.sin(th))), fdir = sub(Wn, add(S, add(scale(u, along), scale(pv, rad))));
102
+ const neutral = mul(align(faDir, faBend, fdir, pv), neutralRel), need = wristNeed(neutral, goal, norm(fdir), apply(neutral, lLocal));
103
+ const z = Math.abs(Math.atan2(Math.sin(th), Math.cos(th))) / SWING;
104
+ const elbow = add(S, add(scale(u, along), scale(pv, rad)));
105
+ // comfort (a carry that lets its aim lean, key-aim.js): the wrist priced against its comfortable range and the shoulder's price
106
+ return { pv, need, elbow, cost: (comfort ? comfortPrice(need) + shoulderPrice(S, elbow) : strainOf(need, margin)) + PULL * (1 - Math.cos(th)) + OVER * Math.max(0, z - 1) ** 2 };
107
+ };
108
+ const angleOf = x => (len(off(x)) > 1e-6 ? Math.atan2(dot(x, e2), dot(x, e1)) : 0);
109
+ const softmin = () => {
110
+ const all = Array.from({ length: POLES }, (_, i) => at((2 * Math.PI * i) / POLES)), low = Math.min(...all.map(p => p.cost));
111
+ const mean = all.reduce((m, p) => add(m, scale(p.pv, Math.exp(-(p.cost - low) / TEMP))), [0, 0, 0]);
112
+ return len(mean) > 1e-3 ? angleOf(mean) : (2 * Math.PI * all.findIndex(p => p.cost === low)) / POLES;
113
+ };
114
+ return { at, angleOf, softmin, miss: Math.max(0, d - dc) };
115
+ }
116
+
117
+ /**
118
+ * The elbow pole for hand rotation `goal` with the wrist at W (landscape above). opts.toward, a
119
+ * world direction, sets the pole instead of the soft minimum (a planned pole, ../hold-plan.js);
120
+ * opts.w (0..1, the keys' blend weight) scales the swing from ref, so the elbow eases out of and
121
+ * back into the plain arm; opts.margin as for landscape. Returns { pole, need, cost, miss }: pole
122
+ * a world direction for twoBone.
123
+ */
124
+ export function swivel(sk, s, side, goal, W, ref, restBend, { toward, w = 1, margin = 0, comfort = false } = {}) {
125
+ const land = landscape(sk, s, side, goal, W, ref, restBend, margin, comfort), best = toward ? land.angleOf(toward) : land.softmin();
126
+ const pick = land.at(w * Math.atan2(Math.sin(best), Math.cos(best)));
127
+ return { pole: pick.pv, need: pick.need, cost: pick.cost, miss: land.miss, elbow: pick.elbow };
128
+ }
@@ -0,0 +1,125 @@
1
+ // Keyed channels -> per-frame values. A channel is interpolated only through the keys that
2
+ // set it; before its first key and after its last it holds. Numbers, number arrays and
3
+ // {preset: weight} objects are supported. `step` keys jump at that frame (visibility switches
4
+ // and the like).
5
+ // Flow (spec.flow): the channels that place the body against the world or a target (feet,
6
+ // root, yaw, hand targets and weights, poles, fingers, objects) are monotone cubic / PCHIP and on
7
+ // time, so a held value never overshoots and a contact lands on its key. The body's own swing
8
+ // (FLOW_SMOOTH: spine, head, arm swing and bend, wrist) carries its speed through a key instead of
9
+ // stopping on it (Catmull-Rom, the overshoot past a key bounded), and trails the body
10
+ // (FLOW_LAG frames: the head behind the body, the arms behind both), the trail easing out over the
11
+ // take's last frames so every value is reached. An arm on its IK (armw) reaches for a target, so
12
+ // flow is gone by the time the arm is a quarter of the way onto it: an arm's swing and bend by its
13
+ // own armw, the spine (which carries the shoulders) and the head (the hands work near it) by the
14
+ // larger of the two; a reach or a held prop lands as keyed. A hold (two equal keys in a row) stays
15
+ // exactly flat either way.
16
+
17
+ function pchipSlopes(x, y) {
18
+ const n = y.length, m = new Array(n).fill(0);
19
+ for (let i = 1; i < n - 1; i++) {
20
+ const h0 = x[i] - x[i - 1], h1 = x[i + 1] - x[i];
21
+ const d0 = (y[i] - y[i - 1]) / h0, d1 = (y[i + 1] - y[i]) / h1;
22
+ if (d0 * d1 > 0) {
23
+ const w1 = 2 * h1 + h0, w2 = h1 + 2 * h0;
24
+ m[i] = (w1 + w2) / (w1 / d0 + w2 / d1);
25
+ }
26
+ }
27
+ return m;
28
+ }
29
+
30
+ function pchipEval(x, y, m, q) {
31
+ if (x.length === 1 || q <= x[0]) return y[0];
32
+ if (q >= x[x.length - 1]) return y[y.length - 1];
33
+ let i = 0;
34
+ while (q > x[i + 1]) i++;
35
+ const h = x[i + 1] - x[i], t = (q - x[i]) / h;
36
+ const t2 = t * t, t3 = t2 * t;
37
+ return (2 * t3 - 3 * t2 + 1) * y[i] + (t3 - 2 * t2 + t) * h * m[i] + (-2 * t3 + 3 * t2) * y[i + 1] + (t3 - t2) * h * m[i + 1];
38
+ }
39
+
40
+ const FLOW_SMOOTH = /^(spine|head)_(yaw|pitch|roll)$|^(aswing|abend)_[lr]$/;
41
+ const FLOW_LAG = { spine: 0, head: 2, arms: 3 };
42
+ const OVER_FRAC = 0.15, OVER_MAX = 6; // a smooth segment's overshoot: at most this share of its change, and degrees
43
+ const ikSides = ch => (/^(spine|head)_/.test(ch) ? ["l", "r"] : /^(aswing|abend)_([lr])$/.test(ch) ? [ch.slice(-1)] : []);
44
+ const lagFamily = ch => (/^spine_/.test(ch) ? "spine" : /^head_/.test(ch) ? "head" : /^(aswing|abend)_[lr]$/.test(ch) ? "arms" : null);
45
+
46
+ /** spec.flow -> { smooth, lag: { spine, head, arms } }: undefined is the default, false turns both off. */
47
+ export function flowOptions(flow) {
48
+ if (flow === false) return { smooth: false, lag: { spine: 0, head: 0, arms: 0 } };
49
+ const f = flow ?? {}, lag = f.lag === false ? 0 : f.lag;
50
+ const each = typeof lag === "number" ? { spine: lag, head: lag, arms: lag } : { ...FLOW_LAG, ...(lag ?? {}) };
51
+ return { smooth: f.smooth !== false, lag: each };
52
+ }
53
+
54
+ /** Catmull-Rom slopes, flat at a hold and at the ends, each segment's overshoot bounded. */
55
+ function smoothSlopes(x, y) {
56
+ const n = y.length, m = new Array(n).fill(0);
57
+ for (let i = 1; i < n - 1; i++) if (y[i] !== y[i - 1] && y[i] !== y[i + 1]) m[i] = (y[i + 1] - y[i - 1]) / (x[i + 1] - x[i - 1]);
58
+ for (let i = 0; i < n - 1; i++) {
59
+ const h = x[i + 1] - x[i], lo = Math.min(y[i], y[i + 1]), hi = Math.max(y[i], y[i + 1]), limit = Math.min(OVER_MAX, OVER_FRAC * (hi - lo));
60
+ for (let tries = 0; tries < 12; tries++) {
61
+ let over = 0;
62
+ for (let k = 1; k < 16; k++) {
63
+ const v = pchipEval([x[i], x[i + 1]], [y[i], y[i + 1]], [m[i], m[i + 1]], x[i] + (h * k) / 16);
64
+ over = Math.max(over, v - hi, lo - v);
65
+ }
66
+ if (over <= limit + 1e-9) break;
67
+ m[i] *= 0.7; m[i + 1] *= 0.7;
68
+ }
69
+ }
70
+ return m;
71
+ }
72
+
73
+ const shapeOf = v => (Array.isArray(v) ? "vec" : v && typeof v === "object" ? "map" : "num");
74
+
75
+ /** keys: [{ f, set: { channel: value }, step?: [channel...] }] -> { channel: Array(frameCount) }; flow: flowOptions(spec.flow). */
76
+ export function bakeChannels(keys, frameCount, flow = flowOptions()) {
77
+ const byChannel = new Map();
78
+ for (const key of [...keys].sort((a, b) => a.f - b.f)) {
79
+ for (const [ch, value] of Object.entries(key.set ?? {})) {
80
+ if (!byChannel.has(ch)) byChannel.set(ch, []);
81
+ byChannel.get(ch).push({ f: key.f, value, step: key.step?.includes(ch) });
82
+ }
83
+ }
84
+ const out = {}, plain = {};
85
+ for (const [ch, list] of byChannel) {
86
+ // later keys at the same frame win
87
+ const pts = [...new Map(list.map(k => [k.f, k])).values()];
88
+ const shape = shapeOf(pts[0].value);
89
+ const comps = shape === "vec" ? pts[0].value.length : 0;
90
+ const series = shape === "num" ? [pts.map(p => p.value)]
91
+ : shape === "vec" ? Array.from({ length: comps }, (_, i) => pts.map(p => p.value[i]))
92
+ : [...new Set(pts.flatMap(p => Object.keys(p.value)))].map(name => ({ name, y: pts.map(p => p.value[name] ?? 0) }));
93
+ const x = pts.map(p => p.f), smooth = flow.smooth && FLOW_SMOOTH.test(ch), lag = flow.lag[lagFamily(ch)] ?? 0;
94
+ // the trail eases out over the last 2 * lag frames, so the take ends on the last value
95
+ const at = f => (lag > 0 ? f - lag * Math.min(1, (frameCount - 1 - f) / (2 * lag)) : f);
96
+ const evalSeries = (y, flowing = true) => {
97
+ const m = smooth && flowing ? smoothSlopes(x, y) : pchipSlopes(x, y);
98
+ return q => {
99
+ const f = flowing ? at(q) : q;
100
+ // a step key holds the previous value right up to its frame
101
+ for (let i = 1; i < pts.length; i++) if (pts[i].step && f >= x[i - 1] && f < x[i]) return y[i - 1];
102
+ return pchipEval(x, y, m, f);
103
+ };
104
+ };
105
+ const frames = new Array(frameCount);
106
+ if (shape === "num") { const e = evalSeries(series[0]); for (let f = 0; f < frameCount; f++) frames[f] = e(f); }
107
+ else if (shape === "vec") { const es = series.map(evalSeries); for (let f = 0; f < frameCount; f++) frames[f] = es.map(e => e(f)); }
108
+ else {
109
+ const es = series.map(({ name, y }) => [name, evalSeries(y)]);
110
+ for (let f = 0; f < frameCount; f++) frames[f] = Object.fromEntries(es.map(([n, e]) => [n, e(f)]));
111
+ }
112
+ out[ch] = frames;
113
+ if (shape === "num" && (smooth || lag > 0) && ikSides(ch).length) { const e = evalSeries(series[0], false); plain[ch] = Array.from({ length: frameCount }, (_, f) => e(f)); }
114
+ }
115
+ // flow fades out as the arm goes onto its IK (the spine with either arm)
116
+ for (const [ch, keyed] of Object.entries(plain)) {
117
+ const ws = ikSides(ch).map(k => out[`armw_${k}`]).filter(Boolean);
118
+ if (!ws.length) continue;
119
+ out[ch] = out[ch].map((v, f) => { const w = Math.min(1, 4 * Math.max(0, ...ws.map(a => a[f]))); return v + (keyed[f] - v) * w * (2 - w); });
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /** Frame f's channel dict. */
125
+ export const channelsAt = (baked, f) => Object.fromEntries(Object.entries(baked).map(([k, v]) => [k, v[f]]));
@@ -0,0 +1,205 @@
1
+ // Motion spec -> baked frames. Deterministic and pure: the same spec and scene give the same
2
+ // frames, so the agent can edit keys and recompile in a loop.
3
+ //
4
+ // spec = { schema: "cozyclay.motion-spec.v1", fps, frames, keys, anchors, props, fits, objects, generators, checks }
5
+ // keys: [{ f, set: { channel: value }, step?: [channel] }] (see pose.js for channel names)
6
+ // hand mix: hand_<l|r> (world point) blended with w_<anchor>_<l|r> weights (anchors.js)
7
+ // fits: [{ side, anchor, effector: { prop, point } | { joints: [...] }, offset? }]
8
+ // moves the wrist so the effector lands on the anchor whenever that weight is on
9
+ // objects: { id: { pivot: [x,y,z], axis: [x,y,z], channel: "door" } } hinge angle in degrees
10
+ // { id: { prop, grab?, release?, at?, along?, visible?, keys? } } carried (../object-pose.js);
11
+ // keys lead the holding hand (../object-keys.js, hold.js)
12
+ // { id: { visible?, level? } } a switch: shown/hidden and dimmed, never moved (C15)
13
+ // { id: { follow, offset?, aim?, visible?, level? } } rides a point of the body (C16, ../follow.js)
14
+ // { id: { keys?, fall?, visible?, level? } } moves on its own: keys and gravity falls (C17, ../free-motion.js)
15
+ // id is a body or a group (Empty); a group moves every body under it
16
+ // generators: [{ type: "walk", f0, ... }] (walk.js)
17
+ // fix: { reach, slide, penetration, retime } deterministic auto-fixes (../fix/)
18
+ // flow: { smooth, lag } | false the body's swing carries through keys and trails (channels.js)
19
+ // idle: false turns off the idle layer (idle.js: a held pose breathes, drifts its head, shifts its weight)
20
+ // ctx = { sk, scene: { objects: { id: { pos, R, half, ancestors } } } }; out.ctx.scene keeps the scene (object-pose.js)
21
+ import { add, sub, scale, norm, len, dot, apply, axisRot } from "./math.js";
22
+ import { isCarried, isFollow, hingePose, movesRecord, compose, relative } from "../carried.js";
23
+ import { isFree, freePoseAt } from "../free-motion.js";
24
+ import { groupsOf } from "../scene.js";
25
+ import { pose, SIDES } from "./pose.js";
26
+ import { gripHand } from "./grip.js";
27
+ import { bakeChannels, channelsAt, flowOptions } from "./channels.js";
28
+ import { idleLayer, withIdle, busyHands } from "./idle.js";
29
+ import { resolveAnchor, measureProps } from "./anchors.js";
30
+ import { expandGenerators } from "./walk.js";
31
+ import { steadyHead } from "./head-steady.js";
32
+ import { fixOptions, applyFixes } from "../fix/index.js";
33
+ import { fitWeight } from "../fix/self.js";
34
+ import { holdHook } from "../object-keys.js";
35
+ import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
36
+ import { objectPoseAt, handOf, restingAt } from "../object-pose.js";
37
+
38
+ const smooth = x => x * x * (3 - 2 * x);
39
+
40
+ /**
41
+ * Scene boxes by frame. A body moves with its own spec.objects entry or its nearest group's (a
42
+ * switch entry moves nothing; hinge: turned; free (C17): where its keys and falls put it, under the time map `mapOf()`;
43
+ * carry and follow: left at its scene pose and marked `carried` with the moving id). `obstacles` drops the carried
44
+ * ones: QA, refs and object anchors read a carry where it rides (object-pose.js objectPoseAt, qa/object-checks.js);
45
+ * a follow entry rides the body and is no obstacle.
46
+ */
47
+ function objectTracks(spec, scene, baked, mapOf = () => undefined) {
48
+ const cache = new Map(), kept = new Map();
49
+ let map = mapOf();
50
+ const all = f => {
51
+ // a retime maps free keys and falls too: frames read before it are read again
52
+ if (mapOf() !== map) { map = mapOf(); cache.clear(); kept.clear(); }
53
+ if (cache.has(f)) return cache.get(f);
54
+ const out = {};
55
+ for (const [id, o] of Object.entries(scene?.objects ?? {})) {
56
+ const mover = [id, ...(o.ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])), entry = spec.objects?.[mover];
57
+ if (!entry) out[id] = o;
58
+ else if (isCarried(entry) || isFollow(entry)) out[id] = { ...o, carried: mover };
59
+ else if (isFree(entry)) out[id] = { ...o, ...compose(freePoseAt({ spec, scene, timeMap: map }, mover, f), relative(scene.objects[mover] ?? groupsOf(scene)[mover], o)) };
60
+ else out[id] = { ...o, ...hingePose(entry, o, baked, f) };
61
+ }
62
+ cache.set(f, out);
63
+ return out;
64
+ };
65
+ const obstacles = f => {
66
+ if (mapOf() !== map) all(f);
67
+ if (!kept.has(f)) kept.set(f, Object.fromEntries(Object.entries(all(f)).filter(([, box]) => !box.carried)));
68
+ return kept.get(f);
69
+ };
70
+ return { all, obstacles };
71
+ }
72
+
73
+ function effectorPoint(sk, s, diag, eff) {
74
+ if (eff.prop) return diag.props?.[eff.prop]?.[eff.point];
75
+ const js = eff.joints ?? ["RightHandIndex2", "RightHandMiddle2"];
76
+ return scale(js.map(j => s.P[sk.index[j]]).reduce((a, p) => add(a, p), [0, 0, 0]), 1 / js.length);
77
+ }
78
+
79
+ /** `lead` false solves without the object keys; a fit's own hand is solved without them (the keys override it). */
80
+ function hooksFor(ctx, c, f, inFit = false, lead = true, fitSide = null) {
81
+ const { sk } = ctx;
82
+ return {
83
+ softReach: ctx.softReach,
84
+ // a fit's passes find where the wrist must go with the elbow unstopped; the final pose stops its fold (limbs.js) and leaves the residual
85
+ foldFree: inFit,
86
+ hold: lead && ctx.hold ? (k, s) => (k === fitSide ? null : ctx.hold.lead(k, f, s, c)) : undefined,
87
+ // auto-fix offsets ride on the final pose only, so a fit never cancels them
88
+ push: inFit ? undefined : k => ctx.push?.[k]?.get(f),
89
+ swivel: k => ctx.swivel?.[k]?.get(f), // in a fit's passes too: a turn that stops at the wrist's limit moves the fit's point (wrist-clamp.js)
90
+ skinPush: inFit ? undefined : k => ctx.skinPush?.[k]?.get(f),
91
+ handTarget(k, s, diag) {
92
+ diag.frame = f;
93
+ const fkHand = s.P[sk.index[k === "l" ? "LeftHand" : "RightHand"]];
94
+ const terms = [];
95
+ for (const [ch, w] of Object.entries(c)) {
96
+ const m = /^w_(.+)_([lr])$/.exec(ch);
97
+ if (!m || m[2] !== k || !(w > 1e-6)) continue;
98
+ terms.push([w, add(resolveAnchor(ctx, m[1], s, diag, c, k), c[`fit_${m[1]}_${k}`] ?? [0, 0, 0])]);
99
+ }
100
+ let tot = terms.reduce((a, [w]) => a + w, 0);
101
+ if (tot > 1) { for (const t of terms) t[0] /= tot; tot = 1; }
102
+ let target = scale(c[`hand_${k}`] ?? fkHand, 1 - tot);
103
+ for (const [w, p] of terms) target = add(target, scale(p, w));
104
+ // Between targets, arc around the torso instead of cutting through it.
105
+ const trans = Math.max(0, ...terms.map(([w]) => smooth(Math.min(1, Math.min(w, 1 - w) * 3))));
106
+ if (trans > 0) {
107
+ const a = s.P[sk.index.Spine], b = s.P[sk.index.Neck], u = sub(b, a);
108
+ const t = Math.min(1, Math.max(0, dot(sub(target, a), u) / dot(u, u))), q = add(a, scale(u, t));
109
+ const r = ctx.spec.detourRadius ?? 0.25, dv = sub(target, q), d = len(dv);
110
+ if (d < r) {
111
+ const fwd = apply(axisRot([0, 1, 0], c.yaw ?? 0), [0, 0, 1]);
112
+ const dir = norm(add(d > 1e-6 ? scale(dv, 1 / d) : fwd, scale(fwd, 0.6)));
113
+ target = add(target, scale(sub(add(q, scale(dir, r)), target), trans));
114
+ }
115
+ }
116
+ // a wrist target inside the body (a pocket on a squatting thigh, a path through the chest) goes out onto its skin
117
+ return target;
118
+ },
119
+ // the wrist target, blended with the FK hand, inside the body goes out onto its skin, twice at most (out of
120
+ // one bone, then the next): a pocket the squat moved into the thigh, a path through the chest, a hand leaving
121
+ // a thigh for its FK rest. A measured target (a fit's anchor: the mouth; or a prop another hand's fit is on:
122
+ // the cigarette under the flame) stays where its goal needs it, and the move fades in and out with its weight;
123
+ // an arm coming in from FK moves it no more than its IK weight.
124
+ keepOut: ctx.keepOut && !inFit ? (k, s, target) => {
125
+ const free = (1 - fitWeight(ctx.spec, c, k)) * Math.min(1, Math.max(0, c[`armw_${k}`] ?? 0));
126
+ for (let i = 0; i < 2 && free > 1e-3; i++) {
127
+ const moved = keepOut(ctx.keepOut.body, sk, s, target, HAND_CLEAR, ctx.keepOut.skip);
128
+ if (moved === target) break;
129
+ target = add(target, scale(sub(moved, target), free));
130
+ }
131
+ return target;
132
+ } : undefined,
133
+ afterArm: (k, s, diag) => measureProps(ctx, k, s, diag),
134
+ // a fit's passes only place the wrist; the final pose (and a plain one) turns the hand onto the body
135
+ grip: inFit ? undefined : (k, s, diag) => gripHand(ctx, sk, s, SIDES[k], k, c, f, diag),
136
+ };
137
+ }
138
+
139
+ /** One frame with every active fit solved (fits run in spec order); lead false leaves the object keys out. */
140
+ export function solveFrame(ctx, c0, f, lead = true) {
141
+ const c = { ...withIdle(c0, ctx.idle, f) };
142
+ // a working hand keeps its forearm's slant (pose.js arm_hang)
143
+ for (const k of ["l", "r"]) if (ctx.busy?.[k]) c[`hangoff_${k}`] = ctx.busy[k][f];
144
+ for (const fit of ctx.spec.fits ?? []) {
145
+ const wch = `w_${fit.anchor}_${fit.side}`;
146
+ if (!(c[wch] > 1e-4)) continue;
147
+ const full = { ...c, [wch]: 1, [`armw_${fit.side}`]: 1 };
148
+ for (const ch of Object.keys(full)) if (ch !== wch && new RegExp(`^w_.+_${fit.side}$`).test(ch)) full[ch] = 0;
149
+ let corr = [0, 0, 0];
150
+ for (let i = 0; i < 6; i++) {
151
+ full[`fit_${fit.anchor}_${fit.side}`] = corr;
152
+ const { s, diag } = pose(ctx.sk, full, hooksFor(ctx, full, f, true, lead, fit.side));
153
+ const eff = effectorPoint(ctx.sk, s, diag, fit.effector);
154
+ const goal = add(resolveAnchor(ctx, fit.anchor, s, diag, full, fit.side), fit.offset ?? [0, 0, 0]);
155
+ if (!eff) break;
156
+ corr = add(corr, sub(goal, eff));
157
+ }
158
+ c[`fit_${fit.anchor}_${fit.side}`] = corr;
159
+ }
160
+ const { s, diag } = pose(ctx.sk, c, hooksFor(ctx, c, f, false, lead));
161
+ // readers of the solved frame (refs, checks, hints) resolve its anchors at f, IK arms or not
162
+ diag.frame = f;
163
+ return { s, diag, c };
164
+ }
165
+
166
+ /**
167
+ * opts.window = [a, b] solves only frames a..b (out.f0 = a, out.frames[i] is frame a + i);
168
+ * opts.keys replaces the generator expansion; opts.fixes = false skips the auto-fix pass.
169
+ */
170
+ export function compileMotion(spec, { sk, scene }, opts = {}) {
171
+ if (spec?.schema !== "cozyclay.motion-spec.v1") throw new Error("MOTION_SPEC_INVALID: schema must be cozyclay.motion-spec.v1");
172
+ const frameCount = Math.round(spec.frames);
173
+ if (!(frameCount >= 1 && frameCount <= 20000)) throw new Error("MOTION_SPEC_INVALID: frames must be 1..20000");
174
+ const fix = fixOptions(spec.fix);
175
+ const [a, b] = opts.window ? [Math.max(0, opts.window[0]), Math.min(frameCount - 1, opts.window[1])] : [0, frameCount - 1];
176
+ const ctx = { sk, spec, scene, softReach: fix.reach, push: { l: new Map(), r: new Map() }, swivel: { l: new Map(), r: new Map() }, skinPush: { l: new Map(), r: new Map() }, keepOut: fix.self ? wristBody(sk) : null };
177
+ const out = { ctx, fps: spec.fps ?? 30, frameCount, f0: a, frames: [] };
178
+ // object anchors read the pose refs read (object-pose.js), on the frame being solved
179
+ ctx.objectPose = (id, f, now) => objectPoseAt(out, id, f, now);
180
+ ctx.objectHand = (id, f) => handOf(out, id, f);
181
+ // a keyed carry's grab offset is taken where it rests at grab (object-keys.js; a fall before grab: where it landed)
182
+ ctx.restingAt = (id, f) => restingAt(out, id, f);
183
+ const engine = {
184
+ rebake(keys) {
185
+ out.keys = keys;
186
+ out.baked = bakeChannels(keys, frameCount, flowOptions(spec.flow));
187
+ steadyHead(out.baked, keys, frameCount);
188
+ // a held pose breathes and shifts its weight (idle.js): offsets solveFrame adds, contacts and out.baked stay as keyed
189
+ ctx.idle = idleLayer(out.baked, { spec, sk, frameCount });
190
+ ctx.busy = busyHands(out.baked, spec, frameCount, out.fps);
191
+ const tracks = objectTracks(spec, scene, out.baked, () => out.timeMap);
192
+ ctx.objectsAt = tracks.all;
193
+ out.objectsAt = a ? f => tracks.obstacles(f + a) : tracks.obstacles;
194
+ },
195
+ solve(frames) { for (const f of frames) if (f >= a && f <= b) out.frames[f - a] = solveFrame(ctx, channelsAt(out.baked, f), f); },
196
+ all: () => Array.from({ length: b - a + 1 }, (_, i) => a + i),
197
+ };
198
+ ctx.hold = holdHook(ctx, out, f => solveFrame(ctx, channelsAt(out.baked, f), f, false), f => solveFrame(ctx, channelsAt(out.baked, f), f));
199
+ engine.rebake(opts.keys ?? expandGenerators(spec, sk));
200
+ engine.solve(engine.all());
201
+ out.fixed = opts.fixes === false ? {} : applyFixes(out, fix, engine, { whole: !opts.window });
202
+ // a retime maps the key frames only once it is done: solve the keyed frames again on the final map
203
+ if (ctx.hold && out.timeMap) engine.solve(engine.all());
204
+ return out;
205
+ }
@@ -0,0 +1,97 @@
1
+ // The walk's rhythm (walk.js): when each step lifts off, how long the foot is in the air, how far
2
+ // it goes, how high it lifts and how much the arms swing. A person does not give every step the
3
+ // same time: the first steps build up, the last one slows into the stop, one side is a little
4
+ // quicker than the other, no two steps are alike, and a turn takes slower, shorter steps.
5
+ // Deterministic (a hash of the walk's own parameters seeds the step-to-step part) and zero-sum:
6
+ // the lift-offs are moved inside the walk's span (the walk still ends 16 + steps * period + 18
7
+ // frames after f0) and the middle strides keep their sum (the walk ends where it did). The first
8
+ // stride stays firstStep and the last step still brings the feet together.
9
+ // A person's strides also differ in length from one another as wholes (both steps a little longer or shorter),
10
+ // which the step-to-step terms alone do not show: their left/right part cancels over a stride. LENGTH.stride gives
11
+ // each middle stride its own length. (A stride time term is left out: any more time variation lowers the hips'
12
+ // rise and fall that walk-pelvis.js fits under the legs below a person's ~5 cm.)
13
+ // vary scales every term: 0 is the metronome (every step period frames apart, swing frames in the
14
+ // air, step metres long), 1 the default, 2 twice as uneven.
15
+ const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
16
+
17
+ /** text -> a generator of numbers in [-1, 1) (FNV-1a hash, then mulberry32). */
18
+ function noiseOf(text) {
19
+ let h = 2166136261;
20
+ for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
21
+ let a = h >>> 0;
22
+ return () => {
23
+ a = (a + 0x6d2b79f5) >>> 0;
24
+ let t = Math.imul(a ^ (a >>> 15), a | 1);
25
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
26
+ return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * 2 - 1;
27
+ };
28
+ }
29
+
30
+ // Sizes at vary 1, as fractions of the step's time or length.
31
+ const TIME = { noise: 0.02, side: 0.03, first: 0.22, second: 0.08, last: 0.22, beforeLast: 0.08, turn: 0 };
32
+ const LENGTH = { noise: 0.025, side: 0.012, stride: 0.035, second: 0.05, beforeLast: 0.06, turn: 0.06 };
33
+ const LIFT = { noise: 0.1, first: 0.1, last: 0.25 }, ARM = 0.1;
34
+ const CLOSE = 3; // frames the closing step may swing longer than swing
35
+ const TURN_FULL = 60; // degrees turned in one step at which a step is as slow and short as a turn makes it
36
+
37
+ /**
38
+ * walkRhythm(W, { s0, turn, seed }) -> per step i (0-based, W.steps of them):
39
+ * lift[i] frames after f0 + 16 at which the foot lifts off (lift[0] = 0)
40
+ * air[i] frames that foot is in the air
41
+ * inc[i] metres it advances along the path (inc[0] = firstStep, the last one 0)
42
+ * height[i] its lift, m; arm[i] a factor on the arm swing it lands with
43
+ * W: the walk's fields with defaults; s0 the path position the first step starts from; turn(sa, sb)
44
+ * the degrees the heading turns between two path positions; seed a text naming this walk.
45
+ */
46
+ export function walkRhythm(W, { s0, turn, seed }) {
47
+ const n = W.steps, v = clamp(W.vary ?? 1, 0, 2), next = noiseOf(seed);
48
+ const rows = Array.from({ length: n }, () => ({ time: next(), length: next(), lift: next(), arm: next() }));
49
+ const quick = next() < 0 ? 1 : -1, side = i => (i % 2 === 0 ? quick : -quick);
50
+ // one draw per stride (steps 1-2, 3-4, ...), after the step draws so those stay as they were
51
+ const strides = Array.from({ length: Math.ceil(n / 2) }, () => next()), strideOf = i => strides[Math.floor((i - 1) / 2)];
52
+ const inc0 = Array.from({ length: n }, (_, i) => (i === n - 1 ? 0 : i === 0 ? W.firstStep : W.step));
53
+ let s = s0;
54
+ const turned = inc0.map(d => { const deg = Math.abs(turn(s, s + d)); s += d; return Math.min(1, deg / TURN_FULL); });
55
+ const long = n >= 4;
56
+ // time: a share of the walk's steps * period frames per step, the shares summing to steps
57
+ const share = rows.map((r, i) => {
58
+ if (n < 2) return 1;
59
+ // the first and the last step vary half as much: their build-up and slow-down should show
60
+ const ends = i === 0 || i === n - 1 ? 0.5 : 1;
61
+ let x = ends * (TIME.noise * r.time + TIME.side * side(i)) + TIME.turn * turned[i];
62
+ if (i === 0) x += TIME.first * (1 - turned[0]);
63
+ if (i === 1 && long) x += TIME.second;
64
+ if (i === n - 1) x += TIME.last;
65
+ if (i === n - 2 && long) x += TIME.beforeLast;
66
+ return 1 + v * x;
67
+ });
68
+ const total = share.reduce((a, b) => a + b, 0), span = n * W.period;
69
+ const lift = [];
70
+ for (let i = 0, t = 0; i < n; i++) { lift.push(Math.round(t)); t += (span * share[i]) / total; }
71
+ // air: a long step swings slower, keeping the double support the walk gives every step (period -
72
+ // swing), so the trailing foot leaves the ground no further behind the hips than in an even walk (its
73
+ // knee would snap straight at toe-off); a short step keeps its swing and shortens the double
74
+ // support instead, the trailing heel rising before the other foot lands. The closing step swings at
75
+ // most CLOSE frames longer: the hips go ahead over the other foot while it swings, and a slower
76
+ // trailing foot falls out of the leg's reach; the time left over is standing still
77
+ const support = W.period - W.swing;
78
+ const air = lift.map((t, i) => {
79
+ if (v === 0) return W.swing;
80
+ const gap = (i < n - 1 ? lift[i + 1] : span) - t;
81
+ return Math.min(gap - 2, Math.max(W.swing, gap - support), i === n - 1 ? W.swing + CLOSE : Infinity);
82
+ });
83
+ // length: the middle strides (not the first, not the closing one) keep their sum
84
+ const mid = inc0.map((_, i) => i > 0 && i < n - 1);
85
+ const g = rows.map((r, i) => {
86
+ if (!mid[i]) return 1;
87
+ let x = LENGTH.noise * (0.7 * r.length + 0.3 * r.time) + LENGTH.side * side(i) + LENGTH.stride * strideOf(i) - LENGTH.turn * turned[i];
88
+ if (i === 1 && long) x -= LENGTH.second;
89
+ if (i === n - 2 && long) x -= LENGTH.beforeLast;
90
+ return 1 + v * x;
91
+ });
92
+ const count = mid.filter(Boolean).length, sum = g.reduce((a, b, i) => a + (mid[i] ? b : 0), 0);
93
+ const inc = inc0.map((d, i) => (mid[i] && v > 0 ? (d * g[i] * count) / sum : d));
94
+ const height = rows.map((r, i) => W.lift * (1 + v * (LIFT.noise * r.lift - (i === 0 ? LIFT.first : 0) - (i === n - 1 && n > 1 ? LIFT.last : 0))));
95
+ const arm = rows.map(r => 1 + v * ARM * r.arm);
96
+ return { lift, air, inc, height, arm };
97
+ }