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,530 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The Field page, for grass, lines, nets, boards, crowd, time of day and weather, with the match
5
+ // view borrowed as a live preview. The preview flies the free camera, and editing a control
6
+ // flies it to the thing being changed, from where the user keeps free control.
7
+
8
+ import * as THREE from 'three';
9
+ import * as Cameras from '../world/cameras.js';
10
+ import {
11
+ BOARDS, CROWDS, MAX_BOARDS, MOWING, PRESETS, TIMES, TIME_HOURS, WEATHERS, cleanField, exportField, importField,
12
+ presetField, saveField,
13
+ } from '../world/field.js';
14
+ import { h } from '../teams/library.js';
15
+ import { menuHeading, menuItem } from '../core/menu.js';
16
+ import { Painter } from '../teams/painter.js';
17
+ import { ARENAS, ARENA_LABELS, ARENA_NOTES, arenaThumbnail } from '../world/arenas.js';
18
+ import { formatHour } from '../world/sky.js';
19
+ import { formatNumber, t, tn, tOr } from '../core/i18n.js';
20
+ import { icon, symbolOnly } from '../core/icons.js';
21
+
22
+ // Board pictures are read whole before they are scaled down, so a very large file is refused first.
23
+ const BOARD_FILE_BYTES = 32 * 1024 * 1024;
24
+
25
+ const sameList = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
26
+ // A number as String writes it, with the decimal separator of the language.
27
+ const decimal = (v) => {
28
+ const text = String(v);
29
+ return /^-?\d+\.\d+$/.test(text) ? text.replace('.', formatNumber(0.5).charAt(1)) : text;
30
+ };
31
+
32
+ const LABELS = {
33
+ stripes: t('fieldpage.mowing.stripes'), checkers: t('fieldpage.mowing.checkers'), circles: t('fieldpage.mowing.circles'),
34
+ none: t('fieldpage.mowing.none'), sponsors: t('fieldpage.boards.sponsors'), shipped: t('fieldpage.boards.shipped'),
35
+ custom: t('fieldpage.boards.custom'), grf: t('fieldpage.crowd.grf'), dawn: t('fieldpage.time.dawn'),
36
+ day: t('fieldpage.time.day'), dusk: t('fieldpage.time.dusk'), night: t('fieldpage.time.night'),
37
+ };
38
+ const CROWD_LABELS = { teams: t('fieldpage.crowd.teams'), custom: t('fieldpage.crowd.custom'), grf: t('fieldpage.crowd.grf') };
39
+ export const WEATHER_LABELS = {
40
+ clear: t('fieldpage.weather.clear'), overcast: t('fieldpage.weather.overcast'), rain: t('fieldpage.weather.rain'),
41
+ storm: t('fieldpage.weather.storm'), snow: t('fieldpage.weather.snow'), fog: t('fieldpage.weather.fog'),
42
+ };
43
+ // The name of a preset in the language of the page, the English name stays its key.
44
+ // The choices that show their symbols alone, the others show their names.
45
+ const CHOICE_ICONS = {
46
+ stripes: 'stripes', checkers: 'checkers', circles: 'circles', none: 'plain',
47
+ dawn: 'dawn', day: 'sun', dusk: 'dusk', night: 'moon',
48
+ clear: 'sun', overcast: 'cloud', rain: 'rain', storm: 'storm', snow: 'snow', fog: 'fog',
49
+ };
50
+ const presetName = (name) => tOr(`fieldpage.preset.${name.toLowerCase().replace(/[^a-z0-9]+/g, '_')}`, name);
51
+
52
+ // Where the preview looks for each part of the field, used when cameras.js has no table of its
53
+ // own. Positions and targets are in three's frame, the main stand on positive z.
54
+ const VIEWS = {
55
+ overview: { pos: [0, 30, 53], target: [0, 0, -4], fov: 55 },
56
+ grass: { pos: [-28, 2.2, 22], target: [8, 0, -4], fov: 60 },
57
+ lines: { pos: [-61, 4.5, 40], target: [10, 0, 34], fov: 55 },
58
+ nets: { pos: [64, 3.4, 7], target: [55, 1.1, 0], fov: 55 },
59
+ boards: { pos: [-18, 1.8, 33], target: [30, 0.6, 41.5], fov: 50 },
60
+ crowd: { pos: [0, 8, 12], target: [0, 12, -70], fov: 55 },
61
+ sky: { pos: [-42, 18, 49], target: [10, 16, -40], fov: 65 },
62
+ arena: { pos: [-48, 46, 34], target: [20, 4, -24], fov: 62 },
63
+ };
64
+ // Each part of the form and the view it flies to.
65
+ const FOCUS = {
66
+ grass: 'grass', mowing: 'grass', lines: 'lines', nets: 'nets', boards: 'boards', crowd: 'crowd',
67
+ time: 'sky', weather: 'sky', arena: 'arena', presets: 'overview',
68
+ };
69
+ const FLY_SECONDS = 1.4;
70
+
71
+ const vec = (v) => {
72
+ if (!v) {
73
+ return null;
74
+ }
75
+ if (v.isVector3) {
76
+ return v.clone();
77
+ }
78
+ if (Array.isArray(v)) {
79
+ return new THREE.Vector3(v[0], v[1], v[2]);
80
+ }
81
+ return new THREE.Vector3(v.x, v.y, v.z);
82
+ };
83
+
84
+ export class FieldPage {
85
+ constructor(app) {
86
+ this.app = app;
87
+ this.root = document.getElementById('page-field');
88
+ this.painter = new Painter();
89
+ this.focused = null;
90
+ this.tween = null;
91
+ this.bindQuick();
92
+ }
93
+
94
+ get field() {
95
+ return this.app.field;
96
+ }
97
+
98
+ change(patch, rerender = false) {
99
+ const next = cleanField({ ...this.field, ...patch });
100
+ // The match view reloads the board pictures when the list changes, so an unchanged list
101
+ // keeps its old array.
102
+ if (sameList(next.customBoards, this.field.customBoards)) {
103
+ next.customBoards = this.field.customBoards;
104
+ }
105
+ this.app.applyField(next);
106
+ const err = saveField(next);
107
+ if (err) {
108
+ this.app.hud.toast(err, 'error', 8000);
109
+ }
110
+ this.syncQuick();
111
+ if (rerender) {
112
+ const selector = 'button, input:not(.color-value), select';
113
+ const active = document.activeElement;
114
+ const textColor = active.matches('.color-value');
115
+ const controls = [...this.root.querySelectorAll(selector)];
116
+ const index = controls.indexOf(textColor ? active.previousElementSibling : active);
117
+ this.render();
118
+ if (index >= 0) {
119
+ const next = this.root.querySelectorAll(selector)[index];
120
+ next?.focus();
121
+ if (textColor) {
122
+ queueMicrotask(() => next?.nextElementSibling?.focus());
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ // While a slider moves only the light follows, the whole field is stored when it stops.
129
+ preview(patch) {
130
+ if (this.app.environment) {
131
+ this.app.environment.setField({ ...this.field, ...patch });
132
+ }
133
+ }
134
+
135
+ render() {
136
+ const f = this.field;
137
+ const chips = (list, current, key, labels = LABELS) => h('div', { class: 'chips' }, list.map((v) => h('button', {
138
+ type: 'button', class: `chip-btn${v === current ? ' active' : ''}`, 'aria-pressed': v === current ? 'true' : 'false',
139
+ ...(CHOICE_ICONS[v] ? symbolOnly(labels[v] || v) : {}),
140
+ onclick: () => this.change({ [key]: v }, true),
141
+ }, CHOICE_ICONS[v] ? icon(CHOICE_ICONS[v]) : labels[v] || v)));
142
+ const color = (label, key) => {
143
+ const input = h('input', { type: 'color', value: f[key] });
144
+ input.addEventListener('input', () => this.change({ [key]: input.value }));
145
+ return h('label', { class: 'swatch' }, input, h('span', {}, label));
146
+ };
147
+ // A slider that shows its value, previews while it moves and stores when it stops.
148
+ const slider = (key, min, max, step, show, rerender = false) => {
149
+ const input = h('input', { type: 'range', min: String(min), max: String(max), step: String(step), value: String(f[key]) });
150
+ const label = h('span', { class: 'value' }, show(f[key]));
151
+ input.addEventListener('input', () => {
152
+ label.textContent = show(Number(input.value));
153
+ this.preview({ [key]: Number(input.value) });
154
+ });
155
+ input.addEventListener('change', () => this.change({ [key]: Number(input.value) }, rerender));
156
+ return [input, label];
157
+ };
158
+ const percent = (v) => t('fieldpage.value.percent', { value: Math.round(v * 100) });
159
+ const section = (focus, ...children) => h('section', { class: 'field-section', 'data-focus': focus }, ...children);
160
+ const [width, widthLabel] = slider('stripeWidth', 2, 16, 0.5, (v) => t('fieldpage.value.meters', { value: decimal(v) }));
161
+ const density = h('input', { type: 'range', min: '0', max: '1', step: '0.05', value: String(f.crowdDensity) });
162
+ density.addEventListener('change', () => this.change({ crowdDensity: Number(density.value) }, true));
163
+ const crowdColors = h('div', { class: 'swatches small' }, f.crowdColors.map((c, i) => {
164
+ const input = h('input', { type: 'color', value: c });
165
+ input.addEventListener('change', () => {
166
+ const list = f.crowdColors.slice();
167
+ list[i] = input.value;
168
+ this.change({ crowdColors: list, crowd: 'custom' }, true);
169
+ });
170
+ return h('label', { class: 'swatch' }, input);
171
+ }), f.crowdColors.length < 12 ? h('button', {
172
+ type: 'button', class: 'btn small', ...symbolOnly(t('fieldpage.crowd.addColor')), onclick: () => this.change({ crowdColors: [...f.crowdColors, '#888888'], crowd: 'custom' }, true),
173
+ }, icon('plus')) : null, f.crowdColors.length > 1 ? h('button', {
174
+ type: 'button', class: 'btn small ghost', ...symbolOnly(t('fieldpage.crowd.removeLast')), onclick: () => this.change({ crowdColors: f.crowdColors.slice(0, -1), crowd: 'custom' }, true),
175
+ }, icon('minus')) : null);
176
+ const presets = h('select', { 'aria-label': t('fieldpage.presets') }, h('option', { value: '' }, t('fieldpage.presets.choose')),
177
+ Object.keys(PRESETS).map((n) => h('option', { value: n }, presetName(n))));
178
+ presets.addEventListener('change', () => {
179
+ if (presets.value) {
180
+ const p = presetField(presets.value);
181
+ // Presets keep the boards you drew, and the arena unless they name one.
182
+ p.customBoards = f.customBoards;
183
+ if (!('arena' in PRESETS[presets.value])) {
184
+ p.arena = f.arena;
185
+ }
186
+ this.change(p, true);
187
+ }
188
+ });
189
+ const importInput = h('input', { type: 'file', accept: 'application/json,.json', hidden: true });
190
+ importInput.addEventListener('change', async () => {
191
+ const file = importInput.files[0];
192
+ importInput.value = '';
193
+ if (!file) {
194
+ return;
195
+ }
196
+ try {
197
+ const { field, notes } = await importField(file, this.field);
198
+ this.change(field, true);
199
+ this.app.hud.toast([t('fieldpage.imported'), ...notes].join(' '), notes.length ? 'info' : undefined, notes.length ? 9000 : undefined);
200
+ } catch (err) {
201
+ this.app.hud.toast(err.message, 'error', 9000);
202
+ }
203
+ });
204
+ const boardFile = h('input', { type: 'file', accept: 'image/*', hidden: true });
205
+ boardFile.addEventListener('change', async () => {
206
+ const file = boardFile.files[0];
207
+ boardFile.value = '';
208
+ if (!file) {
209
+ return;
210
+ }
211
+ if (file.size > BOARD_FILE_BYTES) {
212
+ this.app.hud.toast(t('fieldpage.board.tooLarge', { size: Math.ceil(file.size / 1024 / 1024), limit: BOARD_FILE_BYTES / 1024 / 1024 }), 'error');
213
+ return;
214
+ }
215
+ try {
216
+ this.addBoard(await this.boardDataUrl(file));
217
+ } catch (err) {
218
+ this.app.hud.toast(t('fieldpage.board.unreadable', { reason: err.message || err }), 'error');
219
+ }
220
+ });
221
+ const full = f.customBoards.length >= MAX_BOARDS;
222
+ const boards = h('div', { class: 'board-list' },
223
+ f.customBoards.map((url, i) => h('div', { class: 'board-item' },
224
+ h('img', { src: url, alt: t('fieldpage.board.alt', { number: i + 1 }) }),
225
+ h('button', {
226
+ type: 'button', class: 'btn small ghost danger', ...symbolOnly(t('fieldpage.board.remove')),
227
+ onclick: () => this.change({ customBoards: f.customBoards.filter((_, k) => k !== i) }, true),
228
+ }, icon('close')))),
229
+ h('div', { class: 'row' },
230
+ h('button', { type: 'button', class: 'btn small', disabled: full, ...symbolOnly(t('fieldpage.board.upload')), onclick: () => boardFile.click() }, icon('image')),
231
+ h('button', { type: 'button', class: 'btn small', disabled: full, ...symbolOnly(t('fieldpage.board.draw')), onclick: () => this.drawBoard() }, icon('brush')),
232
+ boardFile),
233
+ full ? h('p', { class: 'muted small' }, t('fieldpage.board.limit', { count: MAX_BOARDS })) : null);
234
+ const [hour, hourLabel] = slider('time', 0, 24, 0.25, formatHour);
235
+ const named = TIMES.find((time) => Math.abs(TIME_HOURS[time] - f.time) < 0.01);
236
+ const timeChips = h('div', { class: 'chips' }, TIMES.map((time) => h('button', {
237
+ type: 'button', class: `chip-btn${time === named ? ' active' : ''}`, 'aria-pressed': time === named ? 'true' : 'false',
238
+ ...symbolOnly(LABELS[time], t('symbol.tip', { name: LABELS[time], text: formatHour(TIME_HOURS[time]) })), onclick: () => this.change({ time: TIME_HOURS[time] }, true),
239
+ }, icon(CHOICE_ICONS[time]))));
240
+ const [amount, amountLabel] = slider('weatherAmount', 0.1, 1, 0.05, percent);
241
+ const [wind, windLabel] = slider('wind', 0, 1, 0.05, (v) => (v < 0.05 ? t('fieldpage.wind.calm') : percent(v)));
242
+ const arenas = h('div', { class: 'arena-list' }, ARENAS.map((name) => h('button', {
243
+ type: 'button', class: `arena-card${name === f.arena ? ' active' : ''}`, 'aria-pressed': name === f.arena ? 'true' : 'false',
244
+ title: ARENA_NOTES[name], onclick: () => this.change({ arena: name }, true),
245
+ }, h('img', { src: this.thumb(name), alt: '', width: '160', height: '90' }), h('span', {}, ARENA_LABELS[name]))));
246
+ const form = h('div', { class: 'field-form' },
247
+ section('presets', h('div', { class: 'row' }, presets,
248
+ h('span', { class: 'spacer' }),
249
+ h('button', { type: 'button', class: 'btn', ...symbolOnly(t('fieldpage.export')), onclick: () => exportField(this.field) }, icon('download')),
250
+ h('button', { type: 'button', class: 'btn', ...symbolOnly(t('fieldpage.import')), onclick: () => importInput.click() }, icon('upload')),
251
+ h('button', {
252
+ type: 'button', class: 'btn ghost', ...symbolOnly(t('fieldpage.reset')),
253
+ onclick: () => {
254
+ if (confirm(t('fieldpage.reset.confirm'))) {
255
+ this.change({ ...presetField('Summer afternoon'), arena: 'stadium' }, true);
256
+ }
257
+ },
258
+ }, icon('reset')),
259
+ importInput)),
260
+ section('arena', h('h2', {}, t('fieldpage.arena')),
261
+ arenas,
262
+ h('p', { class: 'muted small' }, t('fieldpage.arena.note', { note: ARENA_NOTES[f.arena] }))),
263
+ section('grass', h('h2', {}, t('fieldpage.grass')),
264
+ h('div', { class: 'swatches' }, color(t('fieldpage.grass.color'), 'grass'),
265
+ h('span', { 'data-focus': 'lines' }, color(t('fieldpage.lines'), 'lines')),
266
+ h('span', { 'data-focus': 'nets' }, color(t('fieldpage.nets'), 'net'))),
267
+ h('div', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.mowing')), chips(MOWING, f.mowing, 'mowing')),
268
+ h('label', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.bandWidth')), width, widthLabel)),
269
+ section('time', h('h2', {}, t('fieldpage.time')),
270
+ h('label', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.hour')), hour, hourLabel),
271
+ timeChips),
272
+ section('weather', h('h2', {}, t('fieldpage.weather')),
273
+ chips(WEATHERS, f.weather, 'weather', WEATHER_LABELS),
274
+ f.weather !== 'clear' ? h('label', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.weather.strength')), amount, amountLabel) : null,
275
+ h('label', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.wind')), wind, windLabel),
276
+ f.weather === 'snow' ? h('p', { class: 'muted small' }, tn('fieldpage.weather.snowNote')) : null),
277
+ section('boards', h('h2', {}, t('fieldpage.boards')),
278
+ chips(BOARDS, f.boards, 'boards'),
279
+ f.boards === 'custom' ? boards : h('p', { class: 'muted small' }, f.boards === 'sponsors'
280
+ ? t('fieldpage.boards.sponsorsNote')
281
+ : t('fieldpage.boards.shippedNote'))),
282
+ section('crowd', h('h2', {}, t('fieldpage.crowd')),
283
+ chips(CROWDS, f.crowd, 'crowd', CROWD_LABELS),
284
+ f.crowd !== 'grf' ? h('div', {},
285
+ h('label', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.crowd.density')), density),
286
+ f.crowd === 'custom' ? h('div', { class: 'field-row' }, h('span', { class: 'field-label' }, t('fieldpage.crowd.shirts')), crowdColors)
287
+ : h('p', { class: 'muted small' }, t('fieldpage.crowd.teamsNote'))) : null),
288
+ h('p', { class: 'muted small' }, t('fieldpage.exportNote')),
289
+ h('p', { class: 'muted small' }, tn('fieldpage.keys')));
290
+ // Touching a control flies the preview to what it changes.
291
+ const focusFrom = (e) => {
292
+ const el = e.target && e.target.closest && e.target.closest('[data-focus]');
293
+ if (el) {
294
+ this.focus(FOCUS[el.dataset.focus] || 'overview');
295
+ }
296
+ };
297
+ form.addEventListener('pointerdown', focusFrom);
298
+ form.addEventListener('focusin', focusFrom);
299
+ form.addEventListener('input', focusFrom);
300
+ const preview = h('div', { class: 'field-preview', id: 'field-preview' });
301
+ // A choice redraws the form, which keeps its place in the list.
302
+ const old = this.root.querySelector('.field-form');
303
+ const scroll = old ? old.scrollTop : 0;
304
+ this.root.replaceChildren(h('div', { class: 'field-layout' }, form, preview));
305
+ form.scrollTop = scroll;
306
+ if (!this.root.hidden) {
307
+ this.app.view.attach(preview);
308
+ }
309
+ }
310
+
311
+ // Thumbnails are painted once.
312
+ thumb(name) {
313
+ this.thumbs = this.thumbs || {};
314
+ if (!this.thumbs[name]) {
315
+ this.thumbs[name] = arenaThumbnail(name);
316
+ }
317
+ return this.thumbs[name];
318
+ }
319
+
320
+ // The quick controls in the View menu of the Watch page, the hour and the weather, built
321
+ // into the container the menu keeps for them.
322
+ bindQuick() {
323
+ const host = document.getElementById('view-environment');
324
+ if (!host) {
325
+ return;
326
+ }
327
+ const hour = h('input', {
328
+ id: 'env-hour', type: 'range', min: '0', max: '24', step: '0.25', 'aria-label': t('fieldpage.time'),
329
+ });
330
+ const value = h('span', { id: 'env-hour-value', class: 'env-hour-value' });
331
+ hour.addEventListener('input', () => {
332
+ value.textContent = formatHour(Number(hour.value));
333
+ this.preview({ time: Number(hour.value) });
334
+ });
335
+ hour.addEventListener('change', () => this.change({ time: Number(hour.value) }, !this.root.hidden));
336
+ this.quickWeather = WEATHERS.map((w) => {
337
+ const item = menuItem(WEATHER_LABELS[w] || w, {
338
+ checked: false, keepOpen: true, onSelect: () => this.change({ weather: w }, !this.root.hidden),
339
+ });
340
+ item.dataset.weather = w;
341
+ return item;
342
+ });
343
+ this.quickArena = ARENAS.map((name) => {
344
+ const item = menuItem(ARENA_LABELS[name], {
345
+ checked: false, keepOpen: true, onSelect: () => this.change({ arena: name }, !this.root.hidden),
346
+ });
347
+ item.dataset.arena = name;
348
+ return item;
349
+ });
350
+ host.replaceChildren(
351
+ h('label', { class: 'env-hour-row', role: 'presentation' }, h('span', { class: 'env-hour-label' }, t('fieldpage.hour')), hour, value),
352
+ ...this.quickWeather,
353
+ menuHeading(t('fieldpage.arena')),
354
+ ...this.quickArena);
355
+ this.syncQuick();
356
+ }
357
+
358
+ syncQuick() {
359
+ const f = this.field;
360
+ const hour = document.getElementById('env-hour');
361
+ if (!f || !hour) {
362
+ return;
363
+ }
364
+ if (document.activeElement !== hour) {
365
+ hour.value = String(f.time);
366
+ }
367
+ document.getElementById('env-hour-value').textContent = formatHour(f.time);
368
+ for (const item of this.quickWeather || []) {
369
+ item.setAttribute('aria-checked', item.dataset.weather === f.weather ? 'true' : 'false');
370
+ }
371
+ for (const item of this.quickArena || []) {
372
+ item.setAttribute('aria-checked', item.dataset.arena === f.arena ? 'true' : 'false');
373
+ }
374
+ }
375
+
376
+ // Flies the preview to a part of the field, once per part, so moving the camera by hand and
377
+ // then changing the same thing again leaves the camera where the user put it.
378
+ focus(name, snap = false) {
379
+ if (this.root.hidden || (name === this.focused && !snap)) {
380
+ return;
381
+ }
382
+ this.focused = name;
383
+ const table = Cameras.FIELD_VIEWS || VIEWS;
384
+ const view = table[name] || VIEWS[name] || table.overview || VIEWS.overview;
385
+ const pos = vec(view.pos || view.position);
386
+ const target = vec(view.target);
387
+ if (!pos || !target) {
388
+ return;
389
+ }
390
+ const fov = view.fov || 50;
391
+ const rig = this.app.cameras;
392
+ if (typeof rig.flyTo === 'function') {
393
+ rig.flyTo(pos, target, fov, snap ? 0 : (view.seconds || FLY_SECONDS));
394
+ return;
395
+ }
396
+ this.fly(pos, target, snap ? 0 : FLY_SECONDS);
397
+ }
398
+
399
+ // The same flight on the free camera, when the rig has no flyTo of its own.
400
+ fly(pos, target, seconds) {
401
+ const rig = this.app.cameras;
402
+ if (rig.mode !== 'free') {
403
+ rig.setMode('free');
404
+ }
405
+ const cam = rig.camera;
406
+ const d = new THREE.Vector3().subVectors(target, pos).normalize();
407
+ const yaw = Math.atan2(-d.x, -d.z);
408
+ const pitch = Math.asin(THREE.MathUtils.clamp(d.y, -1, 1));
409
+ const from = { pos: cam.position.clone(), yaw: rig.yaw, pitch: rig.pitch };
410
+ let dyaw = yaw - from.yaw;
411
+ dyaw = Math.atan2(Math.sin(dyaw), Math.cos(dyaw));
412
+ const token = {};
413
+ this.tween = token;
414
+ const start = performance.now();
415
+ const step = (now) => {
416
+ if (this.tween !== token || this.root.hidden || rig.mode !== 'free') {
417
+ return;
418
+ }
419
+ const t = seconds > 0 ? Math.min(1, (now - start) / (seconds * 1000)) : 1;
420
+ const e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
421
+ cam.position.lerpVectors(from.pos, pos, e);
422
+ rig.yaw = from.yaw + dyaw * e;
423
+ rig.pitch = from.pitch + (pitch - from.pitch) * e;
424
+ if (t < 1) {
425
+ requestAnimationFrame(step);
426
+ } else {
427
+ this.tween = null;
428
+ }
429
+ };
430
+ step(start);
431
+ }
432
+
433
+ // Any key or drag on the preview hands the camera back to the user.
434
+ stopFlight() {
435
+ this.tween = null;
436
+ const rig = this.app.cameras;
437
+ if (typeof rig.stopFlight === 'function') {
438
+ rig.stopFlight();
439
+ }
440
+ }
441
+
442
+ // Says so when the list of boards is full.
443
+ boardsFull() {
444
+ if (this.field.customBoards.length < MAX_BOARDS) {
445
+ return false;
446
+ }
447
+ this.app.hud.toast(t('fieldpage.board.full', { count: MAX_BOARDS }), 'error');
448
+ return true;
449
+ }
450
+
451
+ addBoard(url) {
452
+ if (this.boardsFull()) {
453
+ return false;
454
+ }
455
+ this.change({ customBoards: [...this.field.customBoards, url], boards: 'custom' }, true);
456
+ return true;
457
+ }
458
+
459
+ async boardDataUrl(file) {
460
+ // Boards are stored small, as 1024 by 128 PNGs, so the settings fit in the browser.
461
+ const bitmap = await createImageBitmap(file);
462
+ const canvas = document.createElement('canvas');
463
+ canvas.width = 1024;
464
+ canvas.height = 128;
465
+ const ctx = canvas.getContext('2d');
466
+ const scale = Math.max(1024 / bitmap.width, 128 / bitmap.height);
467
+ const w = bitmap.width * scale;
468
+ const hgt = bitmap.height * scale;
469
+ ctx.drawImage(bitmap, (1024 - w) / 2, (128 - hgt) / 2, w, hgt);
470
+ bitmap.close();
471
+ return canvas.toDataURL('image/png');
472
+ }
473
+
474
+ drawBoard() {
475
+ if (this.boardsFull()) {
476
+ return;
477
+ }
478
+ this.painter.open({
479
+ title: t('fieldpage.painter.title'),
480
+ width: 1024,
481
+ height: 128,
482
+ note: t('fieldpage.painter.note'),
483
+ onSave: async (png) => {
484
+ const url = await new Promise((resolve, reject) => {
485
+ const r = new FileReader();
486
+ r.onload = () => resolve(r.result);
487
+ r.onerror = () => reject(r.error || new Error(t('fieldpage.painter.unreadable')));
488
+ r.readAsDataURL(png);
489
+ });
490
+ if (!this.addBoard(url)) {
491
+ throw new Error(t('fieldpage.painter.full', { count: MAX_BOARDS }));
492
+ }
493
+ },
494
+ }).catch((err) => this.app.hud.toast(t('fieldpage.painter.failed', { reason: err.message || err }), 'error'));
495
+ }
496
+
497
+ // The preview borrows the match view's canvas while the page is showing, with the free
498
+ // camera on a wide view that shows the sky and the floodlight towers.
499
+ show() {
500
+ this.camera = this.app.cameras.mode;
501
+ this.app.cameras.setMode('free');
502
+ this.render();
503
+ this.focused = null;
504
+ // The sky view shows the towers and the sky, so the time of day reads at once.
505
+ this.focus('sky', true);
506
+ if (!this.stopper) {
507
+ this.stopper = () => {
508
+ if (!this.root.hidden) {
509
+ this.stopFlight();
510
+ }
511
+ };
512
+ this.app.view.renderer.domElement.addEventListener('pointerdown', this.stopper);
513
+ window.addEventListener('keydown', (e) => {
514
+ if (!e.target.closest('input, textarea, select, button, [contenteditable]') &&
515
+ e.key.length === 1 && 'wasdqec'.includes(e.key.toLowerCase())) {
516
+ this.stopper();
517
+ }
518
+ });
519
+ }
520
+ }
521
+
522
+ hide() {
523
+ this.tween = null;
524
+ this.app.view.attach(document.getElementById('stage'));
525
+ if (this.camera) {
526
+ this.app.cameras.setMode(this.camera);
527
+ this.camera = null;
528
+ }
529
+ }
530
+ }