cuju-render 0.1.0__py3-none-any.whl

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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,1238 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Goal celebrations on the pitch, and the dances of the show after the match. The scorer runs
5
+ // off and performs his celebration, teammates run to him and huddle or join in, the other team
6
+ // walks back. Poses come from the animation library of animate.js when it is there, else from
7
+ // simple procedural poses, and are written into the pose buffer in the engine frame.
8
+
9
+ import * as THREE from 'three';
10
+ import { NODE_COUNT, PLAYER_FLOATS, NODES_FLOAT } from '../recording/format.js';
11
+ import { HALF_H, HALF_W } from '../world/pitch.js';
12
+ import { restPoseNodes, walkPoseNodes } from '../players/players.js';
13
+ import { KAYKIT_CELEBRATIONS, KAYKIT_ROLES } from '../players/kaykit.js';
14
+ import { hasUpload, poseUpload } from '../players/retarget.js';
15
+ import { stillRole } from '../presentation/contract.js';
16
+
17
+ // The library loads on its own, a page without it still celebrates with the simple poses.
18
+ let lib = null;
19
+ import('../players/animate.js').then((m) => {
20
+ lib = m;
21
+ }).catch(() => {});
22
+
23
+ export function animationLibrary() {
24
+ return lib;
25
+ }
26
+
27
+ // Names of the library's animations, from a list or a map of them.
28
+ function libraryNames() {
29
+ const all = lib && lib.ANIMATIONS;
30
+ if (!all) {
31
+ return [];
32
+ }
33
+ return Array.isArray(all) ? all.map((a) => (typeof a === 'string' ? a : a.name)).filter(Boolean) : Object.keys(all);
34
+ }
35
+
36
+ function libraryEntry(name) {
37
+ const all = lib && lib.ANIMATIONS;
38
+ if (!all) {
39
+ return null;
40
+ }
41
+ if (Array.isArray(all)) {
42
+ return all.find((a) => a === name || (a && a.name === name)) || null;
43
+ }
44
+ return all[name] || null;
45
+ }
46
+
47
+ // Utility animations by role, the first name the library has wins.
48
+ const ROLES = {
49
+ run: ['run', 'jog', 'sprint'],
50
+ walk: ['walk', 'stroll', 'walk_back'],
51
+ idle: ['idle', 'stand', 'breathe'],
52
+ clap: ['clap', 'applause', 'applaud'],
53
+ dejected: ['dejected', 'hands_on_head', 'sad', 'disappointed'],
54
+ huddle: ['team_huddle', 'huddle', 'hug', 'embrace'],
55
+ };
56
+ const UTILITY = new Set(Object.values(ROLES).flat());
57
+
58
+ export function roleName(role) {
59
+ const names = libraryNames();
60
+ return (ROLES[role] || []).find((n) => names.includes(n)) || null;
61
+ }
62
+
63
+ // The library's celebrations, the entries marked as dances or celebrations, else every entry
64
+ // that is not a utility animation.
65
+ export function danceNames() {
66
+ const names = libraryNames();
67
+ const marked = names.filter((n) => {
68
+ const e = libraryEntry(n);
69
+ if (!e || typeof e !== 'object') {
70
+ return false;
71
+ }
72
+ const kind = e.kind || e.type || e.category || e.group;
73
+ return kind === 'celebration' || kind === 'dance' || kind === 'celebrations' || e.celebration === true || e.dance === true;
74
+ });
75
+ // Clapping and the huddle belong to the teammates, not to a scorer's solo.
76
+ return (marked.length ? marked : names).filter((n) => !UTILITY.has(n));
77
+ }
78
+
79
+ // The library's view of an animation, its kind, length and whether it loops.
80
+ function libraryInfo(name) {
81
+ const e = libraryEntry(name);
82
+ if (!e || typeof e !== 'object') {
83
+ return { kind: '', duration: 0, loop: true, speed: 0 };
84
+ }
85
+ return { kind: e.kind || '', duration: e.duration || 0, loop: e.loop !== false, speed: e.speed || 0 };
86
+ }
87
+
88
+ // The ground speed a gait of the library was made for, so a faster mover steps faster.
89
+ export function gaitSpeed(name) {
90
+ return libraryInfo(name).speed;
91
+ }
92
+
93
+ // A small deterministic random source, so a goal celebrates the same way every time it plays.
94
+ export function seeded(seed) {
95
+ let a = (seed * 2654435761) >>> 0;
96
+ return () => {
97
+ a = (a + 0x6d2b79f5) >>> 0;
98
+ let t = a;
99
+ t = Math.imul(t ^ (t >>> 15), t | 1);
100
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
101
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
102
+ };
103
+ }
104
+
105
+ // The celebration a player performs, from his roster entry. Team picks from the team's list,
106
+ // an empty list or entry picks from every dance. A clip entry names an animation of the model.
107
+ export function celebrationFor(style, index, random) {
108
+ const roster = style && style.players ? style.players[index] : null;
109
+ let choice = roster && typeof roster.celebration === 'string' ? roster.celebration : 'team';
110
+ const dances = danceNames();
111
+ if (choice === 'team') {
112
+ const list = style && Array.isArray(style.celebrations) ? style.celebrations : [];
113
+ choice = list.length ? list[Math.floor(random() * list.length)] : '';
114
+ }
115
+ if (choice && (choice.startsWith('clip:') || dances.includes(choice) || hasUpload(choice))) {
116
+ return choice;
117
+ }
118
+ return dances.length ? dances[Math.floor(random() * dances.length)] : 'dance';
119
+ }
120
+
121
+ // Whether a team or its player chose the celebrations, rather than leaving them to chance.
122
+ export function choseCelebration(style, index) {
123
+ const roster = style && style.players ? style.players[index] : null;
124
+ if (roster && typeof roster.celebration === 'string' && roster.celebration && roster.celebration !== 'team') {
125
+ return true;
126
+ }
127
+ return !!(style && Array.isArray(style.celebrations) && style.celebrations.length);
128
+ }
129
+
130
+ // A KayKit celebration that player k's model plays, picked by random, or null.
131
+ export function kaykitCelebration(hasClip, k, random) {
132
+ if (!hasClip) {
133
+ return null;
134
+ }
135
+ const list = KAYKIT_CELEBRATIONS.filter((n) => hasClip(k, n));
136
+ return list.length ? list[Math.floor(random() * list.length)] : null;
137
+ }
138
+
139
+ // The KayKit role that stands in for each name of the library on a model that plays clips.
140
+ const CLIP_ROLES = {
141
+ idle: 'idle', idle_confident: 'calm', idle_bounce: 'calm', team_huddle: 'huddle', clap: 'wave',
142
+ arms_up: 'cheer', jump_fist: 'jump', dance: 'celebrate', hands_on_head: 'crouch', point: 'handshake',
143
+ };
144
+ // Roles whose clips vary from player to player, so a crowd does not move as one.
145
+ const VARIED = new Set(['idle', 'celebrate', 'jump', 'cheer']);
146
+
147
+ // The clip player k of a choreography plays for a still pose, a clip name that his model has,
148
+ // the KayKit clip of the library name's role, or null for the library's own pose.
149
+ export function stillClip(group, k, still) {
150
+ if (!still || !group.hasClip) {
151
+ return null;
152
+ }
153
+ const cache = group.clipCache || (group.clipCache = new Map());
154
+ const key = `${k}:${still}`;
155
+ if (cache.has(key)) {
156
+ return cache.get(key);
157
+ }
158
+ let out = null;
159
+ try {
160
+ // A team presentation package plays its clip of the pose's semantic role first.
161
+ const semantic = stillRole(still);
162
+ if (semantic && group.hasClip(k, `role:${semantic}`)) {
163
+ out = `role:${semantic}`;
164
+ } else if (still.startsWith('clip:')) {
165
+ out = group.hasClip(k, still) ? still : null;
166
+ } else {
167
+ const role = CLIP_ROLES[still];
168
+ const list = role ? (KAYKIT_ROLES[role] || []).filter((n) => group.hasClip(k, n)) : [];
169
+ if (list.length) {
170
+ out = VARIED.has(role) ? list[k % list.length] : list[0];
171
+ }
172
+ }
173
+ } catch {
174
+ out = null;
175
+ }
176
+ cache.set(key, out);
177
+ return out;
178
+ }
179
+
180
+ // The flavor of the show after the match, the player's own celebration, else his team's first,
181
+ // else any dance.
182
+ export function showDance(style, index, random = Math.random) {
183
+ const roster = style && style.players ? style.players[index] : null;
184
+ const own = roster && roster.celebration;
185
+ if (own && own !== 'team') {
186
+ return celebrationFor(style, index, random);
187
+ }
188
+ const list = style && Array.isArray(style.celebrations) ? style.celebrations : [];
189
+ if (list.length) {
190
+ return list[0];
191
+ }
192
+ // Else a dance that keeps going, for a show that lasts.
193
+ const loops = danceNames().filter((n) => libraryInfo(n).loop && libraryInfo(n).duration);
194
+ if (loops.length) {
195
+ return loops[Math.floor(random() * loops.length)];
196
+ }
197
+ return celebrationFor(style, index, random);
198
+ }
199
+
200
+ // The heading that faces engine direction dx, dy, as restPoseNodes turns a player.
201
+ export function headingTo(dx, dy) {
202
+ return Math.atan2(dx, -dy);
203
+ }
204
+
205
+ const scratch = new Float32Array(PLAYER_FLOATS);
206
+ const scratchNext = new Float32Array(PLAYER_FLOATS);
207
+
208
+ function shift(out, o, x, y, z) {
209
+ for (let n = 0; n < NODE_COUNT; n++) {
210
+ out[o + n * 7] += x;
211
+ out[o + n * 7 + 1] += y;
212
+ out[o + n * 7 + 2] += z;
213
+ }
214
+ }
215
+
216
+ // Writes one player's pose for animation name at time t into out at offset o, standing at x, y
217
+ // and turned to heading. Names of the library go through animate.pose, a clip of the model
218
+ // stands idle here while the model plays it, and the rest fall back to simple poses.
219
+ export function posePlayer(name, t, out, o, x, y, heading, mode = '') {
220
+ // A clip a team uploaded plays on the nodes, so every model follows it. Pinned to its spot on
221
+ // a podium, a stage or beside the scorer, and kept on the pitch for the scorer.
222
+ if (hasUpload(name)) {
223
+ const pin = mode === 'show' || mode === 'pin';
224
+ poseUpload(name, t, out, o, { x, y, heading, pin, bounds: pin ? null : [HALF_W, HALF_H] });
225
+ return;
226
+ }
227
+ if (lib && typeof lib.pose === 'function' && name && libraryEntry(name)) {
228
+ try {
229
+ const info = libraryInfo(name);
230
+ const opts = { x, y, heading };
231
+ const repeat = mode === 'show';
232
+ if (info.loop || !info.duration) {
233
+ lib.pose(name, t, scratch, opts);
234
+ } else {
235
+ // A celebration that plays once goes on into a loop of arms up where it ended,
236
+ // or starts over after a pause for the show, other animations hold their last
237
+ // pose.
238
+ const end = info.duration;
239
+ let local = t;
240
+ let next = null;
241
+ if (repeat) {
242
+ local = t % (end + 1.4);
243
+ next = roleName('idle');
244
+ } else if (info.kind === 'celebration') {
245
+ next = ['arms_up', 'jump_fist'].find((n) => libraryEntry(n)) || null;
246
+ }
247
+ lib.pose(name, Math.min(local, end), scratch, opts);
248
+ if (local > end + 0.5 && next) {
249
+ const u = local - end - 0.5;
250
+ lib.pose(next, u, scratchNext, { x: scratch[0], y: scratch[1], heading });
251
+ blendInto(scratch, 0, scratchNext, 0, Math.min(1, u / 0.45));
252
+ }
253
+ }
254
+ // On a podium, a stage or beside the scorer, a dance that travels stays on its spot.
255
+ if (mode === 'show' || mode === 'pin') {
256
+ shift(scratch, 0, x - scratch[0], y - scratch[1], 0);
257
+ }
258
+ out.set(scratch, o);
259
+ return;
260
+ } catch {
261
+ // A failing animation falls back to the simple poses below.
262
+ }
263
+ }
264
+ const kind = (name && Object.keys(ROLES).find((r) => ROLES[r].includes(name))) || name;
265
+ if (kind === 'run') {
266
+ walkPoseNodes(t * Math.PI * 2 * 1.5, 6, heading, scratch);
267
+ out.set(scratch, o);
268
+ shift(out, o, x, y, 0);
269
+ } else if (kind === 'walk') {
270
+ walkPoseNodes(t * Math.PI * 2 * 0.9, 1.6, heading, scratch);
271
+ out.set(scratch, o);
272
+ shift(out, o, x, y, 0);
273
+ } else if (kind === 'idle' || kind === 'clap' || kind === 'dejected' || kind === 'huddle' || (name && name.startsWith('clip:'))) {
274
+ restPoseNodes(heading, scratch);
275
+ out.set(scratch, o);
276
+ shift(out, o, x, y, Math.sin(t * 2.2) * 0.008);
277
+ } else {
278
+ // A bouncing step on the spot, turning from side to side.
279
+ walkPoseNodes(t * Math.PI * 2 * 1.8, 2.5, heading + Math.sin(t * 2.4) * 0.7, scratch);
280
+ out.set(scratch, o);
281
+ shift(out, o, x, y, Math.abs(Math.sin(t * Math.PI * 1.8)) * 0.12);
282
+ }
283
+ }
284
+
285
+ // Blends one player's pose b into a by weight w, positions linearly and rotations along the
286
+ // shorter arc, normalized.
287
+ export function blendInto(a, ao, b, bo, w) {
288
+ if (w <= 0) {
289
+ return;
290
+ }
291
+ if (w >= 1) {
292
+ for (let i = 0; i < PLAYER_FLOATS; i++) {
293
+ a[ao + i] = b[bo + i];
294
+ }
295
+ return;
296
+ }
297
+ for (let n = 0; n < NODE_COUNT; n++) {
298
+ const p = ao + n * 7;
299
+ const q = bo + n * 7;
300
+ a[p] += (b[q] - a[p]) * w;
301
+ a[p + 1] += (b[q + 1] - a[p + 1]) * w;
302
+ a[p + 2] += (b[q + 2] - a[p + 2]) * w;
303
+ const ax = a[p + 3];
304
+ const ay = a[p + 4];
305
+ const az = a[p + 5];
306
+ const aw = a[p + 6];
307
+ let bx = b[q + 3];
308
+ let by = b[q + 4];
309
+ let bz = b[q + 5];
310
+ let bw = b[q + 6];
311
+ if (ax === 0 && ay === 0 && az === 0 && aw === 0) {
312
+ a[p + 3] = bx;
313
+ a[p + 4] = by;
314
+ a[p + 5] = bz;
315
+ a[p + 6] = bw;
316
+ continue;
317
+ }
318
+ if (ax * bx + ay * by + az * bz + aw * bw < 0) {
319
+ bx = -bx;
320
+ by = -by;
321
+ bz = -bz;
322
+ bw = -bw;
323
+ }
324
+ const x = ax + (bx - ax) * w;
325
+ const y = ay + (by - ay) * w;
326
+ const z = az + (bz - az) * w;
327
+ const s = aw + (bw - aw) * w;
328
+ const len = Math.hypot(x, y, z, s) || 1;
329
+ a[p + 3] = x / len;
330
+ a[p + 4] = y / len;
331
+ a[p + 5] = z / len;
332
+ a[p + 6] = s / len;
333
+ }
334
+ }
335
+
336
+ const smooth = (u) => {
337
+ const s = Math.min(1, Math.max(0, u));
338
+ return s * s * (3 - 2 * s);
339
+ };
340
+
341
+ // The ball after the last frame before a goal, flying on from its last velocity into the net,
342
+ // slowing in the netting and dropping to the grass. t is seconds after frame b.
343
+ export function ballIntoNet(a, b, team, sps, t, out) {
344
+ const goalX = team === 0 ? HALF_W : -HALF_W;
345
+ const dir = Math.sign(goalX);
346
+ let vx = (b[0] - a[0]) * sps;
347
+ let vy = (b[1] - a[1]) * sps;
348
+ let vz = (b[2] - a[2]) * sps;
349
+ // A ball that the frames show slow or going the other way still finishes in the net.
350
+ if (vx * dir < 4) {
351
+ vx = 8 * dir;
352
+ }
353
+ const stopX = goalX + dir * 1.6;
354
+ const tStop = Math.max(0.05, (stopX - b[0]) / vx);
355
+ const s = Math.min(t, tStop);
356
+ // Linear speed until the net, a quick stop over 0.4 seconds after.
357
+ let x = b[0] + vx * s;
358
+ let y = b[1] + vy * s;
359
+ let z = b[2] + vz * s - 4.9 * s * s;
360
+ if (t > tStop) {
361
+ // The netting gives a little and holds the ball.
362
+ const u = Math.min(1, (t - tStop) / 0.4);
363
+ x += dir * 0.35 * (1 - (1 - u) * (1 - u));
364
+ y += Math.sign(vy) * Math.min(0.3, Math.abs(vy) * 0.03) * u;
365
+ vz = 0;
366
+ }
367
+ // Inside the goal, never through its back or roof.
368
+ const back = Math.abs(goalX) + 2.3;
369
+ if (Math.abs(x) > back && Math.abs(b[0]) <= back) {
370
+ x = dir * back;
371
+ }
372
+ const zStop = Math.max(0.11, b[2] + vz * tStop - 4.9 * tStop * tStop);
373
+ if (t > tStop) {
374
+ const fall = Math.min(1, (t - tStop) / 0.45);
375
+ z = zStop + (0.11 - zStop) * fall * fall;
376
+ }
377
+ out[0] = x;
378
+ out[1] = Math.max(-3.4, Math.min(3.4, y));
379
+ out[2] = Math.max(0.11, Math.abs(x) > Math.abs(goalX) ? Math.min(2.3, z) : z);
380
+ }
381
+
382
+ // One goal's celebration, a pure function of the time since the frame before the goal. base is
383
+ // the frame's data, its players' nodes start at NODES_FLOAT.
384
+ export class Celebration {
385
+ constructor({ frame, before, header, styles, team, scorer, sps, seed, hasClip = () => false, radius = null }) {
386
+ this.hasClip = hasClip;
387
+ this.radii = header.players.map((p, k) => (radius ? radius(k) : DEFAULT_RADIUS));
388
+ this.clipped = new Set();
389
+ this.header = header;
390
+ this.team = team;
391
+ this.scorer = scorer;
392
+ this.sps = sps;
393
+ this.frame = frame;
394
+ this.ballA = before.data.slice(8, 11);
395
+ this.ballB = frame.data.slice(8, 11);
396
+ this.base = frame.data.subarray(NODES_FLOAT);
397
+ this.count = header.players.length;
398
+ this.work = new Float32Array(PLAYER_FLOATS);
399
+ const random = seeded(seed);
400
+ const at = (k) => ({ x: this.base[k * PLAYER_FLOATS], y: this.base[k * PLAYER_FLOATS + 1] });
401
+ const goalX = team === 0 ? HALF_W : -HALF_W;
402
+ const dir = Math.sign(goalX);
403
+ this.plans = [];
404
+ // The scorer runs towards the corner of the end he attacked, ten meters or so, and
405
+ // turns to the pitch to celebrate.
406
+ let spot = { x: goalX - dir * 8, y: 0 };
407
+ let dance = 'dance';
408
+ if (scorer >= 0) {
409
+ const p = at(scorer);
410
+ const side = Math.sign(p.y) || (random() < 0.5 ? -1 : 1);
411
+ const corner = { x: goalX - dir * 3, y: side * (HALF_H - 4) };
412
+ let dx = corner.x - p.x;
413
+ let dy = corner.y - p.y;
414
+ const len = Math.hypot(dx, dy) || 1;
415
+ const run = Math.min(len, 9 + random() * 4);
416
+ dx /= len;
417
+ dy /= len;
418
+ spot = {
419
+ x: Math.max(-HALF_W + 1, Math.min(HALF_W - 1, p.x + dx * run)),
420
+ y: Math.max(-HALF_H + 1, Math.min(HALF_H - 1, p.y + dy * run)),
421
+ };
422
+ const pl = header.players[scorer];
423
+ const style = styles ? styles[pl.team] : null;
424
+ dance = celebrationFor(style, pl.index, random);
425
+ // A KayKit scorer left to chance plays one of KayKit's own celebrations.
426
+ if (!choseCelebration(style, pl.index)) {
427
+ dance = kaykitCelebration(hasClip, scorer, random) || dance;
428
+ }
429
+ // A clip the scorer's model does not carry gives way to a dance of the library.
430
+ if (dance.startsWith('clip:') && !hasClip(scorer, dance)) {
431
+ const dances = danceNames();
432
+ dance = dances.length ? dances[Math.floor(random() * dances.length)] : 'dance';
433
+ }
434
+ const runT = Math.max(1.2, run / 5.2);
435
+ this.plans[scorer] = {
436
+ kind: 'scorer', from: p, to: spot, start: 0.15, runT, dance,
437
+ heading: headingTo(-spot.x * 0.4, -spot.y),
438
+ };
439
+ }
440
+ this.spot = spot;
441
+ this.dance = dance;
442
+ const plain = danceNames();
443
+ this.plainDance = dance.startsWith('clip:') ? (plain[Math.floor(random() * plain.length)] || 'dance') : dance;
444
+ // Teammates gather in a ring around the spot, all huddling, all joining the dance, or
445
+ // a mix, chosen per goal.
446
+ const join = random();
447
+ const teamRadius = Math.max(DEFAULT_RADIUS, ...header.players.map((p, k) => (p.team === team ? this.radii[k] : 0)));
448
+ const mates = header.players.map((p, k) => ({ p, k }))
449
+ .filter((m) => m.p.team === team && m.k !== scorer && !m.p.goalkeeper)
450
+ .sort((a, b) => Math.hypot(at(a.k).x - spot.x, at(a.k).y - spot.y) - Math.hypot(at(b.k).x - spot.x, at(b.k).y - spot.y));
451
+ mates.forEach((m, i) => {
452
+ const from = at(m.k);
453
+ // Two rows behind the scorer and to his sides, so the cameras in front see him.
454
+ const face = this.plans[scorer] ? this.plans[scorer].heading : 0;
455
+ const behind = Math.atan2(Math.cos(face), -Math.sin(face));
456
+ const back = i >= 5;
457
+ const offsets = back ? [0.3, -0.3, 0.85, -0.85, 1.35, -1.35] : [0, 0.6, -0.6, 1.15, -1.15];
458
+ const j = back ? i - 5 : i;
459
+ // Rings wide enough for the team's biggest model, neighbours a body apart.
460
+ const reach = 2 * teamRadius + 0.15;
461
+ const front = Math.max(1.3, reach / (2 * Math.sin(0.3)));
462
+ const ring = (back ? front + reach : front) + Math.floor(j / offsets.length) * reach;
463
+ const angle = behind + offsets[j % offsets.length] + (random() - 0.5) * 0.12;
464
+ const to = { x: spot.x + Math.cos(angle) * ring, y: spot.y + Math.sin(angle) * ring };
465
+ const speed = 6 + random() * 1.6;
466
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
467
+ // Nobody beats the scorer to his spot, the nearest arrive just after him.
468
+ const scorerPlan = this.plans[scorer];
469
+ const arrive = scorerPlan ? scorerPlan.start + scorerPlan.runT + 0.3 + i * 0.2 : 0;
470
+ const delay = Math.max(0.2 + random() * 0.5, arrive - dist / speed);
471
+ let mode = 'huddle';
472
+ if (join < 0.35 || (join < 0.7 && i % 2 === 1)) {
473
+ mode = 'dance';
474
+ }
475
+ this.plans[m.k] = {
476
+ kind: 'mate', from, to, start: delay, runT: dist / speed, speed, mode,
477
+ heading: mode === 'dance' ? null : headingTo(spot.x - to.x, spot.y - to.y),
478
+ offset: random() * 0.4,
479
+ };
480
+ });
481
+ header.players.forEach((p, k) => {
482
+ if (this.plans[k]) {
483
+ return;
484
+ }
485
+ const from = at(k);
486
+ if (p.team === team) {
487
+ // The scorer's keeper takes a few steps out and applauds.
488
+ this.plans[k] = { kind: 'keeper', from, to: { x: from.x + dir * 4, y: from.y * 0.8 }, start: 0.6, speed: 1.6, mode: 'clap' };
489
+ } else if (p.goalkeeper) {
490
+ this.plans[k] = { kind: 'beaten', from, to: from, start: 0, speed: 0, mode: 'dejected' };
491
+ } else {
492
+ // The others walk slowly back towards the halfway line for the kick-off.
493
+ const to = { x: from.x * 0.55, y: from.y * 0.7 };
494
+ this.plans[k] = { kind: 'back', from, to, start: 0.5 + random() * 0.8, speed: 1.3 + random() * 0.5, mode: 'idle' };
495
+ }
496
+ });
497
+ }
498
+
499
+ // Hands every model that played a clip back to the nodes.
500
+ release(stopClip) {
501
+ for (const k of this.clipped) {
502
+ stopClip(k);
503
+ }
504
+ this.clipped.clear();
505
+ }
506
+
507
+ // The ground position and heading of player k at time t, and the animation he plays.
508
+ state(k, t) {
509
+ const plan = this.plans[k];
510
+ if (!plan) {
511
+ return null;
512
+ }
513
+ const { from, to } = plan;
514
+ const dx = to.x - from.x;
515
+ const dy = to.y - from.y;
516
+ const dist = Math.hypot(dx, dy);
517
+ const runHeading = dist > 0.01 ? headingTo(dx, dy) : 0;
518
+ if (plan.kind === 'scorer') {
519
+ const u = Math.min(1, Math.max(0, (t - plan.start) / plan.runT));
520
+ // Fast off the mark, slowing into the spot.
521
+ const e = 1 - (1 - u) * (1 - u);
522
+ const pos = { x: from.x + dx * e, y: from.y + dy * e };
523
+ const turn = smooth((t - plan.start - plan.runT + 0.2) / 0.5);
524
+ return {
525
+ ...pos,
526
+ heading: lerpAngle(runHeading, plan.heading, turn),
527
+ moving: u < 1 ? 'run' : null,
528
+ travelled: dist * e,
529
+ still: plan.dance,
530
+ mix: smooth((t - plan.start - plan.runT + 0.25) / 0.35),
531
+ still_t: t - plan.start - plan.runT,
532
+ anchor: true,
533
+ };
534
+ }
535
+ if (plan.kind === 'mate') {
536
+ const travelled = Math.max(0, (t - plan.start) * plan.speed);
537
+ const f = dist > 0 ? Math.min(1, travelled / dist) : 1;
538
+ const pos = { x: from.x + dx * f, y: from.y + dy * f };
539
+ const arrive = plan.start + plan.runT;
540
+ const scorerHeading = this.plans[this.scorer] ? this.plans[this.scorer].heading : runHeading;
541
+ const copy = this.dance.startsWith('clip:') && !this.hasClip(k, this.dance) ? this.plainDance : this.dance;
542
+ const still = plan.mode === 'dance' ? copy : (roleName('huddle') || roleName('clap') || 'idle');
543
+ const heading = plan.mode === 'dance' ? scorerHeading : plan.heading;
544
+ return {
545
+ ...pos,
546
+ heading: lerpAngle(runHeading, heading, smooth((t - arrive + 0.2) / 0.5)),
547
+ moving: f < 1 ? 'run' : null,
548
+ travelled: Math.min(dist, travelled),
549
+ still,
550
+ mix: smooth((t - arrive + 0.2) / 0.35),
551
+ still_t: t - arrive + plan.offset,
552
+ pin: true,
553
+ };
554
+ }
555
+ const travelled = Math.max(0, (t - plan.start) * plan.speed);
556
+ const f = dist > 0.01 ? Math.min(1, travelled / dist) : 1;
557
+ const pos = { x: from.x + dx * f, y: from.y + dy * f };
558
+ const still = roleName(plan.mode) || roleName('idle') || 'idle';
559
+ let heading = runHeading;
560
+ if (plan.kind === 'beaten') {
561
+ heading = headingTo(-from.x, -from.y * 0.2);
562
+ }
563
+ const walking = f < 1 && t > plan.start;
564
+ const head = plan.kind === 'back' || plan.kind === 'beaten' ? 0.35 : 0;
565
+ return { ...pos, heading, moving: walking ? 'walk' : null, travelled: Math.min(dist, travelled), still, mix: walking ? 0 : 1, still_t: t - plan.start, head };
566
+ }
567
+
568
+ // The scorer's place, for the cameras.
569
+ focus(t) {
570
+ const s = this.scorer >= 0 ? this.state(this.scorer, t) : null;
571
+ return s || { x: this.spot.x, y: this.spot.y, heading: 0 };
572
+ }
573
+
574
+ // Writes every player and the ball at time t, blended in from the frame's own pose over the
575
+ // first 0.3 seconds, and by weight overall, which a stop may ease out.
576
+ apply(pose, t, weight = 1, playClip = null) {
577
+ writeGroup(this, pose, t, weight, playClip);
578
+ if (pose.ball) {
579
+ const b = ballScratch;
580
+ ballIntoNet(this.ballA, this.ballB, this.team, this.sps, t, b);
581
+ const bw = Math.min(1, weight);
582
+ pose.ball[0] += (b[0] - pose.ball[0]) * bw;
583
+ pose.ball[1] += (b[1] - pose.ball[1]) * bw;
584
+ pose.ball[2] += (b[2] - pose.ball[2]) * bw;
585
+ }
586
+ }
587
+ }
588
+
589
+ // The horizontal radius of a player when the figures cannot tell, GRF's player with his arms in.
590
+ export const DEFAULT_RADIUS = 0.35;
591
+
592
+ // Keeps the players apart by their radii, pushing each pair that overlaps along the line between
593
+ // them, an anchored player stays put. Three rounds settle a crowd well enough, and the result
594
+ // follows from the states alone, so it holds when the clock runs backwards too.
595
+ function separate(states, radii) {
596
+ for (let round = 0; round < 3; round++) {
597
+ for (let i = 0; i < states.length; i++) {
598
+ const a = states[i];
599
+ if (!a) {
600
+ continue;
601
+ }
602
+ for (let j = i + 1; j < states.length; j++) {
603
+ const b = states[j];
604
+ if (!b) {
605
+ continue;
606
+ }
607
+ let dx = b.x - a.x;
608
+ let dy = b.y - a.y;
609
+ const min = radii[i] + radii[j] + 0.05;
610
+ const d2 = dx * dx + dy * dy;
611
+ if (d2 >= min * min) {
612
+ continue;
613
+ }
614
+ let d = Math.sqrt(d2);
615
+ if (d < 1e-4) {
616
+ // Two on one spot part sideways, the same way every time.
617
+ dx = Math.cos(i * 2.399);
618
+ dy = Math.sin(i * 2.399);
619
+ d = 0;
620
+ } else {
621
+ dx /= d;
622
+ dy /= d;
623
+ }
624
+ const push = min - d;
625
+ let wa = 0.5;
626
+ let wb = 0.5;
627
+ if (a.anchor) {
628
+ wa = 0;
629
+ wb = b.anchor ? 0 : 1;
630
+ } else if (b.anchor) {
631
+ wa = 1;
632
+ wb = 0;
633
+ }
634
+ a.x -= dx * push * wa;
635
+ a.y -= dy * push * wa;
636
+ b.x += dx * push * wb;
637
+ b.y += dy * push * wb;
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ // Tips the head of the player at o forward by angle radians, a turn of the neck node about its
644
+ // own x axis, which leaves the shoulders where they are.
645
+ function bowHead(nodes, o, angle) {
646
+ const n = o + 3 * 7;
647
+ const s = Math.sin(angle / 2);
648
+ const c = Math.cos(angle / 2);
649
+ const x = nodes[n + 3];
650
+ const y = nodes[n + 4];
651
+ const z = nodes[n + 5];
652
+ const w = nodes[n + 6];
653
+ if (x === 0 && y === 0 && z === 0 && w === 0) {
654
+ return;
655
+ }
656
+ nodes[n + 3] = w * s + x * c;
657
+ nodes[n + 4] = y * c + z * s;
658
+ nodes[n + 5] = z * c - y * s;
659
+ nodes[n + 6] = w * c - x * s;
660
+ }
661
+
662
+ // Poses every player of a choreography at time t from its states, kept apart, blended in from
663
+ // the frame's own pose over the first 0.3 seconds.
664
+ function writeGroup(group, pose, t, weight, playClip) {
665
+ const nodes = pose.nodes;
666
+ const w = Math.min(weight, smooth(t / 0.3));
667
+ const work = group.work;
668
+ const states = [];
669
+ for (let k = 0; k < group.count; k++) {
670
+ states.push(group.state(k, t));
671
+ }
672
+ separate(states, group.radii);
673
+ for (let k = 0; k < group.count; k++) {
674
+ const s = states[k];
675
+ if (!s) {
676
+ continue;
677
+ }
678
+ const o = k * PLAYER_FLOATS;
679
+ const still = s.still;
680
+ const mode = s.pin ? 'pin' : '';
681
+ // A model with clips plays the clip of the still pose over the nodes, as strongly as the
682
+ // pose shows, every frame it shows.
683
+ const clip = playClip ? stillClip(group, k, still) : null;
684
+ const stillT = Math.max(0, s.still_t);
685
+ if (s.moving && s.mix < 1) {
686
+ const gait = roleName(s.moving) || s.moving;
687
+ const base = gaitSpeed(gait);
688
+ // The library's gaits step for their own speed, so the steps follow the ground
689
+ // covered and the feet do not slide.
690
+ posePlayer(gait, base > 0 ? s.travelled / base : t, work, 0, s.x, s.y, s.heading);
691
+ // A player slowing down passes from one gait to the next, each stepping for the
692
+ // ground it covers.
693
+ if (s.moving2 && s.gaitMix > 0) {
694
+ const gait2 = roleName(s.moving2) || s.moving2;
695
+ const base2 = gaitSpeed(gait2);
696
+ posePlayer(gait2, base2 > 0 ? s.travelled / base2 : t, scratchGait, 0, s.x, s.y, s.heading);
697
+ blendInto(work, 0, scratchGait, 0, s.gaitMix);
698
+ }
699
+ if (s.mix > 0) {
700
+ posePlayer(still, stillT, scratchStill, 0, s.x, s.y, s.heading, mode);
701
+ blendInto(work, 0, scratchStill, 0, s.mix);
702
+ if (clip) {
703
+ group.clipped.add(k);
704
+ playClip(k, clip, stillT, s.mix * w);
705
+ }
706
+ }
707
+ } else {
708
+ posePlayer(still, stillT, work, 0, s.x, s.y, s.heading, mode);
709
+ if (clip) {
710
+ group.clipped.add(k);
711
+ playClip(k, clip, stillT, w);
712
+ }
713
+ }
714
+ if (s.head) {
715
+ bowHead(work, 0, s.head);
716
+ }
717
+ blendInto(nodes, o, work, 0, w);
718
+ }
719
+ }
720
+
721
+ const scratchStill = new Float32Array(PLAYER_FLOATS);
722
+ const scratchGait = new Float32Array(PLAYER_FLOATS);
723
+ const ballScratch = new Float32Array(3);
724
+
725
+ function lerpAngle(a, b, w) {
726
+ let d = b - a;
727
+ while (d > Math.PI) {
728
+ d -= Math.PI * 2;
729
+ }
730
+ while (d < -Math.PI) {
731
+ d += Math.PI * 2;
732
+ }
733
+ return a + d * Math.min(1, Math.max(0, w));
734
+ }
735
+
736
+ // The size of player k of a set of figures, the horizontal radius and the height in meters, from
737
+ // the figures when they can tell, else measured once from the figure's own bounds.
738
+ const sizeCache = new WeakMap();
739
+
740
+ export function figureSize(players, k) {
741
+ if (!players) {
742
+ return { radius: DEFAULT_RADIUS, height: 1.85 };
743
+ }
744
+ let cache = sizeCache.get(players);
745
+ if (!cache) {
746
+ cache = new Map();
747
+ sizeCache.set(players, cache);
748
+ }
749
+ if (cache.has(k)) {
750
+ return cache.get(k);
751
+ }
752
+ let radius = null;
753
+ let height = null;
754
+ try {
755
+ if (typeof players.radius === 'function') {
756
+ radius = players.radius(k);
757
+ }
758
+ if (typeof players.height === 'function') {
759
+ height = players.height(k);
760
+ }
761
+ if (!(radius > 0) || !(height > 0)) {
762
+ const part = players.where ? players.where[k] : null;
763
+ const entry = part && part.byK ? part.byK.get(k) : null;
764
+ const root = entry ? (entry.instance ? entry.instance.root : entry.root) : null;
765
+ if (root) {
766
+ const box = new THREE.Box3().setFromObject(root);
767
+ const size = box.getSize(new THREE.Vector3());
768
+ if (Number.isFinite(size.x) && size.y > 0.3) {
769
+ radius = radius > 0 ? radius : Math.max(size.x, size.z) / 2;
770
+ height = height > 0 ? height : size.y;
771
+ }
772
+ }
773
+ }
774
+ } catch {
775
+ // The bounds are a nicety, the defaults below still keep players apart.
776
+ }
777
+ const out = {
778
+ radius: Math.max(0.25, Math.min(1.2, radius > 0 ? radius : DEFAULT_RADIUS)),
779
+ height: Math.max(1, Math.min(3.5, height > 0 ? height : 1.85)),
780
+ };
781
+ cache.set(k, out);
782
+ return out;
783
+ }
784
+
785
+ // Where a player of a frame faces, the yaw of his root node.
786
+ function yawOf(base, k) {
787
+ const o = k * PLAYER_FLOATS;
788
+ const z = base[o + 5];
789
+ const w = base[o + 6];
790
+ return z === 0 && w === 0 ? 0 : 2 * Math.atan2(z, w);
791
+ }
792
+
793
+ // Up to this far a player walks towards the fans in the time the shots leave him.
794
+ const STAND_WALK = 9;
795
+
796
+ // The players after the final whistle, a pure function of the time since the last frame. The
797
+ // winners celebrate, jumping, punching the air, gathering in a huddle, a few running to the fans.
798
+ // The losers slump with their hands on their heads and walk slowly with heads down. A draw mixes
799
+ // both and has handshakes. Later everyone walks towards the far stand and applauds the fans.
800
+ export class Reactions {
801
+ constructor({ frame, header, winner, seed, radius = null, applause = 7.2, hasClip = null, hero = -1, prev = null, sps = 10, card = 0 }) {
802
+ this.hasClip = hasClip;
803
+ this.hero = hero;
804
+ this.header = header;
805
+ this.count = header.players.length;
806
+ this.base = frame.data.subarray(NODES_FLOAT);
807
+ this.flags = frame.playerFlags;
808
+ this.work = new Float32Array(PLAYER_FLOATS);
809
+ this.clipped = new Set();
810
+ this.radii = header.players.map((p, k) => (radius ? radius(k) : DEFAULT_RADIUS));
811
+ const random = seeded(seed);
812
+ // The play winds down after the whistle, each player slowing from his last stride to a
813
+ // stand over two to four seconds, and the ball rolls on until the grass stops it.
814
+ this.windDown(frame, prev, sps, seeded(seed + 977));
815
+ // A card of card seconds comes up once play has stopped, and the players react as it
816
+ // goes, standing still behind it.
817
+ this.titleAt = Math.max(1.5, Math.min(4.4, this.windEnd + 0.4));
818
+ this.hold = card > 0 ? this.titleAt + card - 0.7 : this.windEnd;
819
+ applause += this.hold;
820
+ const at = (k) => ({ x: this.wind[k].sx, y: this.wind[k].sy });
821
+ const pick = (names) => {
822
+ const have = names.filter((n) => !lib || libraryEntry(n));
823
+ return have.length ? have[Math.floor(random() * have.length)] : names[0];
824
+ };
825
+ const role = (names, fallback) => names.find((n) => libraryEntry(n)) || fallback;
826
+ const dejected = () => role(['slump', 'crouch', 'kneel', 'hands_on_head'], 'idle');
827
+ const handshake = role(['handshake', 'shake_hands', 'point'], 'idle');
828
+ const clap = roleName('clap') || 'idle';
829
+ this.plans = [];
830
+ const on = (k) => !!(this.flags[k] & 1);
831
+ // Everyone applauds the far stand in the end, from wherever the earlier part left him.
832
+ const toStand = (from, extra = 0) => {
833
+ const dy = HALF_H - 3 - (random() * 3 + extra) - from.y;
834
+ const dx = -from.x * 0.25;
835
+ const len = Math.hypot(dx, dy) || 1;
836
+ const go = Math.min(len, STAND_WALK);
837
+ return { x: from.x + (dx / len) * go, y: from.y + (dy / len) * go };
838
+ };
839
+ const standHeading = headingTo(0, 1);
840
+ header.players.forEach((p, k) => {
841
+ if (!on(k)) {
842
+ return;
843
+ }
844
+ const from = at(k);
845
+ const w = this.wind[k];
846
+ const plan = { from, heading: w.T > 0 ? headingTo(w.vx, w.vy) : yawOf(this.base, k), react: 'idle', reactAt: 0.3 + random() * 0.6, legs: [], head: 0 };
847
+ const won = winner >= 0 && p.team === winner;
848
+ const lost = winner >= 0 && p.team !== winner;
849
+ if (won && k === hero) {
850
+ // The hero jumps and punches the air where he stands, facing the main stand, a
851
+ // KayKit hero cheers with his arms high.
852
+ plan.react = hasClip && hasClip(k, 'clip:Cheering') ? 'clip:Cheering' : role(['jump_fist', 'arms_up'], 'idle');
853
+ plan.legs.push({ to: from, start: plan.reactAt + 0.2, speed: 1, gait: 'walk', still: plan.react, heading: headingTo(0, -1) });
854
+ } else if (won) {
855
+ plan.react = p.goalkeeper ? role(['arms_up', 'jump_fist'], 'idle') : pick(['jump_fist', 'arms_up', 'dance', 'jump_fist']);
856
+ } else if (lost) {
857
+ plan.react = p.goalkeeper || random() < 0.7 ? dejected() : 'idle';
858
+ plan.head = 0.5;
859
+ // A few slow steps towards the middle, heads down.
860
+ const to = { x: from.x * 0.85, y: from.y * 0.85 };
861
+ const leg = { to, start: 2.6 + random() * 1.4, speed: 0.8, gait: 'walk', still: 'idle', heading: null };
862
+ // A KayKit loser sinks to the grass, the keeper flat on his back, and gets up
863
+ // again before he walks.
864
+ const floor = plan.react !== 'idle' ? this.floorClip(k, p.goalkeeper, random) : null;
865
+ if (floor) {
866
+ plan.react = floor.down;
867
+ plan.getUp = { name: floor.up, at: Math.max(leg.start, plan.reactAt + floor.hold), length: floor.length };
868
+ leg.start = plan.getUp.at + floor.length;
869
+ }
870
+ plan.legs.push(leg);
871
+ } else {
872
+ plan.react = pick(['idle_confident', 'hands_on_head', 'clap', 'idle_confident']);
873
+ plan.head = plan.react === 'hands_on_head' ? 0.3 : 0;
874
+ }
875
+ this.plans[k] = plan;
876
+ });
877
+ if (winner >= 0) {
878
+ this.bundleWinners(winner, at, on, random);
879
+ } else {
880
+ this.shakeHands(at, random, handshake);
881
+ }
882
+ // Everything a player does after the whistle waits until he has come to a stand.
883
+ this.plans.forEach((plan, k) => {
884
+ const wait = plan ? Math.max(this.wind[k].T, this.hold) : 0;
885
+ if (!wait) {
886
+ return;
887
+ }
888
+ plan.reactAt += wait;
889
+ for (const leg of plan.legs) {
890
+ leg.start += wait;
891
+ }
892
+ if (plan.getUp) {
893
+ plan.getUp.at += wait;
894
+ }
895
+ });
896
+ // The applause, every leg timed after the one before.
897
+ header.players.forEach((p, k) => {
898
+ const plan = this.plans[k];
899
+ if (!plan) {
900
+ return;
901
+ }
902
+ let pos = plan.from;
903
+ let free = 0;
904
+ for (const leg of plan.legs) {
905
+ leg.from = pos;
906
+ leg.start = Math.max(leg.start, free);
907
+ leg.dist = Math.hypot(leg.to.x - pos.x, leg.to.y - pos.y);
908
+ leg.arrive = leg.start + leg.dist / leg.speed;
909
+ pos = leg.to;
910
+ free = leg.arrive + 0.4;
911
+ }
912
+ const lost = winner >= 0 && p.team !== winner;
913
+ const to = toStand(pos, lost ? 1.5 : 0);
914
+ const leg = {
915
+ from: pos, to, start: Math.max(free, applause + (lost ? 0.8 : 0) + random() * 0.6),
916
+ speed: lost ? 1.0 : 1.45, gait: 'walk', still: clap, heading: standHeading,
917
+ };
918
+ leg.dist = Math.hypot(to.x - pos.x, to.y - pos.y);
919
+ leg.arrive = leg.start + leg.dist / leg.speed;
920
+ plan.legs.push(leg);
921
+ });
922
+ }
923
+
924
+ // How each player and the ball wind down from the last frame, from the velocities between it
925
+ // and the frame before. A player stops at an even deceleration over his own time, the ball
926
+ // flies, bounces and rolls, kept inside the net or the boards.
927
+ windDown(frame, prev, sps, random) {
928
+ const base = this.base;
929
+ const before = prev && prev.data ? prev.data.subarray(NODES_FLOAT) : null;
930
+ this.wind = [];
931
+ this.windEnd = 0;
932
+ for (let k = 0; k < this.count; k++) {
933
+ const o = k * PLAYER_FLOATS;
934
+ const x = base[o];
935
+ const y = base[o + 1];
936
+ let vx = before ? (x - before[o]) * sps : 0;
937
+ let vy = before ? (y - before[o + 1]) * sps : 0;
938
+ let v = Math.hypot(vx, vy);
939
+ // A jump between the two frames is a cut, not a stride.
940
+ if (!(v < 11)) {
941
+ vx = 0;
942
+ vy = 0;
943
+ v = 0;
944
+ }
945
+ let T = v > 0.35 ? 2 + random() * 2 : 0;
946
+ // A sprinter covers at most eight meters while he pulls up.
947
+ if (v * T / 2 > 8) {
948
+ T = 16 / v;
949
+ }
950
+ const run = v * T / 2;
951
+ const sx = Math.max(-HALF_W - 1, Math.min(HALF_W + 1, x + (v > 0 ? (vx / v) * run : 0)));
952
+ const sy = Math.max(-HALF_H - 1, Math.min(HALF_H + 1, y + (v > 0 ? (vy / v) * run : 0)));
953
+ this.wind.push({ x, y, vx, vy, v, T, sx, sy, run: Math.hypot(sx - x, sy - y) });
954
+ this.windEnd = Math.max(this.windEnd, T);
955
+ }
956
+ this.ballPath = rollBall(frame.data.subarray(8, 11), prev && prev.data ? prev.data.subarray(8, 11) : null, sps);
957
+ }
958
+
959
+ // The ball at time t after the last frame, into out.
960
+ ball(t, out) {
961
+ const path = this.ballPath;
962
+ const f = Math.max(0, Math.min(path.length / 3 - 1, t / BALL_STEP));
963
+ const i = Math.floor(f);
964
+ const j = Math.min(path.length / 3 - 1, i + 1);
965
+ const u = f - i;
966
+ for (let c = 0; c < 3; c++) {
967
+ out[c] = path[i * 3 + c] + (path[j * 3 + c] - path[i * 3 + c]) * u;
968
+ }
969
+ return out;
970
+ }
971
+
972
+ // The clips that take a beaten player of a KayKit model down to the grass and up again, the
973
+ // time he stays down at least and the length of getting up, or null.
974
+ floorClip(k, keeper, random) {
975
+ const has = this.hasClip;
976
+ if (!has) {
977
+ return null;
978
+ }
979
+ const options = [
980
+ { down: 'clip:Sit_Floor_Down>Sit_Floor_Idle', up: 'clip:Sit_Floor_StandUp', length: 1.15, hold: 2.4 },
981
+ { down: 'clip:Lie_Down>Lie_Idle', up: 'clip:Lie_StandUp', length: 2.35, hold: 7.6 },
982
+ ].filter((o) => has(k, o.down) && has(k, o.up));
983
+ if (!options.length) {
984
+ return null;
985
+ }
986
+ const lie = options.find((o) => o.down.startsWith('clip:Lie'));
987
+ if (keeper && lie) {
988
+ return lie;
989
+ }
990
+ const sit = options.find((o) => o.down.startsWith('clip:Sit'));
991
+ // Most sit, a few stay on their feet with their hands on their heads.
992
+ return sit && random() < 0.75 ? sit : null;
993
+ }
994
+
995
+ // A bundle of the winners nearest one another, around their middle, and one or two who run
996
+ // off to the fans of the nearest stand with their arms up.
997
+ bundleWinners(winner, at, on, random) {
998
+ const header = this.header;
999
+ const mates = header.players.map((p, k) => ({ p, k })).filter((m) => m.p.team === winner && !m.p.goalkeeper && on(m.k) && m.k !== this.hero);
1000
+ if (mates.length < 3) {
1001
+ return;
1002
+ }
1003
+ const cx = mates.reduce((a, m) => a + at(m.k).x, 0) / mates.length;
1004
+ const cy = mates.reduce((a, m) => a + at(m.k).y, 0) / mates.length;
1005
+ mates.sort((a, b) => Math.hypot(at(a.k).x - cx, at(a.k).y - cy) - Math.hypot(at(b.k).x - cx, at(b.k).y - cy));
1006
+ const bundle = mates.slice(0, Math.min(6, Math.max(3, Math.floor(mates.length / 2))));
1007
+ const bx = Math.max(-HALF_W + 3, Math.min(HALF_W - 3, bundle.reduce((a, m) => a + at(m.k).x, 0) / bundle.length));
1008
+ const by = Math.max(-HALF_H + 3, Math.min(HALF_H - 3, bundle.reduce((a, m) => a + at(m.k).y, 0) / bundle.length));
1009
+ const r = Math.max(...bundle.map((m) => this.radii[m.k]));
1010
+ const ring = Math.max(0.9, (2 * r + 0.1) / (2 * Math.sin(Math.PI / bundle.length)));
1011
+ const huddle = roleName('huddle') || 'arms_up';
1012
+ bundle.forEach((m, i) => {
1013
+ const a = (i / bundle.length) * Math.PI * 2;
1014
+ const to = { x: bx + Math.cos(a) * ring, y: by + Math.sin(a) * ring };
1015
+ const plan = this.plans[m.k];
1016
+ plan.react = libraryEntry('jump_fist') ? 'jump_fist' : plan.react;
1017
+ plan.legs.push({ to, start: 0.5 + random() * 0.5, speed: 5.2, gait: 'run', still: huddle, heading: headingTo(bx - to.x, by - to.y) });
1018
+ });
1019
+ this.bundle = { x: bx, y: by };
1020
+ mates.slice(bundle.length).slice(0, 2).forEach((m) => {
1021
+ const f = at(m.k);
1022
+ const side = f.y >= 0 ? 1 : -1;
1023
+ const to = { x: f.x, y: side * (HALF_H - 1.2) };
1024
+ if (Math.abs(to.y - f.y) < 22) {
1025
+ this.plans[m.k].legs.push({ to, start: 0.4, speed: 5, gait: 'run', still: 'arms_up', heading: headingTo(0, side) });
1026
+ }
1027
+ });
1028
+ }
1029
+
1030
+ // Handshakes between near opponents on a draw, up to four pairs.
1031
+ shakeHands(at, random, handshake) {
1032
+ const header = this.header;
1033
+ const used = new Set();
1034
+ const pairs = [];
1035
+ for (let a = 0; a < this.count && pairs.length < 4; a++) {
1036
+ if (!this.plans[a] || used.has(a) || header.players[a].team !== 0 || header.players[a].goalkeeper) {
1037
+ continue;
1038
+ }
1039
+ let best = -1;
1040
+ let bestD = 12;
1041
+ for (let b = 0; b < this.count; b++) {
1042
+ if (!this.plans[b] || used.has(b) || header.players[b].team !== 1 || header.players[b].goalkeeper) {
1043
+ continue;
1044
+ }
1045
+ const d = Math.hypot(at(a).x - at(b).x, at(a).y - at(b).y);
1046
+ if (d < bestD) {
1047
+ bestD = d;
1048
+ best = b;
1049
+ }
1050
+ }
1051
+ if (best >= 0) {
1052
+ used.add(a);
1053
+ used.add(best);
1054
+ pairs.push([a, best]);
1055
+ }
1056
+ }
1057
+ for (const [a, b] of pairs) {
1058
+ const pa = at(a);
1059
+ const pb = at(b);
1060
+ const mx = (pa.x + pb.x) / 2;
1061
+ const my = (pa.y + pb.y) / 2;
1062
+ const len = Math.hypot(pb.x - pa.x, pb.y - pa.y) || 1;
1063
+ const ux = (pb.x - pa.x) / len;
1064
+ const uy = (pb.y - pa.y) / len;
1065
+ const gap = (this.radii[a] + this.radii[b]) / 2 + 0.35;
1066
+ const start = 0.9 + random() * 0.6;
1067
+ this.plans[a].legs.push({ to: { x: mx - ux * gap, y: my - uy * gap }, start, speed: 1.5, gait: 'walk', still: handshake, heading: headingTo(ux, uy) });
1068
+ this.plans[b].legs.push({ to: { x: mx + ux * gap, y: my + uy * gap }, start, speed: 1.5, gait: 'walk', still: handshake, heading: headingTo(-ux, -uy) });
1069
+ this.plans[a].react = 'idle';
1070
+ this.plans[b].react = 'idle';
1071
+ }
1072
+ this.pairs = pairs;
1073
+ }
1074
+
1075
+ release(stopClip) {
1076
+ for (const k of this.clipped) {
1077
+ stopClip(k);
1078
+ }
1079
+ this.clipped.clear();
1080
+ }
1081
+
1082
+ state(k, t) {
1083
+ const plan = this.plans[k];
1084
+ if (!plan) {
1085
+ return null;
1086
+ }
1087
+ const w = this.wind[k];
1088
+ if (w && w.T > 0 && t < w.T) {
1089
+ // Slowing evenly, from a run through a jog to a walk and a stand.
1090
+ const covered = w.v * (t - (t * t) / (2 * w.T));
1091
+ const f = w.v * w.T > 0 ? Math.min(1, covered / (w.v * w.T / 2)) : 1;
1092
+ const speed = w.v * (1 - t / w.T);
1093
+ return {
1094
+ x: w.x + (w.sx - w.x) * f,
1095
+ y: w.y + (w.sy - w.y) * f,
1096
+ heading: plan.heading,
1097
+ moving: 'run',
1098
+ moving2: 'walk',
1099
+ gaitMix: 1 - smooth((speed - 1.5) / 1.6),
1100
+ travelled: w.run * f,
1101
+ still: 'idle',
1102
+ mix: smooth((0.7 - speed) / 0.7),
1103
+ still_t: 0,
1104
+ head: plan.head * smooth((t - w.T + 1) / 1),
1105
+ };
1106
+ }
1107
+ let x = plan.from.x;
1108
+ let y = plan.from.y;
1109
+ let still = t >= plan.reactAt ? plan.react : 'idle';
1110
+ let stillT = t - plan.reactAt;
1111
+ if (plan.getUp && t >= plan.getUp.at) {
1112
+ still = plan.getUp.name;
1113
+ stillT = t - plan.getUp.at;
1114
+ }
1115
+ let heading = plan.heading;
1116
+ let moving = null;
1117
+ let travelled = 0;
1118
+ let mix = 1;
1119
+ for (const leg of plan.legs) {
1120
+ if (t < leg.start) {
1121
+ break;
1122
+ }
1123
+ const d = Math.min(leg.dist, (t - leg.start) * leg.speed);
1124
+ const f = leg.dist > 0.01 ? d / leg.dist : 1;
1125
+ x = leg.from.x + (leg.to.x - leg.from.x) * f;
1126
+ y = leg.from.y + (leg.to.y - leg.from.y) * f;
1127
+ const walkHeading = leg.dist > 0.01 ? headingTo(leg.to.x - leg.from.x, leg.to.y - leg.from.y) : heading;
1128
+ if (f < 1) {
1129
+ moving = leg.gait;
1130
+ travelled = d;
1131
+ // Out of the pose before into the walk, and into the pose after near the end.
1132
+ const out = 1 - smooth((t - leg.start) / 0.35);
1133
+ const into = smooth((t - leg.arrive + 0.3) / 0.35);
1134
+ mix = Math.max(out, into);
1135
+ if (into > out) {
1136
+ still = leg.still;
1137
+ stillT = t - leg.arrive;
1138
+ }
1139
+ heading = lerpAngle(heading, walkHeading, smooth((t - leg.start) / 0.4));
1140
+ } else {
1141
+ moving = null;
1142
+ still = leg.still;
1143
+ stillT = t - leg.arrive;
1144
+ const settle = smooth((t - leg.arrive + 0.2) / 0.5);
1145
+ heading = lerpAngle(walkHeading, leg.heading == null ? walkHeading : leg.heading, settle);
1146
+ mix = 1;
1147
+ }
1148
+ }
1149
+ return { x, y, heading, moving, travelled, still, mix, still_t: stillT, head: plan.head, pin: true };
1150
+ }
1151
+
1152
+ apply(pose, t, weight = 1, playClip = null) {
1153
+ writeGroup(this, pose, t, weight, playClip);
1154
+ if (pose.ball && this.ballPath) {
1155
+ const b = this.ball(t, ballScratch);
1156
+ const bw = Math.min(1, weight);
1157
+ pose.ball[0] += (b[0] - pose.ball[0]) * bw;
1158
+ pose.ball[1] += (b[1] - pose.ball[1]) * bw;
1159
+ pose.ball[2] += (b[2] - pose.ball[2]) * bw;
1160
+ // The ball spins as it rolls, from its own motion.
1161
+ pose.ballKnownRotation = false;
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ const BALL_STEP = 1 / 50;
1167
+ const BALL_RADIUS_M = 0.11;
1168
+
1169
+ // The flight of the ball after the last frame, from its last velocity, as positions every
1170
+ // BALL_STEP seconds for eight seconds. It falls, bounces lower each time, rolls with the drag of
1171
+ // the grass, and stops in the netting of a goal or bounces back off the boards.
1172
+ function rollBall(b, a, sps) {
1173
+ let x = b[0];
1174
+ let y = b[1];
1175
+ let z = Math.max(BALL_RADIUS_M, b[2]);
1176
+ let vx = a ? (b[0] - a[0]) * sps : 0;
1177
+ let vy = a ? (b[1] - a[1]) * sps : 0;
1178
+ let vz = a ? (b[2] - a[2]) * sps : 0;
1179
+ if (!(Math.hypot(vx, vy, vz) < 45)) {
1180
+ vx = 0;
1181
+ vy = 0;
1182
+ vz = 0;
1183
+ }
1184
+ const steps = Math.round(8 / BALL_STEP);
1185
+ const out = new Float32Array((steps + 1) * 3);
1186
+ const netX = HALF_W + 1.6;
1187
+ const boardX = HALF_W + 4;
1188
+ const boardY = HALF_H + 3.5;
1189
+ for (let i = 0; i <= steps; i++) {
1190
+ out[i * 3] = x;
1191
+ out[i * 3 + 1] = y;
1192
+ out[i * 3 + 2] = z;
1193
+ const dt = BALL_STEP;
1194
+ const ground = z <= BALL_RADIUS_M + 1e-3 && Math.abs(vz) < 0.5;
1195
+ if (ground) {
1196
+ z = BALL_RADIUS_M;
1197
+ vz = 0;
1198
+ // Rolling friction on grass, about a third of the speed lost each second.
1199
+ const drag = Math.exp(-0.45 * dt);
1200
+ vx = vx * drag - Math.sign(vx) * Math.min(Math.abs(vx), 0.35 * dt);
1201
+ vy = vy * drag - Math.sign(vy) * Math.min(Math.abs(vy), 0.35 * dt);
1202
+ } else {
1203
+ vz -= 9.81 * dt;
1204
+ vx *= Math.exp(-0.05 * dt);
1205
+ vy *= Math.exp(-0.05 * dt);
1206
+ }
1207
+ x += vx * dt;
1208
+ y += vy * dt;
1209
+ z += vz * dt;
1210
+ if (z < BALL_RADIUS_M) {
1211
+ z = BALL_RADIUS_M;
1212
+ vz = vz < -0.5 ? -vz * 0.5 : 0;
1213
+ vx *= 0.85;
1214
+ vy *= 0.85;
1215
+ }
1216
+ // Into the goal the netting holds the ball, wide of it the boards send it back softly.
1217
+ if (Math.abs(x) > HALF_W) {
1218
+ const mouth = Math.abs(y) < 3.66 && z < 2.44;
1219
+ if (mouth && Math.abs(x) > netX) {
1220
+ x = Math.sign(x) * netX;
1221
+ vx = 0;
1222
+ vy *= 0.3;
1223
+ } else if (!mouth && Math.abs(x) > boardX) {
1224
+ x = Math.sign(x) * boardX;
1225
+ vx = -vx * 0.25;
1226
+ }
1227
+ }
1228
+ if (Math.abs(y) > boardY) {
1229
+ y = Math.sign(y) * boardY;
1230
+ vy = -vy * 0.25;
1231
+ }
1232
+ if (z > 2.44 && Math.abs(x) > HALF_W && Math.abs(y) < 3.66 && Math.abs(x) < netX) {
1233
+ // Over the bar onto the roof of the net, it drops behind.
1234
+ vz = Math.min(vz, 0);
1235
+ }
1236
+ }
1237
+ return out;
1238
+ }