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,185 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The range and settings of a video export, as the Export video dialog of the Watch page reads
5
+ // them and the server's endpoint POST /api/matches/{id}/video takes them.
6
+
7
+ import { formatNumber, serverText, t } from '../core/i18n.js';
8
+ import { SIZE_PRESETS, checkSize, parseGraphicsScale, parseSafe } from './sizes.js';
9
+
10
+ // The sizes, frame rates, qualities and cameras that the server accepts from the page. The sizes
11
+ // are the shared presets of sizes.js, and a custom size takes any even width and height within
12
+ // their limits.
13
+ export const VIDEO_SIZES = SIZE_PRESETS;
14
+ export const VIDEO_RATES = [24, 25, 30, 50, 60];
15
+ export const VIDEO_QUALITIES = [
16
+ { id: 'draft', label: t('video.quality.draft') },
17
+ { id: 'standard', label: t('video.quality.standard') },
18
+ { id: 'high', label: t('video.quality.high') },
19
+ ];
20
+ export const VIDEO_CAMERAS = [
21
+ { id: 'broadcast', label: t('video.camera.broadcast') },
22
+ { id: 'tactical', label: t('video.camera.tactical') },
23
+ { id: 'goal', label: t('video.camera.goal') },
24
+ { id: 'ball', label: t('video.camera.ball') },
25
+ { id: 'owner', label: t('video.camera.owner') },
26
+ ];
27
+ // The longest export that the page asks for, in seconds, as the server's default limit.
28
+ export const VIDEO_MAX_SECONDS = 600;
29
+ // A goal's range runs from this long before the goal to this long after it.
30
+ export const GOAL_BEFORE = 8;
31
+ export const GOAL_AFTER = 4;
32
+
33
+ // Reads a moment typed as seconds, such as 75 or 75.5, or as minutes and seconds, such as 1:15.
34
+ // Returns the seconds, or null for text that names no moment.
35
+ export function parseMoment(text) {
36
+ const value = String(text ?? '').trim();
37
+ let m = value.match(/^(\d+(?:\.\d+)?)$/);
38
+ if (m) {
39
+ return Number(m[1]);
40
+ }
41
+ m = value.match(/^(\d+):(\d{1,2}(?:\.\d+)?)$/);
42
+ if (m && Number(m[2]) < 60) {
43
+ return Number(m[1]) * 60 + Number(m[2]);
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // Writes seconds as minutes and seconds with a tenth, such as 1:05.3, or 1:05 for whole seconds.
49
+ export function formatMoment(seconds) {
50
+ const tenths = Math.max(0, Math.round(seconds * 10));
51
+ const whole = Math.floor(tenths / 10);
52
+ const text = `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}`;
53
+ return tenths % 10 ? `${text}.${tenths % 10}` : text;
54
+ }
55
+
56
+ // The range around a goal at the given seconds, kept within a recording of duration seconds.
57
+ export function goalRange(at, duration) {
58
+ return { start: Math.max(0, at - GOAL_BEFORE), end: Math.min(duration, at + GOAL_AFTER) };
59
+ }
60
+
61
+ // Checks a range typed in the dialog against a recording of duration seconds. Returns the start
62
+ // and end in seconds, or an error to show.
63
+ export function checkRange(startText, endText, duration) {
64
+ const start = parseMoment(startText);
65
+ const end = parseMoment(endText);
66
+ if (start == null || end == null) {
67
+ return { error: t('video.range.unreadable') };
68
+ }
69
+ if (start >= duration) {
70
+ return { error: t('video.range.past_end', { end: formatMoment(duration) }) };
71
+ }
72
+ const last = Math.min(end, duration);
73
+ if (last <= start) {
74
+ return { error: t('video.range.order') };
75
+ }
76
+ if (last - start > VIDEO_MAX_SECONDS) {
77
+ return { error: t('video.range.too_long', { count: VIDEO_MAX_SECONDS / 60 }) };
78
+ }
79
+ return { start, end: last };
80
+ }
81
+
82
+ // The body of an export request, from a checked range and the choices of the dialog. The arena
83
+ // and time of day come from the field settings of the page.
84
+ export function exportRequest(range, choices, field) {
85
+ const size = videoSize(choices.size);
86
+ const body = {
87
+ start: Math.round(range.start * 1000) / 1000,
88
+ end: Math.round(range.end * 1000) / 1000,
89
+ camera: VIDEO_CAMERAS.some((c) => c.id === choices.camera) ? choices.camera : 'broadcast',
90
+ width: size.width,
91
+ height: size.height,
92
+ fps: VIDEO_RATES.includes(Number(choices.fps)) ? Number(choices.fps) : 30,
93
+ quality: VIDEO_QUALITIES.some((q) => q.id === choices.quality) ? choices.quality : 'standard',
94
+ score: choices.score !== false,
95
+ };
96
+ // The annotations show only when asked for, so the field stays out otherwise.
97
+ if (choices.annotations === true) {
98
+ body.annotations = true;
99
+ }
100
+ const graphics = graphicsScale(choices.scale);
101
+ if (graphics !== 1) {
102
+ body.scale = graphics;
103
+ }
104
+ const safe = safeChoice(choices.safe);
105
+ if (safe !== 'auto') {
106
+ body.safe = safe;
107
+ }
108
+ if (field && typeof field.arena === 'string') {
109
+ body.arena = field.arena;
110
+ }
111
+ if (field && typeof field.time === 'number' && Number.isFinite(field.time)) {
112
+ body.time = Math.min(24, Math.max(0, field.time));
113
+ }
114
+ return body;
115
+ }
116
+
117
+ // The size of a choice, a preset's identifier or a width and height, which a video needs even.
118
+ // Anything else falls back to 720p.
119
+ export function videoSize(choice) {
120
+ const preset = VIDEO_SIZES.find((s) => s.id === choice);
121
+ if (preset) {
122
+ return preset;
123
+ }
124
+ if (choice && typeof choice === 'object') {
125
+ try {
126
+ return checkSize(choice.width, choice.height, { even: true });
127
+ } catch {
128
+ // A size out of range falls back below.
129
+ }
130
+ }
131
+ return VIDEO_SIZES.find((s) => s.id === '720p');
132
+ }
133
+
134
+ function graphicsScale(value) {
135
+ try {
136
+ return parseGraphicsScale(value);
137
+ } catch {
138
+ return 1;
139
+ }
140
+ }
141
+
142
+ function safeChoice(value) {
143
+ try {
144
+ return parseSafe(value);
145
+ } catch {
146
+ return 'auto';
147
+ }
148
+ }
149
+
150
+ // The size of a file for people, in KB below a megabyte and in MB from there.
151
+ export function fileSize(bytes) {
152
+ return bytes < 1e6
153
+ ? t('video.size.kb', { kb: Math.max(1, Math.round(bytes / 1e3)) })
154
+ : t('video.size.mb', { mb: formatNumber(bytes / 1e6, { minimumFractionDigits: 1, maximumFractionDigits: 1, useGrouping: false }) });
155
+ }
156
+
157
+ // The line of progress of an export entry from the server.
158
+ export function progressText(entry) {
159
+ if (!entry) {
160
+ return '';
161
+ }
162
+ if (entry.state === 'done') {
163
+ // The entry may be an export of before, so the line names its range, which the fields of
164
+ // the dialog may no longer show.
165
+ if (typeof entry.start === 'number' && typeof entry.end === 'number') {
166
+ const range = { start: formatMoment(entry.start), end: formatMoment(entry.end) };
167
+ return entry.bytes ? t('video.progress.ready_range_size', { ...range, size: fileSize(entry.bytes) }) : t('video.progress.ready_range', range);
168
+ }
169
+ return entry.bytes ? t('video.progress.ready_size', { size: fileSize(entry.bytes) }) : t('video.progress.ready');
170
+ }
171
+ if (entry.state === 'failed') {
172
+ const error = serverText(entry);
173
+ return error ? t('video.progress.failed_reason', { error }) : t('video.progress.failed');
174
+ }
175
+ if (entry.state === 'cancelled') {
176
+ return t('video.progress.cancelled');
177
+ }
178
+ if (!entry.frame) {
179
+ return t(entry.browser ? 'video.browser.starting' : 'video.progress.starting');
180
+ }
181
+ if (entry.progress >= 1) {
182
+ return t('video.progress.finishing');
183
+ }
184
+ return t('video.progress.drawing', { frame: entry.frame, frames: entry.frames, percent: Math.round(entry.progress * 100) });
185
+ }
@@ -0,0 +1,198 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // A small writer of WebM files of one video track, VP9 or VP8, for the video export of a browser
5
+ // without H.264. It writes the EBML header and a segment of Matroska, with a seek head, the
6
+ // segment's information and its track, the frames in clusters that each start at a key frame, and
7
+ // cues that point at the clusters, so a player seeks the file. The clusters gather into blobs as
8
+ // they close, so a browser may keep a long video on its disk rather than in memory.
9
+
10
+ const ID = {
11
+ ebml: 0x1a45dfa3, ebmlVersion: 0x4286, ebmlReadVersion: 0x42f7, maxIdLength: 0x42f2, maxSizeLength: 0x42f3,
12
+ docType: 0x4282, docTypeVersion: 0x4287, docTypeReadVersion: 0x4285,
13
+ segment: 0x18538067, seekHead: 0x114d9b74, seek: 0x4dbb, seekId: 0x53ab, seekPosition: 0x53ac,
14
+ info: 0x1549a966, timestampScale: 0x2ad7b1, duration: 0x4489, muxingApp: 0x4d80, writingApp: 0x5741,
15
+ tracks: 0x1654ae6b, trackEntry: 0xae, trackNumber: 0xd7, trackUid: 0x73c5, trackType: 0x83,
16
+ flagLacing: 0x9c, codecId: 0x86, codecPrivate: 0x63a2, defaultDuration: 0x23e383, video: 0xe0,
17
+ pixelWidth: 0xb0, pixelHeight: 0xba,
18
+ cluster: 0x1f43b675, timestamp: 0xe7, simpleBlock: 0xa3,
19
+ cues: 0x1c53bb6b, cuePoint: 0xbb, cueTime: 0xb3, cueTrackPositions: 0xb7, cueTrack: 0xf7,
20
+ cueClusterPosition: 0xf1,
21
+ };
22
+
23
+ // The codec names of Matroska for the codecs of the browser's encoder.
24
+ export const WEBM_CODECS = Object.freeze({ vp8: 'V_VP8', vp09: 'V_VP9', av01: 'V_AV1' });
25
+
26
+ // The bytes of an element's identifier, which carries its own length.
27
+ function idBytes(id) {
28
+ const out = [];
29
+ for (let value = id; value > 0; value = Math.floor(value / 256)) {
30
+ out.unshift(value & 255);
31
+ }
32
+ return Uint8Array.from(out);
33
+ }
34
+
35
+ // A size as a variable length integer, of the given length or the shortest that holds it.
36
+ export function sizeBytes(size, length = 0) {
37
+ let n = length;
38
+ if (!n) {
39
+ n = 1;
40
+ while (n < 8 && size >= 2 ** (7 * n) - 1) {
41
+ n++;
42
+ }
43
+ }
44
+ const out = new Uint8Array(n);
45
+ let value = BigInt(size);
46
+ for (let i = n - 1; i >= 0; i--) {
47
+ out[i] = Number(value & 255n);
48
+ value >>= 8n;
49
+ }
50
+ out[0] |= 1 << (8 - n);
51
+ return out;
52
+ }
53
+
54
+ function uintBytes(value, length = 0) {
55
+ let n = length;
56
+ if (!n) {
57
+ n = 1;
58
+ while (n < 8 && value >= 2 ** (8 * n)) {
59
+ n++;
60
+ }
61
+ }
62
+ const out = new Uint8Array(n);
63
+ let v = BigInt(value);
64
+ for (let i = n - 1; i >= 0; i--) {
65
+ out[i] = Number(v & 255n);
66
+ v >>= 8n;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function concat(parts) {
72
+ const out = new Uint8Array(parts.reduce((sum, p) => sum + p.length, 0));
73
+ let at = 0;
74
+ for (const p of parts) {
75
+ out.set(p, at);
76
+ at += p.length;
77
+ }
78
+ return out;
79
+ }
80
+
81
+ const text = (value) => Uint8Array.from(value, (c) => c.charCodeAt(0));
82
+
83
+ // An element of an identifier and a body of bytes.
84
+ function element(id, body) {
85
+ return concat([idBytes(id), sizeBytes(body.length), body]);
86
+ }
87
+
88
+ const master = (id, ...children) => element(id, concat(children));
89
+ const uint = (id, value, length = 0) => element(id, uintBytes(value, length));
90
+
91
+ function float(id, value) {
92
+ const body = new Uint8Array(8);
93
+ new DataView(body.buffer).setFloat64(0, value);
94
+ return element(id, body);
95
+ }
96
+
97
+ // Gathers the encoded frames of one video track and writes the file.
98
+ export class WebmWriter {
99
+ // codec is the codec string of the encoder, such as vp09.00.41.08 or vp8.
100
+ constructor({ width, height, fps, codec }) {
101
+ this.width = width;
102
+ this.height = height;
103
+ this.fps = fps;
104
+ const family = Object.keys(WEBM_CODECS).find((key) => String(codec).startsWith(key));
105
+ if (!family) {
106
+ throw new Error(`WebM holds VP8, VP9 or AV1, not ${codec}.`);
107
+ }
108
+ this.codecId = WEBM_CODECS[family];
109
+ this.description = null;
110
+ this.clusters = [];
111
+ this.cues = [];
112
+ this.open = null;
113
+ this.position = 0;
114
+ this.bytes = 0;
115
+ this.count = 0;
116
+ this.end = 0;
117
+ }
118
+
119
+ // Adds one encoded frame, its bytes, its time and length in microseconds and whether it is a
120
+ // key frame.
121
+ add(data, { timestamp, duration, key, description }) {
122
+ if (description && !this.description) {
123
+ const view = description instanceof ArrayBuffer ? new Uint8Array(description) : new Uint8Array(description.buffer, description.byteOffset, description.byteLength);
124
+ this.description = view.slice();
125
+ }
126
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
127
+ const ms = Math.round(timestamp / 1000);
128
+ if (!this.open || key || ms - this.open.time > 32767) {
129
+ this.close();
130
+ this.open = { time: ms, parts: [uint(ID.timestamp, ms)], size: 0 };
131
+ this.open.size = this.open.parts[0].length;
132
+ if (key) {
133
+ this.cues.push({ time: ms, position: this.position });
134
+ }
135
+ }
136
+ const head = new Uint8Array(4);
137
+ head[0] = 0x81;
138
+ new DataView(head.buffer).setInt16(1, ms - this.open.time);
139
+ head[3] = key ? 0x80 : 0;
140
+ const block = concat([idBytes(ID.simpleBlock), sizeBytes(head.length + bytes.length), head]);
141
+ this.open.parts.push(block, bytes);
142
+ this.open.size += block.length + bytes.length;
143
+ this.bytes += bytes.length;
144
+ this.count++;
145
+ this.end = Math.max(this.end, (timestamp + duration) / 1000);
146
+ }
147
+
148
+ // Closes the open cluster into a blob, or a list of byte arrays where the runtime has none.
149
+ close() {
150
+ if (!this.open) {
151
+ return;
152
+ }
153
+ const head = concat([idBytes(ID.cluster), sizeBytes(this.open.size, 8)]);
154
+ const parts = [head, ...this.open.parts];
155
+ this.clusters.push(typeof Blob === 'function' ? new Blob(parts) : parts);
156
+ this.position += head.length + this.open.size;
157
+ this.open = null;
158
+ }
159
+
160
+ // The finished file as a blob of the type video/webm, or as a list of byte arrays where the
161
+ // runtime has no Blob.
162
+ finish() {
163
+ if (!this.count) {
164
+ throw new Error('The video holds no frames.');
165
+ }
166
+ this.close();
167
+ const header = master(ID.ebml,
168
+ uint(ID.ebmlVersion, 1), uint(ID.ebmlReadVersion, 1), uint(ID.maxIdLength, 4), uint(ID.maxSizeLength, 8),
169
+ element(ID.docType, text('webm')), uint(ID.docTypeVersion, 4), uint(ID.docTypeReadVersion, 2));
170
+ const info = master(ID.info, uint(ID.timestampScale, 1e6), float(ID.duration, this.end),
171
+ element(ID.muxingApp, text('cuju')), element(ID.writingApp, text('cuju')));
172
+ const entry = [
173
+ uint(ID.trackNumber, 1), uint(ID.trackUid, 1), uint(ID.trackType, 1), uint(ID.flagLacing, 0),
174
+ element(ID.codecId, text(this.codecId)),
175
+ ];
176
+ if (this.description) {
177
+ entry.push(element(ID.codecPrivate, this.description));
178
+ }
179
+ entry.push(uint(ID.defaultDuration, Math.round(1e9 / this.fps)),
180
+ master(ID.video, uint(ID.pixelWidth, this.width), uint(ID.pixelHeight, this.height)));
181
+ const tracks = master(ID.tracks, master(ID.trackEntry, ...entry));
182
+ // The seek head takes positions of eight bytes, so its size is known before the positions.
183
+ const seekHead = (positions) => master(ID.seekHead, ...positions.map(([id, at]) => master(ID.seek,
184
+ element(ID.seekId, idBytes(id)), uint(ID.seekPosition, at, 8))));
185
+ const headSize = seekHead([[ID.info, 0], [ID.tracks, 0], [ID.cues, 0]]).length;
186
+ const first = headSize + info.length + tracks.length;
187
+ const cues = master(ID.cues, ...this.cues.map((cue) => master(ID.cuePoint, uint(ID.cueTime, cue.time),
188
+ master(ID.cueTrackPositions, uint(ID.cueTrack, 1), uint(ID.cueClusterPosition, first + cue.position)))));
189
+ const seek = seekHead([[ID.info, headSize], [ID.tracks, headSize + info.length], [ID.cues, first + this.position]]);
190
+ const segmentSize = first + this.position + cues.length;
191
+ const segment = concat([idBytes(ID.segment), sizeBytes(segmentSize, 8)]);
192
+ const parts = [header, segment, seek, info, tracks, ...this.clusters, cues];
193
+ if (typeof Blob === 'function') {
194
+ return new Blob(parts, { type: 'video/webm' });
195
+ }
196
+ return parts.flat();
197
+ }
198
+ }
@@ -0,0 +1,151 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The start of the page as a clean feed, active only when the address carries the query parameter
5
+ // feed, as the frame of /feed gives it. It runs before main.js, so the viewer starts with the
6
+ // feed's own settings. The settings live in memory for the visit, so a feed never changes the
7
+ // settings that the browser keeps for the page, and the same address always looks the same. The
8
+ // pointer stays free, keys and controllers reach nothing, and the page opens on Watch.
9
+
10
+ import { parseFeed } from './export/feed-params.js';
11
+
12
+ const params = new URLSearchParams(location.search);
13
+
14
+ // Settings in memory, with the interface of localStorage.
15
+ export class MemoryStorage {
16
+ constructor(entries) {
17
+ this.map = new Map(entries);
18
+ }
19
+
20
+ get length() {
21
+ return this.map.size;
22
+ }
23
+
24
+ key(index) {
25
+ return [...this.map.keys()][index] ?? null;
26
+ }
27
+
28
+ getItem(key) {
29
+ return this.map.has(String(key)) ? this.map.get(String(key)) : null;
30
+ }
31
+
32
+ setItem(key, value) {
33
+ this.map.set(String(key), String(value));
34
+ }
35
+
36
+ removeItem(key) {
37
+ this.map.delete(String(key));
38
+ }
39
+
40
+ clear() {
41
+ this.map.clear();
42
+ }
43
+ }
44
+
45
+ // The stored settings that give the feed its look, from its checked options.
46
+ export function feedSettings(options) {
47
+ const broadcast = options.broadcast;
48
+ const remembered = ['broadcast', 'tactical', 'goal'].includes(options.camera) ? options.camera : 'broadcast';
49
+ const settings = {
50
+ muted: !options.audio,
51
+ volume: options.volume,
52
+ quality: options.quality,
53
+ theme: options.theme,
54
+ tvMode: true,
55
+ idleReturn: false,
56
+ pointerLock: false,
57
+ camera: remembered,
58
+ lineups: broadcast,
59
+ celebrations: broadcast,
60
+ replays: broadcast,
61
+ decisionReplays: broadcast,
62
+ transitions: broadcast,
63
+ podium: broadcast && options.show,
64
+ tips: { off: true },
65
+ playerLabels: false,
66
+ clickFollow: false,
67
+ view2dDeclined: true,
68
+ touchControls: 'off',
69
+ annotations: { players: false, actions: false, text: false },
70
+ captions: options.captions,
71
+ };
72
+ const field = {};
73
+ if (options.arena) {
74
+ field.arena = options.arena;
75
+ }
76
+ if (options.time != null) {
77
+ field.time = options.time;
78
+ }
79
+ if (Object.keys(field).length) {
80
+ settings.field = field;
81
+ }
82
+ return settings;
83
+ }
84
+
85
+ function boot() {
86
+ const { options, problems } = parseFeed(params);
87
+ const settings = feedSettings(options);
88
+ const storage = new MemoryStorage(Object.entries(settings).map(([key, value]) => [`cuju.${key}`, JSON.stringify(value)]));
89
+ try {
90
+ Object.defineProperty(window, 'localStorage', { configurable: true, get: () => storage });
91
+ } catch {
92
+ // A browser that keeps its storage fixed runs the feed on the stored settings.
93
+ }
94
+ // The frame of /feed scales the page up to the picture, so the drawing takes the picture's
95
+ // own pixels rather than those of the layout.
96
+ let ratio = Math.min(8, Math.max(0.25, Number(params.get('ratio')) || window.devicePixelRatio || 1));
97
+ try {
98
+ Object.defineProperty(window, 'devicePixelRatio', { configurable: true, get: () => ratio });
99
+ } catch {
100
+ // The drawing then takes the window's own ratio.
101
+ }
102
+ Element.prototype.requestPointerLock = function () {
103
+ return Promise.reject(new Error('The pointer stays free in a feed.'));
104
+ };
105
+ Object.defineProperty(navigator, 'getGamepads', { configurable: true, value: () => [] });
106
+ // Keys reach nothing, a feed shows the match on its own.
107
+ for (const type of ['keydown', 'keyup', 'keypress']) {
108
+ window.addEventListener(type, (e) => {
109
+ e.stopImmediatePropagation();
110
+ e.preventDefault();
111
+ }, true);
112
+ }
113
+ window.addEventListener('contextmenu', (e) => e.preventDefault(), true);
114
+ history.replaceState(null, '', `${location.pathname}${location.search}#page=watch`);
115
+ const root = document.documentElement;
116
+ root.classList.add('cuju-feed');
117
+ root.dataset.feedLayer = options.layer;
118
+ root.dataset.feedBackground = options.background === 'transparent' ? 'transparent' : 'solid';
119
+ root.style.setProperty('--feed-background', options.background);
120
+ if (!options.score) {
121
+ root.classList.add('feed-no-score');
122
+ }
123
+ window.cujuFeed = {
124
+ options,
125
+ problems,
126
+ ready: false,
127
+ broken: null,
128
+ alive: Date.now(),
129
+ get ratio() {
130
+ return ratio;
131
+ },
132
+ // The frame calls this when the window changes size, and the page then draws anew.
133
+ setRatio(value) {
134
+ const next = Math.min(8, Math.max(0.25, Number(value) || 1));
135
+ if (next !== ratio) {
136
+ ratio = next;
137
+ const app = window.cuju;
138
+ if (app && app.view && app.settings) {
139
+ app.view.setQuality(app.settings.quality);
140
+ }
141
+ }
142
+ },
143
+ };
144
+ for (const problem of problems) {
145
+ console.warn(`cuju feed: ${problem}`);
146
+ }
147
+ }
148
+
149
+ if (params.has('feed')) {
150
+ boot();
151
+ }
@@ -0,0 +1,101 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The frame of the clean feed, the page at /feed. It lays the Watch page out at the size that
5
+ // keeps the graphics in proportion, as layoutFor of sizes.js gives it, and scales the frame up to
6
+ // the picture, so a feed of 1920 by 1080 shows the graphics as the page shows them at 1280 by
7
+ // 720, and a vertical feed lays out 720 wide. A window of another size than the picture fits the
8
+ // picture inside it, centered. The page inside draws at the picture's own pixels, and a page
9
+ // that stops answering, fails to load or loses its graphics context loads again. The page inside
10
+ // takes the whole query of the feed's address, so the parameter lang names the language of the
11
+ // feed, as i18n.js reads it, and a feed without one follows the language of the browser.
12
+
13
+ import './core/i18n.js';
14
+ import { frameScheme, parseFeed } from './export/feed-params.js';
15
+
16
+ // A page that has not started this long after it loaded, or has not answered this long, loads
17
+ // again. The check pauses while the window is hidden.
18
+ const START_MS = 20000;
19
+ const SILENT_MS = 30000;
20
+ const CHECK_MS = 5000;
21
+
22
+ const frame = document.getElementById('feed-frame');
23
+ const query = location.search.replace(/^\?/, '');
24
+ const { options } = parseFeed(query);
25
+ document.documentElement.style.background = options.background;
26
+ document.body.style.background = options.background;
27
+ // The frame takes the color scheme of the page inside it. Chrome paints an opaque backdrop behind
28
+ // a frame whose page has another scheme than the frame itself, which a transparent feed would show.
29
+ frame.style.colorScheme = frameScheme(options.theme);
30
+
31
+ // Places the frame and returns the ratio of the picture's pixels to the layout's.
32
+ function place() {
33
+ const { width, height, layout } = options;
34
+ const vw = window.innerWidth || width;
35
+ const vh = window.innerHeight || height;
36
+ const fit = Math.min(vw / width, vh / height) || 1;
37
+ const sx = (width * fit) / layout.width;
38
+ const sy = (height * fit) / layout.height;
39
+ frame.style.width = `${layout.width}px`;
40
+ frame.style.height = `${layout.height}px`;
41
+ frame.style.transform = `translate(${(vw - width * fit) / 2}px, ${(vh - height * fit) / 2}px) scale(${sx}, ${sy})`;
42
+ return Math.max(sx, sy) * (window.devicePixelRatio || 1);
43
+ }
44
+
45
+ let ratio = place();
46
+ let loadedAt = performance.now();
47
+
48
+ function load() {
49
+ loadedAt = performance.now();
50
+ frame.src = `./?feed=1${query ? `&${query}` : ''}&ratio=${ratio.toFixed(4)}#page=watch`;
51
+ }
52
+
53
+ function inner() {
54
+ try {
55
+ return frame.contentWindow && frame.contentWindow.cujuFeed;
56
+ } catch {
57
+ // A page of the browser's own, such as an error page, is out of reach.
58
+ return null;
59
+ }
60
+ }
61
+
62
+ frame.addEventListener('load', () => {
63
+ loadedAt = performance.now();
64
+ });
65
+
66
+ window.addEventListener('resize', () => {
67
+ ratio = place();
68
+ const feed = inner();
69
+ if (feed) {
70
+ feed.setRatio(ratio);
71
+ }
72
+ });
73
+
74
+ setInterval(() => {
75
+ if (document.hidden) {
76
+ return;
77
+ }
78
+ const feed = inner();
79
+ const now = performance.now();
80
+ const stale = feed
81
+ ? !!feed.broken || (feed.ready && Date.now() - feed.alive > SILENT_MS) || (!feed.ready && now - loadedAt > START_MS)
82
+ : now - loadedAt > START_MS;
83
+ if (stale) {
84
+ load();
85
+ }
86
+ }, CHECK_MS);
87
+
88
+ // Scripts that hold this window reach the feed's state here as well.
89
+ window.cujuFeedFrame = {
90
+ options,
91
+ get ratio() {
92
+ return ratio;
93
+ },
94
+ state: () => {
95
+ const app = frame.contentWindow && frame.contentWindow.cuju;
96
+ return app && app.feed ? app.feed.state() : null;
97
+ },
98
+ reload: load,
99
+ };
100
+
101
+ load();