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,376 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The painted crowd. Every strip of the atlas is one row of seats seen from the pitch, a person
5
+ // to each seat, and a second atlas holds the same people on their feet for a goal. Shirts and
6
+ // scarves are painted grey and marked in a mask, so the stands take any colors without a repaint.
7
+
8
+ import * as THREE from 'three';
9
+
10
+ export const ATLAS = {
11
+ // Only every fourth column holds a tall pose. Heights in meters above the tread that the calm
12
+ // people of the other columns, the calm people of any column and GRF's people stay below.
13
+ tallEvery: 4,
14
+ seatedTop: 1.48,
15
+ calmTop: 1.88,
16
+ grfTop: 1.4,
17
+ // Meters of one seat, of the painted part of a strip and of the whole strip with its margin.
18
+ seat: 0.5,
19
+ card: 112 / 48,
20
+ strip: 128 / 48,
21
+ seats: 64,
22
+ strips: 8,
23
+ // Pixels per meter at full scale.
24
+ pxm: 48,
25
+ // The grey of a shirt at its brightest fold, the shader divides by it to keep the shading.
26
+ shirtGrey: 214,
27
+ };
28
+
29
+ const SKINS = ['#f4d0b4', '#eabd98', '#dba47e', '#c68c62', '#a8714a', '#8a5a3a', '#6a4129', '#4e2f1d'];
30
+ const HAIR = ['#141010', '#231812', '#3a2618', '#5a3c22', '#7c5530', '#a8834e', '#d2b47a', '#8c8c8c',
31
+ '#c8c8c8', '#6e2c14'];
32
+ const LEGS = ['#1e2633', '#2b3a52', '#262626', '#3b3f45', '#4a5a78', '#2a2f26'];
33
+ const BACKGROUND = [28, 33, 41];
34
+
35
+ function rng(seed) {
36
+ let s = seed >>> 0;
37
+ return () => {
38
+ s = (s + 0x6d2b79f5) >>> 0;
39
+ let t = s;
40
+ t = Math.imul(t ^ (t >>> 15), t | 1);
41
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
42
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
43
+ };
44
+ }
45
+
46
+ function pick(rand, list) {
47
+ return list[Math.floor(rand() * list.length)];
48
+ }
49
+
50
+ function weighted(rand, table) {
51
+ let r = rand() * table.reduce((a, [, w]) => a + w, 0);
52
+ for (const [name, w] of table) {
53
+ r -= w;
54
+ if (r <= 0) {
55
+ return name;
56
+ }
57
+ }
58
+ return table[0][0];
59
+ }
60
+
61
+ const CALM = [['sit', 58], ['lean', 10], ['clap', 9], ['crossed', 11], ['phone', 5]];
62
+ // Poses that rise above a seated head go only in every fourth column, so the shader skips the
63
+ // air above the heads of the other three without reading the atlas.
64
+ const CALM_TALL = [['stand', 50], ['armUp', 20], ['scarfUp', 22], ['sit', 8]];
65
+ const CHEER = [['armsUp', 42], ['fist', 18], ['scarfUp', 14], ['clapStand', 14], ['sitClap', 12]];
66
+
67
+ // One person for every seat of every strip, the same in both atlases.
68
+ function people() {
69
+ const rand = rng(20261106);
70
+ const out = [];
71
+ for (let k = 0; k < ATLAS.strips * ATLAS.seats; k++) {
72
+ const tall = (k % ATLAS.seats) % ATLAS.tallEvery === 0;
73
+ const style = weighted(rand, [['short', 40], ['long', 14], ['bald', 8], ['cap', 14], ['beanie', 10],
74
+ ['curly', 14]]);
75
+ out.push({
76
+ skin: pick(rand, SKINS),
77
+ hair: rand() < 0.12 ? pick(rand, HAIR.slice(7, 9)) : pick(rand, HAIR),
78
+ style,
79
+ legs: pick(rand, LEGS),
80
+ sleeves: rand() < 0.45,
81
+ scarf: rand() < 0.32,
82
+ build: 0.9 + rand() * 0.22,
83
+ tall: 0.94 + rand() * 0.12,
84
+ dx: (rand() - 0.5) * 0.06,
85
+ calm: weighted(rand, tall ? CALM_TALL : CALM),
86
+ cheer: weighted(rand, CHEER),
87
+ lean: (rand() - 0.5) * 0.05,
88
+ side: rand() < 0.5 ? -1 : 1,
89
+ });
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // Paints on three canvases at once, the colors, the coverage and the shirt and accent mask.
95
+ class Brush {
96
+ constructor(width, height) {
97
+ this.layers = [0, 1, 2].map(() => {
98
+ const canvas = document.createElement('canvas');
99
+ canvas.width = width;
100
+ canvas.height = height;
101
+ return canvas.getContext('2d', { willReadFrequently: true });
102
+ });
103
+ const [color, cover, mask] = this.layers;
104
+ color.fillStyle = `rgb(${BACKGROUND.join(',')})`;
105
+ color.fillRect(0, 0, width, height);
106
+ cover.fillStyle = '#000';
107
+ cover.fillRect(0, 0, width, height);
108
+ mask.fillStyle = '#000';
109
+ mask.fillRect(0, 0, width, height);
110
+ }
111
+
112
+ style(paint, role) {
113
+ return [paint, '#fff', role === 'shirt' ? '#f00' : role === 'accent' ? '#0f0' : '#000'];
114
+ }
115
+
116
+ fill(path, paint, role = 'none') {
117
+ const styles = this.style(paint, role);
118
+ this.layers.forEach((ctx, i) => {
119
+ ctx.fillStyle = styles[i];
120
+ ctx.fill(path);
121
+ });
122
+ }
123
+
124
+ stroke(path, width, paint, role = 'none') {
125
+ const styles = this.style(paint, role);
126
+ this.layers.forEach((ctx, i) => {
127
+ ctx.lineWidth = width;
128
+ ctx.lineCap = 'round';
129
+ ctx.lineJoin = 'round';
130
+ ctx.strokeStyle = styles[i];
131
+ ctx.stroke(path);
132
+ });
133
+ }
134
+
135
+ // A tint on the colors alone, for shading that leaves the masks as they are.
136
+ shade(path, paint) {
137
+ this.layers[0].fillStyle = paint;
138
+ this.layers[0].fill(path);
139
+ }
140
+
141
+ // RGBA bytes with the coverage as alpha, and RG bytes of the mask.
142
+ read() {
143
+ const [color, cover, mask] = this.layers;
144
+ const { width, height } = color.canvas;
145
+ const c = color.getImageData(0, 0, width, height).data;
146
+ const a = cover.getImageData(0, 0, width, height).data;
147
+ const m = mask.getImageData(0, 0, width, height).data;
148
+ const rgba = new Uint8Array(width * height * 4);
149
+ const rg = new Uint8Array(width * height * 2);
150
+ for (let i = 0, j = 0, k = 0; i < c.length; i += 4, j += 4, k += 2) {
151
+ rgba[j] = c[i];
152
+ rgba[j + 1] = c[i + 1];
153
+ rgba[j + 2] = c[i + 2];
154
+ rgba[j + 3] = a[i];
155
+ rg[k] = m[i];
156
+ rg[k + 1] = m[i + 1];
157
+ }
158
+ return { rgba, rg };
159
+ }
160
+ }
161
+
162
+ // Paints one person into its seat, in meters from the middle of the seat at the height of the
163
+ // tread, so the same code serves both scales.
164
+ function paintPerson(brush, p, pose, X, Y, pxm) {
165
+ const g = (v) => `rgb(${v},${v},${v})`;
166
+ const G = ATLAS.shirtGrey;
167
+ const standing = !['sit', 'lean', 'clap', 'crossed', 'armUp', 'scarfUp', 'phone', 'sitClap'].includes(pose);
168
+ const lift = standing ? 0.44 : pose === 'lean' ? -0.1 : 0;
169
+ const t = p.tall;
170
+ const bw = p.build;
171
+ const hip = 0.5 + lift;
172
+ const shoulder = (1.0 + lift) * (standing ? 1 : t) + (standing ? (t - 1) * 1.4 : 0);
173
+ const head = shoulder + 0.17;
174
+ const dx = p.dx + (pose === 'lean' ? p.lean : 0);
175
+ const x = (m) => X + (dx + m) * pxm;
176
+ const y = (m) => Y - m * pxm;
177
+ const rect = (x0, h0, x1, h1, r) => {
178
+ const path = new Path2D();
179
+ path.roundRect(x(Math.min(x0, x1)), y(Math.max(h0, h1)), Math.abs(x1 - x0) * pxm, Math.abs(h1 - h0) * pxm, r * pxm);
180
+ return path;
181
+ };
182
+ const ellipse = (cx, cy, rx, ry, a0 = 0, a1 = Math.PI * 2) => {
183
+ const path = new Path2D();
184
+ path.ellipse(x(cx), y(cy), rx * pxm, ry * pxm, 0, a0, a1);
185
+ return path;
186
+ };
187
+ const line = (pts) => {
188
+ const path = new Path2D();
189
+ pts.forEach(([px, py], i) => (i ? path.lineTo(x(px), y(py)) : path.moveTo(x(px), y(py))));
190
+ return path;
191
+ };
192
+ const sw = 0.19 * bw;
193
+ const arm = 0.075 * bw;
194
+ const sleeve = p.sleeves ? 'shirt' : 'none';
195
+ const sleevePaint = p.sleeves ? g(Math.round(G * 0.8)) : p.skin;
196
+ // Legs, mostly hidden behind the row in front but seen between the seats.
197
+ if (standing) {
198
+ brush.fill(rect(-0.15, 0.0, -0.02, hip + 0.04, 0.03), p.legs);
199
+ brush.fill(rect(0.02, 0.0, 0.15, hip + 0.04, 0.03), p.legs);
200
+ } else {
201
+ brush.fill(rect(-0.17, 0.22, 0.17, hip + 0.06, 0.05), p.legs);
202
+ }
203
+ // Long hair falls behind the shoulders, so it goes first.
204
+ if (p.style === 'long') {
205
+ brush.fill(rect(-0.1, shoulder - 0.08, 0.1, head + 0.08, 0.06), p.hair);
206
+ }
207
+ // Arms hanging at the sides go behind the hands but in front of nothing else.
208
+ const hang = ['sit', 'stand', 'lean', 'phone'].includes(pose);
209
+ if (hang) {
210
+ for (const s of [-1, 1]) {
211
+ if (pose === 'phone' && s === p.side) {
212
+ continue;
213
+ }
214
+ brush.stroke(line([[s * (sw - 0.01), shoulder - 0.06], [s * (sw + 0.035), hip + 0.12]]), arm * pxm, sleevePaint, sleeve);
215
+ brush.fill(ellipse(s * (sw + 0.035), hip + 0.08, 0.04, 0.045), p.skin);
216
+ }
217
+ }
218
+ // The shirt, grey with lit folds, which the shader colors.
219
+ const torso = rect(-sw, hip, sw, shoulder + 0.02, 0.07);
220
+ const grad = brush.layers[0].createLinearGradient(x(-sw), 0, x(sw), 0);
221
+ grad.addColorStop(0, g(Math.round(G * 0.62)));
222
+ grad.addColorStop(0.45, g(G));
223
+ grad.addColorStop(1, g(Math.round(G * 0.7)));
224
+ brush.fill(torso, grad, 'shirt');
225
+ brush.shade(rect(-sw, hip, sw, hip + 0.1, 0.04), 'rgba(0,0,0,0.18)');
226
+ // Neck and head.
227
+ brush.fill(rect(-0.042, shoulder - 0.03, 0.042, shoulder + 0.08, 0.02), p.skin);
228
+ brush.fill(ellipse(0, head, 0.082, 0.105), p.skin);
229
+ brush.shade(ellipse(0, head - 0.06, 0.07, 0.04), 'rgba(0,0,0,0.12)');
230
+ brush.shade(rect(-0.05, head + 0.005, 0.05, head + 0.022, 0.005), 'rgba(40,20,10,0.35)');
231
+ // Hair or a hat, a hat counts as an accent in the team's colors.
232
+ if (p.style === 'short' || p.style === 'long') {
233
+ brush.fill(ellipse(0, head + 0.03, 0.088, 0.085, Math.PI, Math.PI * 2), p.hair);
234
+ } else if (p.style === 'curly') {
235
+ brush.fill(ellipse(0, head + 0.045, 0.097, 0.08, Math.PI * 0.95, Math.PI * 2.05), p.hair);
236
+ } else if (p.style === 'cap') {
237
+ brush.fill(ellipse(0, head + 0.035, 0.09, 0.08, Math.PI, Math.PI * 2), g(G), 'accent');
238
+ brush.fill(rect(-0.1, head + 0.02, 0.1, head + 0.045, 0.01), g(Math.round(G * 0.75)), 'accent');
239
+ } else if (p.style === 'beanie') {
240
+ brush.fill(ellipse(0, head + 0.045, 0.09, 0.1, Math.PI, Math.PI * 2), g(G), 'accent');
241
+ brush.fill(rect(-0.092, head + 0.025, 0.092, head + 0.06, 0.01), g(Math.round(G * 0.85)), 'shirt');
242
+ }
243
+ // A scarf around the neck, with an end over the chest.
244
+ if (p.scarf && pose !== 'scarfUp') {
245
+ brush.fill(rect(-0.12, shoulder - 0.05, 0.12, shoulder + 0.03, 0.03), g(G), 'accent');
246
+ brush.fill(rect(0.02, shoulder - 0.34, 0.085, shoulder - 0.02, 0.01), g(Math.round(G * 0.9)), 'accent');
247
+ brush.fill(rect(0.02, shoulder - 0.24, 0.085, shoulder - 0.19, 0), g(Math.round(G * 0.9)), 'shirt');
248
+ }
249
+ // Arms that move, drawn over the shirt.
250
+ const sh = (s) => [s * (sw - 0.02), shoulder - 0.05];
251
+ const hand = (hx, hy) => brush.fill(ellipse(hx, hy, 0.042, 0.048), p.skin);
252
+ const raise = (s, hx, hy, ex, ey) => {
253
+ brush.stroke(line([sh(s), [ex, ey], [hx, hy]]), arm * pxm, sleevePaint, sleeve);
254
+ hand(hx, hy);
255
+ };
256
+ if (pose === 'clap' || pose === 'sitClap' || pose === 'clapStand') {
257
+ const cy = shoulder - 0.2;
258
+ raise(-1, -0.02, cy, -sw - 0.03, shoulder - 0.3);
259
+ raise(1, 0.02, cy + 0.02, sw + 0.03, shoulder - 0.3);
260
+ } else if (pose === 'crossed') {
261
+ brush.fill(rect(-sw - 0.02, shoulder - 0.33, sw + 0.02, shoulder - 0.22, 0.05), sleevePaint, sleeve);
262
+ brush.fill(ellipse(-sw + 0.02, shoulder - 0.275, 0.035, 0.04), p.skin);
263
+ brush.fill(ellipse(sw - 0.02, shoulder - 0.275, 0.035, 0.04), p.skin);
264
+ } else if (pose === 'armUp') {
265
+ raise(p.side, p.side * 0.26, shoulder + 0.5, p.side * (sw + 0.07), shoulder + 0.2);
266
+ } else if (pose === 'fist') {
267
+ raise(p.side, p.side * 0.2, shoulder + 0.52, p.side * (sw + 0.1), shoulder + 0.18);
268
+ raise(-p.side, -p.side * 0.05, shoulder - 0.2, -p.side * (sw + 0.03), shoulder - 0.3);
269
+ } else if (pose === 'armsUp') {
270
+ raise(-1, -0.27 - p.lean, shoulder + 0.52, -sw - 0.07, shoulder + 0.22);
271
+ raise(1, 0.27 + p.lean, shoulder + 0.55, sw + 0.07, shoulder + 0.24);
272
+ } else if (pose === 'phone') {
273
+ raise(p.side, p.side * 0.06, head + 0.02, p.side * (sw + 0.05), shoulder - 0.2);
274
+ brush.fill(rect(p.side * 0.06 - 0.03, head - 0.03, p.side * 0.06 + 0.03, head + 0.07, 0.01), '#0c0f14');
275
+ } else if (pose === 'scarfUp') {
276
+ const top = shoulder + 0.45;
277
+ raise(-1, -0.2, top, -sw - 0.06, shoulder + 0.15);
278
+ raise(1, 0.2, top, sw + 0.06, shoulder + 0.15);
279
+ brush.fill(rect(-0.26, top - 0.06, 0.26, top + 0.06, 0.01), g(G), 'accent');
280
+ for (const sx of [-0.18, -0.02, 0.14]) {
281
+ brush.fill(rect(sx, top - 0.06, sx + 0.06, top + 0.06, 0), g(Math.round(G * 0.92)), 'shirt');
282
+ }
283
+ brush.fill(rect(-0.26, top - 0.06, -0.22, top - 0.2, 0.01), g(G), 'accent');
284
+ }
285
+ }
286
+
287
+ const cache = new Map();
288
+
289
+ // The two atlases at a scale, painted once and kept for every stadium after.
290
+ export function paintCrowd(scale = 1) {
291
+ if (cache.has(scale)) {
292
+ return cache.get(scale);
293
+ }
294
+ const pxm = ATLAS.pxm * scale;
295
+ const seatPx = ATLAS.seat * pxm;
296
+ const stripPx = ATLAS.strip * pxm;
297
+ const width = Math.round(ATLAS.seats * seatPx);
298
+ const height = Math.round(ATLAS.strips * stripPx);
299
+ const crowd = people();
300
+ const atlas = { width, height };
301
+ for (const variant of ['calm', 'cheer']) {
302
+ const brush = new Brush(width, height);
303
+ for (let s = 0; s < ATLAS.strips; s++) {
304
+ const bottom = (s + 1) * stripPx;
305
+ for (let c = 0; c < ATLAS.seats; c++) {
306
+ const p = crowd[s * ATLAS.seats + c];
307
+ paintPerson(brush, p, p[variant], (c + 0.5) * seatPx, bottom, pxm);
308
+ }
309
+ }
310
+ atlas[variant] = brush.read();
311
+ }
312
+ cache.set(scale, atlas);
313
+ return atlas;
314
+ }
315
+
316
+ // GRF's strip of people laid along every row of the atlas, with no mask.
317
+ export function grfCrowd(image, scale = 1) {
318
+ const pxm = ATLAS.pxm * scale;
319
+ const stripPx = ATLAS.strip * pxm;
320
+ const width = Math.round(ATLAS.seats * ATLAS.seat * pxm);
321
+ const height = Math.round(ATLAS.strips * stripPx);
322
+ const brush = new Brush(width, height);
323
+ // Six tiles of the strip across the atlas, each 1.35 meters high, which puts the heads where
324
+ // seated heads are.
325
+ const tileW = width / 6;
326
+ const tileH = 1.35 * pxm;
327
+ for (let s = 0; s < ATLAS.strips; s++) {
328
+ const top = (s + 1) * stripPx - tileH;
329
+ for (let k = 0; k < 6; k++) {
330
+ brush.layers[0].drawImage(image, k * tileW, top, tileW, tileH);
331
+ }
332
+ }
333
+ // The colors lie over the dark background, the coverage comes from a clean copy.
334
+ const alpha = document.createElement('canvas');
335
+ alpha.width = width;
336
+ alpha.height = height;
337
+ const actx = alpha.getContext('2d', { willReadFrequently: true });
338
+ for (let s = 0; s < ATLAS.strips; s++) {
339
+ const top = (s + 1) * stripPx - tileH;
340
+ for (let k = 0; k < 6; k++) {
341
+ actx.drawImage(image, k * tileW, top, tileW, tileH);
342
+ }
343
+ }
344
+ const read = brush.read();
345
+ const a = actx.getImageData(0, 0, width, height).data;
346
+ for (let i = 3; i < read.rgba.length; i += 4) {
347
+ read.rgba[i] = a[i];
348
+ }
349
+ return { width, height, calm: read, cheer: read };
350
+ }
351
+
352
+ export function atlasTextures(atlas, anisotropy = 4) {
353
+ const make = (data, format, colorSpace) => {
354
+ const tex = new THREE.DataTexture(data, atlas.width, atlas.height, format, THREE.UnsignedByteType);
355
+ tex.colorSpace = colorSpace;
356
+ tex.wrapS = THREE.RepeatWrapping;
357
+ tex.wrapT = THREE.ClampToEdgeWrapping;
358
+ tex.magFilter = THREE.LinearFilter;
359
+ tex.minFilter = THREE.LinearMipmapLinearFilter;
360
+ tex.generateMipmaps = true;
361
+ tex.anisotropy = anisotropy;
362
+ tex.unpackAlignment = 1;
363
+ tex.needsUpdate = true;
364
+ return tex;
365
+ };
366
+ const calmColor = make(atlas.calm.rgba, THREE.RGBAFormat, THREE.SRGBColorSpace);
367
+ const calmMask = make(atlas.calm.rg, THREE.RGFormat, THREE.NoColorSpace);
368
+ const same = atlas.cheer === atlas.calm;
369
+ return {
370
+ calmColor,
371
+ calmMask,
372
+ cheerColor: same ? calmColor : make(atlas.cheer.rgba, THREE.RGBAFormat, THREE.SRGBColorSpace),
373
+ cheerMask: same ? calmMask : make(atlas.cheer.rg, THREE.RGFormat, THREE.NoColorSpace),
374
+ size: new THREE.Vector2(atlas.width, atlas.height),
375
+ };
376
+ }
@@ -0,0 +1,227 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Field settings for grass, lines, nets, boards, crowd, time of day and weather. They live in this
5
+ // browser only, and travel as a JSON file with any images embedded as data URLs.
6
+
7
+ import { ARENAS } from './arenas.js';
8
+ import { PRESET_HOURS, WEATHER } from './sky.js';
9
+ import { t } from '../core/i18n.js';
10
+
11
+ const KEY = 'cuju.field';
12
+
13
+ export const MOWING = ['stripes', 'checkers', 'circles', 'none'];
14
+ export const BOARDS = ['sponsors', 'shipped', 'custom'];
15
+ // Named hours for the time of day, the slider takes any hour between.
16
+ export const TIMES = ['dawn', 'day', 'dusk', 'night'];
17
+ export const TIME_HOURS = PRESET_HOURS;
18
+ export const WEATHERS = Object.keys(WEATHER);
19
+ export const FIELD_VERSION = 2;
20
+ export const CROWDS = ['teams', 'custom', 'grf'];
21
+ // Boards are kept as data addresses in the browser's storage, which holds only a few megabytes.
22
+ export const MAX_BOARDS = 8;
23
+ // An exported field holds at most eight boards of well under a megabyte each.
24
+ export const MAX_IMPORT_BYTES = 8 * 1024 * 1024;
25
+
26
+ export const DEFAULT_FIELD = {
27
+ version: FIELD_VERSION,
28
+ grass: '#3f7f2c',
29
+ mowing: 'stripes',
30
+ stripeWidth: 5.5,
31
+ lines: '#ffffff',
32
+ net: '#ffffff',
33
+ boards: 'sponsors',
34
+ customBoards: [],
35
+ crowd: 'teams',
36
+ crowdColors: ['#d8d8d8', '#202428', '#b3262e', '#2a56b8', '#e0b33a', '#3c8a4a'],
37
+ crowdDensity: 0.9,
38
+ time: 15,
39
+ weather: 'clear',
40
+ weatherAmount: 0.75,
41
+ wind: 0.2,
42
+ arena: 'stadium',
43
+ };
44
+
45
+ export const PRESETS = {
46
+ 'Summer afternoon': {},
47
+ 'Autumn dusk': { grass: '#4a6e2a', time: 19, mowing: 'checkers', stripeWidth: 8, weather: 'overcast', weatherAmount: 0.5 },
48
+ 'Floodlit night': { grass: '#2f6a2a', time: 22, mowing: 'stripes', stripeWidth: 5.5 },
49
+ 'Rainy night': { grass: '#2f6a2a', time: 21.5, weather: 'rain', weatherAmount: 0.8, wind: 0.35 },
50
+ 'Winter snow': { grass: '#4a7a3a', time: 14, weather: 'snow', weatherAmount: 0.8, wind: 0.15, lines: '#2a56b8' },
51
+ 'Beach at sunset': { arena: 'beach', grass: '#4a8a34', time: 19.25, weather: 'clear', wind: 0.3 },
52
+ 'Rooftop by night': { arena: 'rooftop', grass: '#2f6a2a', time: 22.5, weather: 'clear', wind: 0.1 },
53
+ 'In orbit': { arena: 'space', grass: '#3b7a34', mowing: 'checkers', stripeWidth: 8, lines: '#bfefff', weather: 'clear' },
54
+ 'Training ground': {
55
+ grass: '#5a8a3a', mowing: 'none', crowd: 'custom', crowdDensity: 0.08, boards: 'shipped',
56
+ },
57
+ 'Circles and gold': {
58
+ grass: '#3b7a34', mowing: 'circles', stripeWidth: 6, lines: '#fff4c4', net: '#f2c14e',
59
+ crowd: 'custom', crowdColors: ['#f2c14e', '#1d1d1d', '#ffffff'], crowdDensity: 0.95,
60
+ },
61
+ };
62
+
63
+ const isColor = (v) => typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v);
64
+ const isBoard = (b) => typeof b === 'string' && b.startsWith('data:image/');
65
+ const unit = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);
66
+
67
+ // The hour of a stored time, which older settings keep as a name.
68
+ export function fieldHour(time) {
69
+ if (typeof time === 'string' && time in PRESET_HOURS) {
70
+ return PRESET_HOURS[time];
71
+ }
72
+ return typeof time === 'number' && Number.isFinite(time) ? Math.min(24, Math.max(0, time)) : DEFAULT_FIELD.time;
73
+ }
74
+
75
+ // Fills and checks every field, unknown values fall back to the defaults.
76
+ export function cleanField(raw) {
77
+ const r = raw && typeof raw === 'object' ? raw : {};
78
+ const d = DEFAULT_FIELD;
79
+ const width = Number(r.stripeWidth);
80
+ const density = Number(r.crowdDensity);
81
+ const colors = Array.isArray(r.crowdColors) ? r.crowdColors.filter(isColor).slice(0, 12) : [];
82
+ return {
83
+ version: FIELD_VERSION,
84
+ grass: isColor(r.grass) ? r.grass : d.grass,
85
+ mowing: MOWING.includes(r.mowing) ? r.mowing : d.mowing,
86
+ stripeWidth: Number.isFinite(width) ? Math.min(20, Math.max(1, width)) : d.stripeWidth,
87
+ lines: isColor(r.lines) ? r.lines : d.lines,
88
+ net: isColor(r.net) ? r.net : d.net,
89
+ boards: BOARDS.includes(r.boards) ? r.boards : d.boards,
90
+ customBoards: Array.isArray(r.customBoards)
91
+ ? r.customBoards.filter(isBoard).slice(0, MAX_BOARDS)
92
+ : [],
93
+ crowd: CROWDS.includes(r.crowd) ? r.crowd : d.crowd,
94
+ crowdColors: colors.length ? colors : d.crowdColors.slice(),
95
+ crowdDensity: Number.isFinite(density) ? Math.min(1, Math.max(0, density)) : d.crowdDensity,
96
+ time: fieldHour(r.time),
97
+ weather: WEATHERS.includes(r.weather) ? r.weather : d.weather,
98
+ weatherAmount: unit(r.weatherAmount, d.weatherAmount),
99
+ wind: unit(r.wind, d.wind),
100
+ arena: ARENAS.includes(r.arena) ? r.arena : d.arena,
101
+ };
102
+ }
103
+
104
+ export function loadField() {
105
+ try {
106
+ return cleanField(JSON.parse(localStorage.getItem(KEY) || '{}'));
107
+ } catch {
108
+ return cleanField({});
109
+ }
110
+ }
111
+
112
+ // Returns an error message when the browser refuses to store the settings.
113
+ export function saveField(field) {
114
+ try {
115
+ localStorage.setItem(KEY, JSON.stringify(field));
116
+ return null;
117
+ } catch (err) {
118
+ return t('field.store.failed', { reason: err.name === 'QuotaExceededError' ? t('field.store.tooLarge') : err.message });
119
+ }
120
+ }
121
+
122
+ export function presetField(name) {
123
+ return cleanField({ ...DEFAULT_FIELD, ...(PRESETS[name] || {}) });
124
+ }
125
+
126
+ export function exportField(field) {
127
+ const blob = new Blob([JSON.stringify(field, null, 2)], { type: 'application/json' });
128
+ const a = document.createElement('a');
129
+ a.href = URL.createObjectURL(blob);
130
+ a.download = 'cuju-field.json';
131
+ document.body.appendChild(a);
132
+ a.click();
133
+ a.remove();
134
+ setTimeout(() => URL.revokeObjectURL(a.href), 2000);
135
+ }
136
+
137
+ // What each setting of an export must hold. A file with a wrong value is refused whole, so an
138
+ // import never half applies.
139
+ const CHECKS = {
140
+ version: (v) => Number.isInteger(v) && v >= 1 && v <= FIELD_VERSION,
141
+ grass: isColor,
142
+ lines: isColor,
143
+ net: isColor,
144
+ mowing: (v) => MOWING.includes(v),
145
+ stripeWidth: (v) => typeof v === 'number' && v >= 1 && v <= 20,
146
+ boards: (v) => BOARDS.includes(v),
147
+ customBoards: (v) => Array.isArray(v) && v.every(isBoard),
148
+ crowd: (v) => CROWDS.includes(v),
149
+ crowdColors: (v) => Array.isArray(v) && v.length > 0 && v.every(isColor),
150
+ crowdDensity: (v) => typeof v === 'number' && v >= 0 && v <= 1,
151
+ time: (v) => (typeof v === 'string' && v in PRESET_HOURS) || (typeof v === 'number' && v >= 0 && v <= 24),
152
+ weather: (v) => WEATHERS.includes(v),
153
+ weatherAmount: (v) => typeof v === 'number' && v >= 0 && v <= 1,
154
+ wind: (v) => typeof v === 'number' && v >= 0 && v <= 1,
155
+ arena: (v) => ARENAS.includes(v),
156
+ };
157
+ // The names of the settings in the messages of a refused import, the keys stay English.
158
+ const NAMES = {
159
+ grass: t('field.setting.grass'), lines: t('field.setting.lines'), net: t('field.setting.net'),
160
+ mowing: t('field.setting.mowing'), stripeWidth: t('field.setting.stripeWidth'), boards: t('field.setting.boards'),
161
+ customBoards: t('field.setting.customBoards'), crowd: t('field.setting.crowd'), crowdColors: t('field.setting.crowdColors'),
162
+ crowdDensity: t('field.setting.crowdDensity'), time: t('field.setting.time'), weather: t('field.setting.weather'),
163
+ weatherAmount: t('field.setting.weatherAmount'), wind: t('field.setting.wind'), arena: t('field.setting.arena'),
164
+ version: t('field.setting.version'),
165
+ };
166
+
167
+ // Checks a parsed export against the current field. Returns the field to apply and notes for
168
+ // the user, or throws with the reason the file is refused. Settings the file leaves out keep
169
+ // their current values, so a file without boards keeps the boards already drawn.
170
+ export function validateImport(raw, current) {
171
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
172
+ throw new Error(t('field.import.notExport'));
173
+ }
174
+ const known = Object.keys(raw).filter((k) => k in CHECKS);
175
+ if (!('version' in raw) || known.length < 2) {
176
+ throw new Error(t('field.import.noSettings'));
177
+ }
178
+ if (typeof raw.version === 'number' && raw.version > FIELD_VERSION) {
179
+ throw new Error(t('field.import.newer'));
180
+ }
181
+ const bad = known.filter((k) => !CHECKS[k](raw[k]));
182
+ if (bad.length) {
183
+ const list = bad.map((k) => NAMES[k] || k).join(', ');
184
+ throw new Error(t('field.import.damaged', { count: bad.length, settings: list }));
185
+ }
186
+ const notes = [];
187
+ const merged = { ...current };
188
+ for (const k of known) {
189
+ merged[k] = raw[k];
190
+ }
191
+ if (Array.isArray(raw.customBoards) && raw.customBoards.length > MAX_BOARDS) {
192
+ const left = raw.customBoards.length - MAX_BOARDS;
193
+ notes.push(t('field.import.boardsLeft', { count: left, total: raw.customBoards.length, max: MAX_BOARDS }));
194
+ }
195
+ const unknown = Object.keys(raw).filter((k) => !(k in CHECKS));
196
+ if (unknown.length) {
197
+ const names = unknown.slice(0, 4).join(', ');
198
+ notes.push(unknown.length > 4 ? t('field.import.unknownMore', { names }) : t('field.import.unknown', { count: unknown.length, names }));
199
+ }
200
+ return { field: cleanField(merged), notes };
201
+ }
202
+
203
+ export async function importField(file, current = DEFAULT_FIELD) {
204
+ if (file.size > MAX_IMPORT_BYTES) {
205
+ throw new Error(t('field.import.tooLarge', { size: Math.ceil(file.size / 1024 / 1024), max: MAX_IMPORT_BYTES / 1024 / 1024 }));
206
+ }
207
+ const text = await file.text();
208
+ let raw;
209
+ try {
210
+ raw = JSON.parse(text);
211
+ } catch {
212
+ throw new Error(t('field.import.notJson'));
213
+ }
214
+ return validateImport(raw, current);
215
+ }
216
+
217
+ export async function loadImages(dataUrls) {
218
+ const out = [];
219
+ for (const url of dataUrls) {
220
+ try {
221
+ out.push(await createImageBitmap(await (await fetch(url)).blob()));
222
+ } catch {
223
+ // A broken image is skipped.
224
+ }
225
+ }
226
+ return out;
227
+ }