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,295 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // A small writer of MP4 files of one H.264 video track, for the video export of the page. It
5
+ // takes the encoded frames in the order of decoding with their times in microseconds and writes
6
+ // the boxes of ISO/IEC 14496-12 and 14496-15 that a player needs, the file type, the movie with
7
+ // its sample tables, and the frames in one media box. The movie comes before the frames, so a
8
+ // player starts before the whole file has arrived. The frames gather into blobs as they come, so
9
+ // a browser may keep a long video on its disk rather than in memory.
10
+
11
+ const textBytes = (text) => Uint8Array.from(text, (c) => c.charCodeAt(0));
12
+
13
+ // A writer of big-endian fields into a growing list of bytes.
14
+ class Bytes {
15
+ constructor() {
16
+ this.parts = [];
17
+ this.length = 0;
18
+ }
19
+
20
+ push(array) {
21
+ this.parts.push(array);
22
+ this.length += array.length;
23
+ return this;
24
+ }
25
+
26
+ u8(value) {
27
+ return this.push(Uint8Array.of(value & 255));
28
+ }
29
+
30
+ u16(value) {
31
+ const b = new Uint8Array(2);
32
+ new DataView(b.buffer).setUint16(0, value);
33
+ return this.push(b);
34
+ }
35
+
36
+ u32(value) {
37
+ const b = new Uint8Array(4);
38
+ new DataView(b.buffer).setUint32(0, value >>> 0);
39
+ return this.push(b);
40
+ }
41
+
42
+ i32(value) {
43
+ const b = new Uint8Array(4);
44
+ new DataView(b.buffer).setInt32(0, value);
45
+ return this.push(b);
46
+ }
47
+
48
+ u64(value) {
49
+ const b = new Uint8Array(8);
50
+ new DataView(b.buffer).setBigUint64(0, BigInt(value));
51
+ return this.push(b);
52
+ }
53
+
54
+ zeros(count) {
55
+ return this.push(new Uint8Array(count));
56
+ }
57
+
58
+ text(value) {
59
+ return this.push(textBytes(value));
60
+ }
61
+
62
+ bytes() {
63
+ const out = new Uint8Array(this.length);
64
+ let at = 0;
65
+ for (const part of this.parts) {
66
+ out.set(part, at);
67
+ at += part.length;
68
+ }
69
+ return out;
70
+ }
71
+ }
72
+
73
+ // A box of a type and a body that fill writes, or of children given as bytes.
74
+ function box(type, fill, ...children) {
75
+ const body = new Bytes();
76
+ if (fill) {
77
+ fill(body);
78
+ }
79
+ for (const child of children) {
80
+ body.push(child);
81
+ }
82
+ const out = new Bytes().u32(8 + body.length).text(type);
83
+ out.push(body.bytes());
84
+ return out.bytes();
85
+ }
86
+
87
+ // A full box, a box whose body starts with a version and flags.
88
+ function fullBox(type, version, flags, fill, ...children) {
89
+ return box(type, (b) => {
90
+ b.u8(version).u8(flags >> 16).u16(flags & 0xffff);
91
+ if (fill) {
92
+ fill(b);
93
+ }
94
+ }, ...children);
95
+ }
96
+
97
+ // The matrix of a track that shows as it was encoded.
98
+ const MATRIX = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
99
+
100
+ // Runs of equal values, as the sample tables count them.
101
+ function runs(values) {
102
+ const out = [];
103
+ for (const value of values) {
104
+ const last = out[out.length - 1];
105
+ if (last && last[1] === value) {
106
+ last[0]++;
107
+ } else {
108
+ out.push([1, value]);
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ // Gathers the encoded frames of one video track and writes the file.
115
+ export class Mp4Writer {
116
+ // width and height are the picture's, fps the frame rate, which sets the time scale.
117
+ constructor({ width, height, fps }) {
118
+ this.width = width;
119
+ this.height = height;
120
+ this.timescale = Math.max(1, Math.round(fps)) * 1000;
121
+ this.samples = [];
122
+ this.description = null;
123
+ this.pending = [];
124
+ this.pendingBytes = 0;
125
+ this.blobs = [];
126
+ this.bytes = 0;
127
+ }
128
+
129
+ // Adds one encoded frame, its bytes, its time and length in microseconds and whether it is a
130
+ // key frame. description is the AVC decoder configuration that the encoder gives with its
131
+ // first frame.
132
+ add(data, { timestamp, duration, key, description }) {
133
+ if (description && !this.description) {
134
+ this.description = new Uint8Array(description instanceof ArrayBuffer ? description : description.buffer.slice(description.byteOffset, description.byteOffset + description.byteLength));
135
+ }
136
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
137
+ this.samples.push({ size: bytes.length, timestamp, duration, key: !!key });
138
+ this.pending.push(bytes);
139
+ this.pendingBytes += bytes.length;
140
+ this.bytes += bytes.length;
141
+ // The frames gather into a blob every few megabytes.
142
+ if (this.pendingBytes >= 8 << 20 && typeof Blob === 'function') {
143
+ this.flushPending();
144
+ }
145
+ }
146
+
147
+ flushPending() {
148
+ if (this.pending.length) {
149
+ this.blobs.push(typeof Blob === 'function' ? new Blob(this.pending) : this.pending.slice());
150
+ this.pending = [];
151
+ this.pendingBytes = 0;
152
+ }
153
+ }
154
+
155
+ // The times of the samples in the units of the track, in the order of decoding, with their
156
+ // offsets to the order of showing.
157
+ timing() {
158
+ const scale = this.timescale / 1e6;
159
+ const shown = this.samples.map((s) => Math.round(s.timestamp * scale));
160
+ const decoded = [...shown].sort((a, b) => a - b);
161
+ const deltas = decoded.map((t, i) => (i + 1 < decoded.length ? decoded[i + 1] - t : Math.max(1, Math.round(this.samples[i].duration * scale))));
162
+ const offsets = shown.map((t, i) => t - decoded[i]);
163
+ const total = decoded.length ? decoded[decoded.length - 1] - decoded[0] + deltas[deltas.length - 1] : 0;
164
+ return { deltas, offsets, total, start: decoded.length ? decoded[0] : 0 };
165
+ }
166
+
167
+ // The file type, the movie and the head of the media box, with the frames placed after them.
168
+ head(large) {
169
+ if (!this.description) {
170
+ throw new Error('The encoder gave no decoder configuration of H.264.');
171
+ }
172
+ const { deltas, offsets, total } = this.timing();
173
+ const ftyp = box('ftyp', (b) => b.text('isom').u32(512).text('isom').text('iso2').text('avc1').text('mp41'));
174
+ const movieDuration = Math.round((total * 1000) / this.timescale);
175
+ const mdatHead = large ? 16 : 8;
176
+ // The offsets of the frames follow the movie, whose size changes with the offsets only when
177
+ // they outgrow 32 bits, so the movie is built again until its size holds.
178
+ const build = (first) => {
179
+ const count = this.samples.length;
180
+ let at = first;
181
+ const offsetsOfChunks = this.samples.map((s) => {
182
+ const here = at;
183
+ at += s.size;
184
+ return here;
185
+ });
186
+ const wide = at > 0xffffffff;
187
+ const stsd = fullBox('stsd', 0, 0, (b) => b.u32(1), box('avc1', (b) => {
188
+ b.zeros(6).u16(1).zeros(16).u16(this.width).u16(this.height)
189
+ .u32(0x00480000).u32(0x00480000).u32(0).u16(1).zeros(32).u16(0x0018).u16(0xffff);
190
+ }, box('avcC', (b) => b.push(this.description)),
191
+ box('pasp', (b) => b.u32(1).u32(1))));
192
+ const stts = fullBox('stts', 0, 0, (b) => {
193
+ const list = runs(deltas);
194
+ b.u32(list.length);
195
+ for (const [n, delta] of list) {
196
+ b.u32(n).u32(delta);
197
+ }
198
+ });
199
+ const tables = [stsd, stts];
200
+ if (offsets.some((o) => o !== 0)) {
201
+ tables.push(fullBox('ctts', 1, 0, (b) => {
202
+ const list = runs(offsets);
203
+ b.u32(list.length);
204
+ for (const [n, offset] of list) {
205
+ b.u32(n).i32(offset);
206
+ }
207
+ }));
208
+ }
209
+ if (this.samples.some((s) => !s.key)) {
210
+ tables.push(fullBox('stss', 0, 0, (b) => {
211
+ const keys = [];
212
+ this.samples.forEach((s, i) => s.key && keys.push(i + 1));
213
+ b.u32(keys.length);
214
+ for (const k of keys) {
215
+ b.u32(k);
216
+ }
217
+ }));
218
+ }
219
+ tables.push(fullBox('stsc', 0, 0, (b) => b.u32(1).u32(1).u32(1).u32(1)));
220
+ tables.push(fullBox('stsz', 0, 0, (b) => {
221
+ b.u32(0).u32(count);
222
+ for (const s of this.samples) {
223
+ b.u32(s.size);
224
+ }
225
+ }));
226
+ tables.push(wide
227
+ ? fullBox('co64', 0, 0, (b) => {
228
+ b.u32(count);
229
+ for (const o of offsetsOfChunks) {
230
+ b.u64(o);
231
+ }
232
+ })
233
+ : fullBox('stco', 0, 0, (b) => {
234
+ b.u32(count);
235
+ for (const o of offsetsOfChunks) {
236
+ b.u32(o);
237
+ }
238
+ }));
239
+ const stbl = box('stbl', null, ...tables);
240
+ const dinf = box('dinf', null, fullBox('dref', 0, 0, (b) => b.u32(1), fullBox('url ', 0, 1)));
241
+ const minf = box('minf', null, fullBox('vmhd', 0, 1, (b) => b.zeros(8)), dinf, stbl);
242
+ const hdlr = fullBox('hdlr', 0, 0, (b) => b.u32(0).text('vide').zeros(12).text('VideoHandler').u8(0));
243
+ const mdhd = fullBox('mdhd', 0, 0, (b) => b.u32(0).u32(0).u32(this.timescale).u32(total).u16(0x55c4).u16(0));
244
+ const mdia = box('mdia', null, mdhd, hdlr, minf);
245
+ const tkhd = fullBox('tkhd', 0, 3, (b) => {
246
+ b.u32(0).u32(0).u32(1).u32(0).u32(movieDuration).zeros(8).u16(0).u16(0).u16(0).u16(0);
247
+ for (const value of MATRIX) {
248
+ b.u32(value);
249
+ }
250
+ b.u32(this.width * 65536).u32(this.height * 65536);
251
+ });
252
+ const trak = box('trak', null, tkhd, mdia);
253
+ const mvhd = fullBox('mvhd', 0, 0, (b) => {
254
+ b.u32(0).u32(0).u32(1000).u32(movieDuration).u32(0x00010000).u16(0x0100).zeros(10);
255
+ for (const value of MATRIX) {
256
+ b.u32(value);
257
+ }
258
+ b.zeros(24).u32(2);
259
+ });
260
+ return box('moov', null, mvhd, trak);
261
+ };
262
+ let moov = build(ftyp.length + mdatHead);
263
+ for (let tries = 0; tries < 3; tries++) {
264
+ const next = build(ftyp.length + moov.length + mdatHead);
265
+ const settled = next.length === moov.length;
266
+ moov = next;
267
+ if (settled) {
268
+ break;
269
+ }
270
+ }
271
+ const mdatSize = mdatHead + this.bytes;
272
+ const mdat = new Bytes();
273
+ if (large) {
274
+ mdat.u32(1).text('mdat').u64(mdatSize);
275
+ } else {
276
+ mdat.u32(mdatSize).text('mdat');
277
+ }
278
+ return [ftyp, moov, mdat.bytes()];
279
+ }
280
+
281
+ // The finished file as a blob of the type video/mp4, or as a list of byte arrays where the
282
+ // runtime has no Blob.
283
+ finish() {
284
+ if (!this.samples.length) {
285
+ throw new Error('The video holds no frames.');
286
+ }
287
+ const large = 8 + this.bytes > 0xffffffff;
288
+ const head = this.head(large);
289
+ this.flushPending();
290
+ if (typeof Blob === 'function') {
291
+ return new Blob([...head, ...this.blobs], { type: 'video/mp4' });
292
+ }
293
+ return [...head, ...this.blobs.flat()];
294
+ }
295
+ }
@@ -0,0 +1,313 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The parts of photo mode that need no page and no three.js, the film looks, the frame guides,
5
+ // the sizes of a saved photo, its file name, the checked settings and the reading of a
6
+ // controller. photo.js draws and steers with them, and tests/web/photo.mjs tests them.
7
+
8
+ import { SIZE_ALIASES, SIZE_PRESETS } from './sizes.js';
9
+
10
+ // A look grades the finished picture. Exposure is in stops, temperature and tint lean the white
11
+ // balance from minus one to one, contrast and saturation scale around their neutral value of one,
12
+ // fade lifts the blacks, mono turns the picture to black and white, shadows and highlights tint
13
+ // the dark and the bright parts, and gain scales each channel.
14
+ export const LOOKS = Object.freeze([
15
+ look('natural', 'Natural', {}),
16
+ look('warm', 'Warm', {
17
+ exposure: 0.05, temperature: 0.45, tint: 0.05, contrast: 1.05, saturation: 1.08,
18
+ highlights: [0.03, 0.015, -0.01],
19
+ }),
20
+ look('cold', 'Cold', {
21
+ exposure: -0.03, temperature: -0.45, contrast: 1.06, saturation: 0.9,
22
+ shadows: [-0.01, 0.01, 0.04],
23
+ }),
24
+ look('noir', 'Noir', {
25
+ exposure: -0.05, contrast: 1.4, saturation: 0, mono: 1, gain: [1.04, 1.04, 1.04],
26
+ }),
27
+ look('vintage', 'Vintage', {
28
+ temperature: 0.2, contrast: 0.9, saturation: 0.7, fade: 0.08,
29
+ shadows: [0.02, 0.03, 0.01], highlights: [0.03, 0.02, -0.03], gain: [1.02, 0.99, 0.88],
30
+ }),
31
+ look('vivid', 'Vivid', {
32
+ exposure: 0.05, contrast: 1.15, saturation: 1.4,
33
+ }),
34
+ look('matchday', 'Matchday', {
35
+ exposure: 0.04, temperature: 0.1, contrast: 1.12, saturation: 1.2,
36
+ shadows: [-0.02, 0.01, 0.035], highlights: [0.035, 0.015, -0.015],
37
+ }),
38
+ ]);
39
+
40
+ function look(id, label, values) {
41
+ return Object.freeze({
42
+ id,
43
+ label,
44
+ exposure: 0,
45
+ temperature: 0,
46
+ tint: 0,
47
+ contrast: 1,
48
+ saturation: 1,
49
+ fade: 0,
50
+ mono: 0,
51
+ shadows: [0, 0, 0],
52
+ highlights: [0, 0, 0],
53
+ gain: [1, 1, 1],
54
+ ...values,
55
+ });
56
+ }
57
+
58
+ export function lookById(id) {
59
+ return LOOKS.find((l) => l.id === id) || LOOKS[0];
60
+ }
61
+
62
+ // The look after or before the given one, round the list.
63
+ export function nextLook(id, step = 1) {
64
+ const i = Math.max(0, LOOKS.findIndex((l) => l.id === id));
65
+ return LOOKS[(((i + step) % LOOKS.length) + LOOKS.length) % LOOKS.length].id;
66
+ }
67
+
68
+ export const GUIDES = Object.freeze([
69
+ { id: 'off', label: 'Off' },
70
+ { id: 'thirds', label: 'Rule of thirds' },
71
+ { id: 'golden', label: 'Golden ratio' },
72
+ { id: 'center', label: 'Center cross' },
73
+ { id: 'crop-16-9', label: 'Crop frame, 16 by 9' },
74
+ { id: 'crop-4-5', label: 'Crop frame, 4 by 5' },
75
+ ]);
76
+
77
+ export function nextGuide(id, step = 1) {
78
+ const i = Math.max(0, GUIDES.findIndex((g) => g.id === id));
79
+ return GUIDES[(((i + step) % GUIDES.length) + GUIDES.length) % GUIDES.length].id;
80
+ }
81
+
82
+ // The window's own size and the shared sizes of sizes.js, each named by its width and height.
83
+ export const RESOLUTIONS = Object.freeze([
84
+ { id: 'window', label: 'Window size' },
85
+ ...SIZE_PRESETS.map((p) => Object.freeze({ id: `${p.width}x${p.height}`, width: p.width, height: p.height, label: p.label, preset: p.id })),
86
+ ]);
87
+
88
+ // The smallest and largest side of a saved photo.
89
+ export const MIN_SIDE = 16;
90
+ export const MAX_SIDE = 8192;
91
+
92
+ // The width and height of a photo for a choice of RESOLUTIONS, a string such as '1280x720', or
93
+ // an object with width and height. Window is the window's own size. A size out of range throws.
94
+ export function resolveResolution(choice, windowWidth, windowHeight, max = MAX_SIDE) {
95
+ let width;
96
+ let height;
97
+ if (choice == null || choice === 'window') {
98
+ width = Math.round(windowWidth);
99
+ height = Math.round(windowHeight);
100
+ } else if (typeof choice === 'object') {
101
+ ({ width, height } = choice);
102
+ } else {
103
+ const named = String(choice).toLowerCase();
104
+ const known = RESOLUTIONS.find((r) => r.id === choice || r.preset === (SIZE_ALIASES[named] || named));
105
+ const m = String(choice).match(/^(\d+)\s*[x×]\s*(\d+)$/i);
106
+ if (known) {
107
+ ({ width, height } = known);
108
+ } else if (m) {
109
+ width = Number(m[1]);
110
+ height = Number(m[2]);
111
+ } else {
112
+ throw new RangeError(`Unknown resolution ${JSON.stringify(choice)}, use window, a width and height such as 1280x720, or a size of ${SIZE_PRESETS.map((p) => p.id).join(', ')}.`);
113
+ }
114
+ }
115
+ const limit = Math.min(MAX_SIDE, Math.max(MIN_SIDE, Math.floor(max)));
116
+ for (const [name, value] of [['width', width], ['height', height]]) {
117
+ if (!Number.isInteger(value) || value < MIN_SIDE || value > limit) {
118
+ throw new RangeError(`The ${name} of a photo is a whole number from ${MIN_SIDE} to ${limit}, not ${value}.`);
119
+ }
120
+ }
121
+ return { width, height };
122
+ }
123
+
124
+ // The part of a view of the given size that a photo of another shape shows, centered. A narrower
125
+ // photo keeps the height of the view and a wider one keeps its width.
126
+ export function captureRegion(viewWidth, viewHeight, outWidth, outHeight) {
127
+ return fitAspect({ x: 0, y: 0, width: viewWidth, height: viewHeight }, outWidth / outHeight);
128
+ }
129
+
130
+ // The vertical field of view in degrees of a photo of another shape, so it shows exactly the
131
+ // region of captureRegion.
132
+ export function captureFov(fov, viewAspect, outAspect) {
133
+ if (!(outAspect > viewAspect)) {
134
+ return fov;
135
+ }
136
+ const half = Math.tan((fov * Math.PI) / 360) * (viewAspect / outAspect);
137
+ return (Math.atan(half) * 360) / Math.PI;
138
+ }
139
+
140
+ // The largest rectangle of the given aspect inside a rectangle, centered in it.
141
+ export function fitAspect(rect, aspect) {
142
+ if (rect.width / rect.height > aspect) {
143
+ const width = rect.height * aspect;
144
+ return { x: rect.x + (rect.width - width) / 2, y: rect.y, width, height: rect.height };
145
+ }
146
+ const height = rect.width / aspect;
147
+ return { x: rect.x, y: rect.y + (rect.height - height) / 2, width: rect.width, height };
148
+ }
149
+
150
+ const GOLDEN = (Math.sqrt(5) - 1) / 2;
151
+
152
+ // The lines of a guide over a view of the given size, each [x1, y1, x2, y2] in pixels, and the
153
+ // frame, the part of the view that a saved photo or a crop frame keeps, or null when that is the
154
+ // whole view. Output is the size of the photo, or null for the window's size.
155
+ export function guideGeometry(kind, width, height, output = null) {
156
+ const region = output ? captureRegion(width, height, output.width, output.height) : { x: 0, y: 0, width, height };
157
+ let frame = region;
158
+ if (kind === 'crop-16-9' || kind === 'crop-4-5') {
159
+ frame = fitAspect(region, kind === 'crop-16-9' ? 16 / 9 : 4 / 5);
160
+ }
161
+ const lines = [];
162
+ const cut = (fractions) => {
163
+ for (const f of fractions) {
164
+ const x = frame.x + f * frame.width;
165
+ const y = frame.y + f * frame.height;
166
+ lines.push([x, frame.y, x, frame.y + frame.height]);
167
+ lines.push([frame.x, y, frame.x + frame.width, y]);
168
+ }
169
+ };
170
+ if (kind === 'thirds') {
171
+ cut([1 / 3, 2 / 3]);
172
+ } else if (kind === 'golden') {
173
+ cut([1 - GOLDEN, GOLDEN]);
174
+ } else if (kind === 'center') {
175
+ const cx = frame.x + frame.width / 2;
176
+ const cy = frame.y + frame.height / 2;
177
+ const arm = Math.min(frame.width, frame.height) * 0.05;
178
+ lines.push([cx - arm, cy, cx + arm, cy], [cx, cy - arm, cx, cy + arm]);
179
+ }
180
+ const whole = frame.width >= width - 0.5 && frame.height >= height - 0.5;
181
+ return { kind, lines, frame: whole ? null : frame };
182
+ }
183
+
184
+ // The name of a saved photo, from the recording or source and the frame shown.
185
+ export function photoFileName(recording, frame) {
186
+ const slug = String(recording ?? '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
187
+ .replace(/-{2,}/g, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 64).replace(/[-.]+$/, '') || 'view';
188
+ const part = Number.isInteger(frame) && frame >= 0 ? `-${frame}` : '';
189
+ return `cuju-photo-${slug}${part}.png`;
190
+ }
191
+
192
+ // The focus distance in meters and its slider, from 0 to 1000 on a logarithmic scale.
193
+ export const FOCUS_MIN = 0.5;
194
+ export const FOCUS_MAX = 300;
195
+
196
+ export function focusToSlider(meters) {
197
+ const m = Math.min(FOCUS_MAX, Math.max(FOCUS_MIN, meters));
198
+ return Math.round((1000 * Math.log(m / FOCUS_MIN)) / Math.log(FOCUS_MAX / FOCUS_MIN));
199
+ }
200
+
201
+ export function sliderToFocus(value) {
202
+ const v = Math.min(1000, Math.max(0, Number(value) || 0));
203
+ return Math.round(FOCUS_MIN * Math.pow(FOCUS_MAX / FOCUS_MIN, v / 1000) * 10) / 10;
204
+ }
205
+
206
+ export const DEFAULTS = Object.freeze({
207
+ look: 'natural',
208
+ vignette: 0.2,
209
+ grain: 0.05,
210
+ focus: 'auto',
211
+ aperture: 0.15,
212
+ fov: 40,
213
+ roll: 0,
214
+ guide: 'off',
215
+ resolution: 'window',
216
+ });
217
+
218
+ // The ranges of the numeric settings, field of view and roll in degrees.
219
+ export const RANGES = Object.freeze({
220
+ vignette: [0, 1],
221
+ grain: [0, 1],
222
+ aperture: [0, 1],
223
+ fov: [10, 100],
224
+ roll: [-90, 90],
225
+ });
226
+
227
+ // The settings after a change, each value checked. An unknown name or a value out of place
228
+ // throws with a message that says what fits, so a script learns its mistake. A number out of its
229
+ // range throws when strict, as for a script, and is held to the range otherwise, as for the keys
230
+ // and the controller that step a setting past its end.
231
+ export function normalizeSettings(current, patch, { strict = false } = {}) {
232
+ const next = { ...DEFAULTS, ...current };
233
+ if (patch == null) {
234
+ return next;
235
+ }
236
+ if (typeof patch !== 'object') {
237
+ throw new TypeError('Photo settings are an object, such as { look: \'noir\', vignette: 0.4 }.');
238
+ }
239
+ for (const [key, value] of Object.entries(patch)) {
240
+ if (key === 'look') {
241
+ if (!LOOKS.some((l) => l.id === value)) {
242
+ throw new RangeError(`Unknown look ${JSON.stringify(value)}, use one of ${LOOKS.map((l) => l.id).join(', ')}.`);
243
+ }
244
+ next.look = value;
245
+ } else if (key === 'guide') {
246
+ if (!GUIDES.some((g) => g.id === value)) {
247
+ throw new RangeError(`Unknown guide ${JSON.stringify(value)}, use one of ${GUIDES.map((g) => g.id).join(', ')}.`);
248
+ }
249
+ next.guide = value;
250
+ } else if (key === 'focus') {
251
+ if (value === 'auto' || value === null) {
252
+ next.focus = 'auto';
253
+ } else if (typeof value === 'number' && Number.isFinite(value) && (!strict || (value >= FOCUS_MIN && value <= FOCUS_MAX))) {
254
+ next.focus = Math.min(FOCUS_MAX, Math.max(FOCUS_MIN, value));
255
+ } else {
256
+ throw new RangeError(`The focus is 'auto' or a distance in meters from ${FOCUS_MIN} to ${FOCUS_MAX}.`);
257
+ }
258
+ } else if (key === 'resolution') {
259
+ const size = resolveResolution(value, 1920, 1080);
260
+ // A preset's name keeps the identifier of its width and height, as the panel lists it.
261
+ const listed = RESOLUTIONS.find((r) => r.width === size.width && r.height === size.height);
262
+ next.resolution = typeof value === 'object' ? { width: value.width, height: value.height }
263
+ : typeof value === 'string' && value !== 'window' && listed ? listed.id : value;
264
+ } else if (key in RANGES) {
265
+ const [lo, hi] = RANGES[key];
266
+ if (typeof value !== 'number' || !Number.isFinite(value) || (strict && (value < lo || value > hi))) {
267
+ throw new RangeError(`The ${key} is a number from ${lo} to ${hi}, not ${JSON.stringify(value)}.`);
268
+ }
269
+ next[key] = Math.min(hi, Math.max(lo, value));
270
+ } else {
271
+ throw new RangeError(`Unknown photo setting ${JSON.stringify(key)}, use ${Object.keys(DEFAULTS).join(', ')}.`);
272
+ }
273
+ }
274
+ return next;
275
+ }
276
+
277
+ // The buttons of a controller with the standard mapping that photo mode uses, by index.
278
+ export const PAD_BUTTONS = Object.freeze({
279
+ 0: 'save', 1: 'leave', 2: 'interface', 3: 'guide', 4: 'previousLook', 5: 'nextLook',
280
+ });
281
+
282
+ function stick(x, y, deadzone) {
283
+ const length = Math.hypot(x, y);
284
+ if (!(length > deadzone)) {
285
+ return [0, 0];
286
+ }
287
+ const scale = Math.min(1, (length - deadzone) / (1 - deadzone)) / length;
288
+ return [x * scale, y * scale];
289
+ }
290
+
291
+ const buttonValue = (b) => (typeof b === 'number' ? b : (b ? (b.pressed ? Math.max(0.5, b.value || 1) : b.value || 0) : 0));
292
+
293
+ // What a controller asks for, from its axes and buttons and the buttons held at the last poll.
294
+ // Move and look are the sticks past the dead zone, rise is the right trigger less the left one,
295
+ // zoom and roll are the vertical and horizontal pad, and pressed names the buttons of PAD_BUTTONS
296
+ // pressed since the last poll.
297
+ export function readPad(pad, before = [], deadzone = 0.15) {
298
+ const axes = (pad && pad.axes) || [];
299
+ const raw = (pad && pad.buttons) || [];
300
+ const value = (i) => buttonValue(raw[i]);
301
+ const buttons = raw.map((b, i) => value(i) > 0.5);
302
+ const pressed = Object.entries(PAD_BUTTONS)
303
+ .filter(([i]) => buttons[i] && !before[i]).map(([, name]) => name);
304
+ return {
305
+ move: stick(axes[0] || 0, axes[1] || 0, deadzone),
306
+ look: stick(axes[2] || 0, axes[3] || 0, deadzone),
307
+ rise: value(7) - value(6),
308
+ zoom: (buttons[13] ? 1 : 0) - (buttons[12] ? 1 : 0),
309
+ roll: (buttons[15] ? 1 : 0) - (buttons[14] ? 1 : 0),
310
+ pressed,
311
+ buttons,
312
+ };
313
+ }