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.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The graphics of a page as a picture, for the video export of the browser. The scoreboard, the
|
|
5
|
+
// banners, the captions and every other part of the page over the view are HTML, which a canvas
|
|
6
|
+
// cannot draw by itself, so each frame writes the shown part of the page as XHTML with the page's
|
|
7
|
+
// own styles into an SVG image, which the browser lays out and draws as it lays out the page. The
|
|
8
|
+
// view's own canvas stays empty in the picture and is drawn beneath it, so the backgrounds behind
|
|
9
|
+
// the view turn clear. CSS animations show at the moment they have reached, each animated value
|
|
10
|
+
// written onto its element. Pictures of the page become data addresses, since an SVG image loads
|
|
11
|
+
// nothing of its own.
|
|
12
|
+
|
|
13
|
+
const XHTML = 'http://www.w3.org/1999/xhtml';
|
|
14
|
+
const SVG = 'http://www.w3.org/2000/svg';
|
|
15
|
+
// Elements that never show or that the styles already hold.
|
|
16
|
+
const SKIPPED = new Set(['script', 'style', 'link', 'meta', 'title', 'template', 'noscript', 'head', 'iframe', 'audio', 'video']);
|
|
17
|
+
// Properties of an animation's keyframes that name no style.
|
|
18
|
+
const NOT_STYLES = new Set(['offset', 'easing', 'composite', 'computedOffset']);
|
|
19
|
+
|
|
20
|
+
const escapeText = (value) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
21
|
+
const escapeAttribute = (value) => escapeText(value).replace(/"/g, '"').replace(/\n/g, ' ');
|
|
22
|
+
// Characters that XML refuses, which an attribute or a text may still hold.
|
|
23
|
+
// eslint-disable-next-line no-control-regex
|
|
24
|
+
const clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, '');
|
|
25
|
+
|
|
26
|
+
// Reads a blob as a data address.
|
|
27
|
+
function dataAddress(blob) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const reader = new FileReader();
|
|
30
|
+
reader.onload = () => resolve(reader.result);
|
|
31
|
+
reader.onerror = () => reject(reader.error);
|
|
32
|
+
reader.readAsDataURL(blob);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Whether this browser draws an SVG image with HTML inside into a canvas that it can still read,
|
|
37
|
+
// which the export needs. Some browsers mark such a canvas as unreadable.
|
|
38
|
+
export async function readableHtmlPictures() {
|
|
39
|
+
try {
|
|
40
|
+
const svg = `<svg xmlns="${SVG}" width="4" height="4"><foreignObject width="4" height="4"><div xmlns="${XHTML}" style="width:4px;height:4px;background:#f00"></div></foreignObject></svg>`;
|
|
41
|
+
const image = new Image();
|
|
42
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
43
|
+
await image.decode();
|
|
44
|
+
const canvas = document.createElement('canvas');
|
|
45
|
+
canvas.width = 4;
|
|
46
|
+
canvas.height = 4;
|
|
47
|
+
const context = canvas.getContext('2d');
|
|
48
|
+
context.drawImage(image, 0, 0);
|
|
49
|
+
return context.getImageData(1, 1, 1, 1).data[0] > 128;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class DomPicture {
|
|
56
|
+
// win is the window of the page, view the canvas that is drawn beneath the picture, width and
|
|
57
|
+
// height the size of the page's layout in CSS pixels.
|
|
58
|
+
constructor(win, view, width, height) {
|
|
59
|
+
this.win = win;
|
|
60
|
+
this.doc = win.document;
|
|
61
|
+
this.view = view;
|
|
62
|
+
this.width = width;
|
|
63
|
+
this.height = height;
|
|
64
|
+
this.urls = new Map();
|
|
65
|
+
this.stylesKey = '';
|
|
66
|
+
this.styles = '';
|
|
67
|
+
this.markup = '';
|
|
68
|
+
this.image = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The text of every style sheet of the page, gathered again whenever a sheet or rule changes.
|
|
72
|
+
async gatherStyles() {
|
|
73
|
+
const sheets = [...this.doc.styleSheets];
|
|
74
|
+
let key = `${sheets.length}`;
|
|
75
|
+
for (const sheet of sheets) {
|
|
76
|
+
try {
|
|
77
|
+
key += `,${sheet.cssRules.length}`;
|
|
78
|
+
} catch {
|
|
79
|
+
key += ',x';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (key === this.stylesKey) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const texts = [];
|
|
86
|
+
for (const sheet of sheets) {
|
|
87
|
+
if (sheet.disabled) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let rules;
|
|
91
|
+
try {
|
|
92
|
+
rules = [...sheet.cssRules];
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const base = sheet.href || this.doc.baseURI;
|
|
97
|
+
let text = rules.map((rule) => rule.cssText).join('\n');
|
|
98
|
+
text = await this.inlineUrls(text, base);
|
|
99
|
+
const media = sheet.media && sheet.media.mediaText;
|
|
100
|
+
texts.push(media ? `@media ${media} {\n${text}\n}` : text);
|
|
101
|
+
}
|
|
102
|
+
this.styles = texts.join('\n');
|
|
103
|
+
this.stylesKey = key;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The data address of a file of the page, fetched once.
|
|
107
|
+
async dataOf(address) {
|
|
108
|
+
if (!address || address.startsWith('data:')) {
|
|
109
|
+
return address;
|
|
110
|
+
}
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(address, this.doc.baseURI).href;
|
|
114
|
+
} catch {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
if (!this.urls.has(url)) {
|
|
118
|
+
this.urls.set(url, (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const response = await this.win.fetch(url);
|
|
121
|
+
return response.ok ? await dataAddress(await response.blob()) : '';
|
|
122
|
+
} catch {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
})());
|
|
126
|
+
}
|
|
127
|
+
return this.urls.get(url);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The text with each url() of a file made a data address.
|
|
131
|
+
async inlineUrls(text, base) {
|
|
132
|
+
const found = [...text.matchAll(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g)];
|
|
133
|
+
if (!found.length) {
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
let out = text;
|
|
137
|
+
for (const match of found) {
|
|
138
|
+
const address = match[2];
|
|
139
|
+
if (address.startsWith('data:') || address.startsWith('#')) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let url;
|
|
143
|
+
try {
|
|
144
|
+
url = new URL(address, base).href;
|
|
145
|
+
} catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const data = await this.dataOf(url);
|
|
149
|
+
out = out.split(match[0]).join(data ? `url("${data}")` : 'none');
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The values that the animations of the page have reached, by element, as styles to write.
|
|
155
|
+
animated() {
|
|
156
|
+
const out = new Map();
|
|
157
|
+
for (const animation of this.doc.getAnimations()) {
|
|
158
|
+
const effect = animation.effect;
|
|
159
|
+
const target = effect && effect.target;
|
|
160
|
+
if (!target || effect.pseudoElement) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const names = new Set();
|
|
164
|
+
if (typeof animation.transitionProperty === 'string') {
|
|
165
|
+
names.add(animation.transitionProperty);
|
|
166
|
+
} else if (typeof effect.getKeyframes === 'function') {
|
|
167
|
+
for (const frame of effect.getKeyframes()) {
|
|
168
|
+
for (const name of Object.keys(frame)) {
|
|
169
|
+
if (!NOT_STYLES.has(name)) {
|
|
170
|
+
names.add(name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const style = this.win.getComputedStyle(target);
|
|
176
|
+
const list = out.get(target) || [];
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
const value = style.getPropertyValue(name);
|
|
179
|
+
if (value) {
|
|
180
|
+
list.push(`${name}: ${value} !important`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
out.set(target, list);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Writes an element and what it holds as XHTML, leaving out what does not show.
|
|
189
|
+
async write(node, out, context) {
|
|
190
|
+
if (node.nodeType === 3) {
|
|
191
|
+
out.push(escapeText(clean(node.data)));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (node.nodeType !== 1) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const tag = node.localName;
|
|
198
|
+
if (SKIPPED.has(tag) || (tag === 'dialog' && !node.open)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const svg = node.namespaceURI === SVG;
|
|
202
|
+
if (!svg && node.hidden && node !== this.doc.body) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// The parts of an SVG that only other parts use, its symbols and definitions, never show
|
|
206
|
+
// by themselves, so their own display says nothing.
|
|
207
|
+
const used = context.defs || (svg && (tag === 'defs' || tag === 'symbol'));
|
|
208
|
+
if (!used && this.win.getComputedStyle(node).display === 'none') {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let name = tag;
|
|
212
|
+
const attributes = [];
|
|
213
|
+
let extra = context.animated.get(node) || [];
|
|
214
|
+
if (context.clear.has(node)) {
|
|
215
|
+
extra = [...extra, 'background: transparent !important', 'background-image: none !important'];
|
|
216
|
+
}
|
|
217
|
+
let source = null;
|
|
218
|
+
if (tag === 'canvas') {
|
|
219
|
+
if (node !== this.view) {
|
|
220
|
+
// Another canvas shows as a picture of itself.
|
|
221
|
+
try {
|
|
222
|
+
source = node.toDataURL();
|
|
223
|
+
} catch {
|
|
224
|
+
source = '';
|
|
225
|
+
}
|
|
226
|
+
name = 'img';
|
|
227
|
+
extra = [...extra, `width: ${node.clientWidth}px`, `height: ${node.clientHeight}px`];
|
|
228
|
+
}
|
|
229
|
+
} else if (tag === 'img') {
|
|
230
|
+
source = await this.dataOf(node.currentSrc || node.getAttribute('src') || '');
|
|
231
|
+
}
|
|
232
|
+
for (const attribute of node.attributes) {
|
|
233
|
+
const key = attribute.name;
|
|
234
|
+
if (key.startsWith('on') || key === 'src' || key === 'srcset' || key === 'xmlns' || key.startsWith('xmlns:')) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (tag === 'canvas' && node !== this.view && (key === 'width' || key === 'height')) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
let value = attribute.value;
|
|
241
|
+
if (key === 'style' && value.includes('url(')) {
|
|
242
|
+
value = await this.inlineUrls(value, this.doc.baseURI);
|
|
243
|
+
}
|
|
244
|
+
if (key === 'style' && extra.length) {
|
|
245
|
+
value = `${value};${extra.join(';')}`;
|
|
246
|
+
extra = [];
|
|
247
|
+
}
|
|
248
|
+
if ((key === 'value' || key === 'checked') && tag === 'input') {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
attributes.push(` ${key}="${escapeAttribute(clean(value))}"`);
|
|
252
|
+
}
|
|
253
|
+
if (extra.length) {
|
|
254
|
+
attributes.push(` style="${escapeAttribute(extra.join(';'))}"`);
|
|
255
|
+
}
|
|
256
|
+
if (tag === 'input') {
|
|
257
|
+
if (node.type === 'checkbox' || node.type === 'radio') {
|
|
258
|
+
if (node.checked) {
|
|
259
|
+
attributes.push(' checked="checked"');
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
attributes.push(` value="${escapeAttribute(clean(String(node.value)))}"`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (source !== null) {
|
|
266
|
+
attributes.push(` src="${escapeAttribute(source)}"`);
|
|
267
|
+
}
|
|
268
|
+
if (svg && node.parentNode && node.parentNode.namespaceURI !== SVG) {
|
|
269
|
+
attributes.push(` xmlns="${SVG}" xmlns:xlink="http://www.w3.org/1999/xlink"`);
|
|
270
|
+
}
|
|
271
|
+
const children = name === 'img' ? [] : node.childNodes;
|
|
272
|
+
if (!children.length) {
|
|
273
|
+
out.push(`<${name}${attributes.join('')}/>`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
out.push(`<${name}${attributes.join('')}>`);
|
|
277
|
+
if (tag === 'textarea') {
|
|
278
|
+
out.push(escapeText(clean(node.value)));
|
|
279
|
+
} else {
|
|
280
|
+
const inner = used && !context.defs ? { ...context, defs: true } : context;
|
|
281
|
+
for (const child of children) {
|
|
282
|
+
await this.write(child, out, inner);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
out.push(`</${name}>`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The SVG markup of the page as it shows now.
|
|
289
|
+
async markupNow() {
|
|
290
|
+
await this.gatherStyles();
|
|
291
|
+
const root = this.doc.documentElement;
|
|
292
|
+
const clear = new Set();
|
|
293
|
+
for (let node = this.view; node; node = node.parentElement) {
|
|
294
|
+
clear.add(node);
|
|
295
|
+
}
|
|
296
|
+
const context = { animated: this.animated(), clear };
|
|
297
|
+
const body = [];
|
|
298
|
+
await this.write(this.doc.body, body, context);
|
|
299
|
+
const rootAttributes = [...root.attributes]
|
|
300
|
+
.filter((a) => !a.name.startsWith('xmlns') && a.name !== 'style')
|
|
301
|
+
.map((a) => ` ${a.name}="${escapeAttribute(clean(a.value))}"`).join('');
|
|
302
|
+
const rootStyle = escapeAttribute(clean(root.getAttribute('style') || ''));
|
|
303
|
+
// The root of the image is the SVG element, which the rules for the page's root then match,
|
|
304
|
+
// so it takes the attributes of the page's root as well.
|
|
305
|
+
const overrides = [
|
|
306
|
+
'*, *::before, *::after { animation: none !important; transition: none !important; caret-color: transparent !important; }',
|
|
307
|
+
':root, html, body { background: transparent !important; overflow: hidden !important; }',
|
|
308
|
+
].join('\n');
|
|
309
|
+
const { width, height } = this;
|
|
310
|
+
return `<svg xmlns="${SVG}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"${rootAttributes} style="${rootStyle}">`
|
|
311
|
+
+ `<foreignObject x="0" y="0" width="${width}" height="${height}">`
|
|
312
|
+
+ `<html xmlns="${XHTML}"${rootAttributes} style="${rootStyle};width:${width}px;height:${height}px">`
|
|
313
|
+
+ `<head><style>${escapeText(this.styles)}\n${overrides}</style></head>`
|
|
314
|
+
+ `${body.join('')}</html></foreignObject></svg>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Draws the graphics of the page over a context of the picture's size.
|
|
318
|
+
async draw(context, width, height) {
|
|
319
|
+
const markup = await this.markupNow();
|
|
320
|
+
if (markup !== this.markup || !this.image) {
|
|
321
|
+
const image = new Image();
|
|
322
|
+
image.decoding = 'sync';
|
|
323
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
|
|
324
|
+
await image.decode();
|
|
325
|
+
this.image = image;
|
|
326
|
+
this.markup = markup;
|
|
327
|
+
}
|
|
328
|
+
context.drawImage(this.image, 0, 0, width, height);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The page of the command cuju render video, export.html, which src/cuju/render/_export/video.py
|
|
5
|
+
// opens in a headless Chrome, Edge or Chromium. It runs the browser's own export of
|
|
6
|
+
// video-browser.js, so the command needs no program beyond the browser, and the command follows
|
|
7
|
+
// it through window.cujuVideoExport over the debugging protocol. It asks what the browser can
|
|
8
|
+
// encode, starts one export, reads its state, cancels it and hands the finished file back in
|
|
9
|
+
// pieces of base64, so neither side holds the file twice in memory.
|
|
10
|
+
|
|
11
|
+
import { BrowserExport, hasEncoder } from './video-browser.js';
|
|
12
|
+
import { readableHtmlPictures } from './dom-picture.js';
|
|
13
|
+
import { chooseEncoder, encoderCandidates } from './video-plan.js';
|
|
14
|
+
|
|
15
|
+
let job = null;
|
|
16
|
+
let state = { state: 'idle' };
|
|
17
|
+
let result = null;
|
|
18
|
+
|
|
19
|
+
// The encoder of each container at a size, or null where the browser has none.
|
|
20
|
+
async function support(width, height, fps, quality) {
|
|
21
|
+
const out = { mp4: null, webm: null, pictures: await readableHtmlPictures() };
|
|
22
|
+
if (!hasEncoder()) {
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
for (const container of ['mp4', 'webm']) {
|
|
26
|
+
const chosen = await chooseEncoder(encoderCandidates(width, height, fps, quality, container), (config) => VideoEncoder.isConfigSupported(config));
|
|
27
|
+
out[container] = chosen ? chosen.config.codec : null;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Starts an export. options holds the recording's match and title, the body of the export, the
|
|
33
|
+
// container, the language and the limits of frames and bytes.
|
|
34
|
+
function start(options) {
|
|
35
|
+
if (job) {
|
|
36
|
+
throw new Error('An export runs already.');
|
|
37
|
+
}
|
|
38
|
+
result = null;
|
|
39
|
+
state = { state: 'running', frame: 0, progress: 0 };
|
|
40
|
+
const current = new BrowserExport({
|
|
41
|
+
match: options.match,
|
|
42
|
+
title: options.title,
|
|
43
|
+
body: options.body,
|
|
44
|
+
lang: options.lang,
|
|
45
|
+
container: options.container,
|
|
46
|
+
maxFrames: options.maxFrames,
|
|
47
|
+
maxBytes: options.maxBytes,
|
|
48
|
+
report: (entry) => {
|
|
49
|
+
if (job === current) {
|
|
50
|
+
state = { ...entry };
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
job = current;
|
|
55
|
+
current.run().then((done) => {
|
|
56
|
+
result = done;
|
|
57
|
+
const { blob, ...facts } = done;
|
|
58
|
+
state = { ...facts, state: 'done' };
|
|
59
|
+
}, (error) => {
|
|
60
|
+
state = error && error.id === 'cancelled'
|
|
61
|
+
? { state: 'cancelled' }
|
|
62
|
+
: { state: 'failed', id: error && error.id, values: (error && error.values) || {}, message: String((error && error.message) || error) };
|
|
63
|
+
}).finally(() => {
|
|
64
|
+
job = null;
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The bytes of the finished file from start, at most length of them, as base64.
|
|
70
|
+
async function read(from, length) {
|
|
71
|
+
if (!result) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
const bytes = new Uint8Array(await result.blob.slice(from, from + length).arrayBuffer());
|
|
75
|
+
let text = '';
|
|
76
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
77
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
78
|
+
}
|
|
79
|
+
return btoa(text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
window.cujuVideoExport = {
|
|
83
|
+
support,
|
|
84
|
+
start,
|
|
85
|
+
read,
|
|
86
|
+
state: () => state,
|
|
87
|
+
cancel: () => {
|
|
88
|
+
if (job) {
|
|
89
|
+
job.cancel();
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
clear: () => {
|
|
94
|
+
result = null;
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The Copy feed address dialog of the Watch page. It writes the address of a clean feed, the page
|
|
5
|
+
// at /feed that a browser source of OBS or another streaming program shows, from what the feed
|
|
6
|
+
// follows, its size, the scale and safe area of its graphics, its background, whether it shows
|
|
7
|
+
// the graphics alone, its camera, its sound and its frame rate, and copies the address or opens
|
|
8
|
+
// the feed in a new tab. The server's live sources and tournaments join the sources it follows.
|
|
9
|
+
|
|
10
|
+
import { h } from '../teams/library.js';
|
|
11
|
+
import { icon, symbolOnly } from '../core/icons.js';
|
|
12
|
+
import { feedAddress, namedSources } from './feed-params.js';
|
|
13
|
+
import { serverText, t, tn } from '../core/i18n.js';
|
|
14
|
+
import { sizeFields } from './size-fields.js';
|
|
15
|
+
import { VIDEO_CAMERAS } from './video-range.js';
|
|
16
|
+
|
|
17
|
+
// The sources that the dialog offers beside the open match.
|
|
18
|
+
const FOLLOW = [
|
|
19
|
+
['auto', t('feed.dialog.follow.auto')],
|
|
20
|
+
['live', t('feed.dialog.follow.live')],
|
|
21
|
+
['latest', t('feed.dialog.follow.latest')],
|
|
22
|
+
['tournament', t('feed.dialog.follow.tournament')],
|
|
23
|
+
['queue', t('feed.dialog.follow.queue')],
|
|
24
|
+
];
|
|
25
|
+
const BACKGROUNDS = [
|
|
26
|
+
['black', t('feed.dialog.background.black')],
|
|
27
|
+
['transparent', t('feed.dialog.background.transparent')],
|
|
28
|
+
['green', t('feed.dialog.background.green')],
|
|
29
|
+
];
|
|
30
|
+
const RATES = [
|
|
31
|
+
['0', t('feed.dialog.rate.display')],
|
|
32
|
+
['30', t('feed.dialog.rate.option', { rate: 30 })],
|
|
33
|
+
['60', t('feed.dialog.rate.option', { rate: 60 })],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// The source of the open match, a recording of the library or a live source of this server.
|
|
37
|
+
export function openSource(source) {
|
|
38
|
+
if (!source || source.game) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (source.kind === 'match') {
|
|
42
|
+
return { value: `recording:${source.id}`, label: t('feed.dialog.follow.this_recording') };
|
|
43
|
+
}
|
|
44
|
+
if (source.kind === 'live' && source.live && !source.live.url) {
|
|
45
|
+
return { value: `live:${source.id}`, label: t('feed.dialog.follow.this_live') };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class FeedDialog {
|
|
51
|
+
constructor(app) {
|
|
52
|
+
this.app = app;
|
|
53
|
+
this.dialog = h('dialog', { class: 'feed-address', 'aria-labelledby': 'feed-address-title' });
|
|
54
|
+
document.getElementById('app').appendChild(this.dialog);
|
|
55
|
+
this.dialog.addEventListener('input', () => this.update());
|
|
56
|
+
this.dialog.addEventListener('change', () => this.update());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
open() {
|
|
60
|
+
if (this.dialog.open) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (document.pointerLockElement) {
|
|
64
|
+
document.exitPointerLock();
|
|
65
|
+
}
|
|
66
|
+
this.render();
|
|
67
|
+
this.dialog.showModal();
|
|
68
|
+
this.update();
|
|
69
|
+
this.addNamed();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Adds the live sources and tournaments of the server to the Follows list once it answers.
|
|
73
|
+
async addNamed() {
|
|
74
|
+
let rows;
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch('/api/feed', { cache: 'no-store' });
|
|
77
|
+
rows = res.ok ? await res.json() : [];
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!this.dialog.open) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const taken = [...this.source.options].map((o) => o.value);
|
|
85
|
+
// A row's label shows in the page's language when the server names its message.
|
|
86
|
+
const shown = (Array.isArray(rows) ? rows : []).map((row) => (row && row.label_id ? { ...row, label: serverText(row, 'label') } : row));
|
|
87
|
+
this.source.append(...namedSources(shown, taken).map(([value, label]) => h('option', { value }, label)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
close() {
|
|
91
|
+
if (this.dialog.open) {
|
|
92
|
+
this.dialog.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
render() {
|
|
97
|
+
const d = this.dialog;
|
|
98
|
+
const own = openSource(this.app.source);
|
|
99
|
+
const choices = [...(own ? [[own.value, own.label]] : []), ...FOLLOW];
|
|
100
|
+
const select = (id, entries) => h('select', { id }, entries.map(([value, label]) => h('option', { value }, label)));
|
|
101
|
+
this.source = select('fa-source', choices);
|
|
102
|
+
this.sizes = sizeFields('fa', { value: '1080p' });
|
|
103
|
+
this.background = select('fa-bg', BACKGROUNDS);
|
|
104
|
+
this.fps = select('fa-fps', RATES);
|
|
105
|
+
this.camera = select('fa-camera', VIDEO_CAMERAS.map((c) => [c.id, c.label]));
|
|
106
|
+
this.graphicsOnly = h('input', { id: 'fa-layer', type: 'checkbox' });
|
|
107
|
+
this.audio = h('input', { id: 'fa-audio', type: 'checkbox' });
|
|
108
|
+
this.loop = h('input', { id: 'fa-loop', type: 'checkbox' });
|
|
109
|
+
this.captions = h('input', { id: 'fa-captions', type: 'checkbox' });
|
|
110
|
+
this.output = h('input', { id: 'fa-url', type: 'text', readonly: true, spellcheck: 'false', 'aria-label': t('feed.dialog.address') });
|
|
111
|
+
this.note = h('p', { class: 've-note', role: 'status', 'aria-live': 'polite' });
|
|
112
|
+
this.openLink = h('a', { class: 'btn', target: '_blank', rel: 'noopener' }, t('feed.dialog.open'));
|
|
113
|
+
const field = (label, control) => h('label', { class: 've-field' }, h('span', {}, label), control);
|
|
114
|
+
d.replaceChildren(
|
|
115
|
+
h('div', { class: 've-head' },
|
|
116
|
+
h('h2', { id: 'feed-address-title' }, t('feed.dialog.title')),
|
|
117
|
+
h('button', { class: 'icon-btn', type: 'button', 'aria-label': t('feed.dialog.close'), title: t('feed.dialog.close.title'), onclick: () => this.close() }, icon('close'))),
|
|
118
|
+
h('p', { class: 'muted small' }, tn('feed.dialog.intro')),
|
|
119
|
+
h('div', { class: 've-grid' },
|
|
120
|
+
field(t('feed.dialog.follows'), this.source),
|
|
121
|
+
...this.sizes.fields,
|
|
122
|
+
field(t('feed.dialog.background'), this.background),
|
|
123
|
+
field(t('feed.dialog.rate'), this.fps),
|
|
124
|
+
field(t('feed.dialog.camera'), this.camera),
|
|
125
|
+
h('label', { class: 'check' }, this.graphicsOnly, t('feed.dialog.graphicsOnly')),
|
|
126
|
+
h('label', { class: 'check' }, this.audio, t('feed.dialog.audio')),
|
|
127
|
+
h('label', { class: 'check' }, this.loop, t('feed.dialog.loop')),
|
|
128
|
+
h('label', { class: 'check' }, this.captions, t('feed.dialog.captions'))),
|
|
129
|
+
this.output,
|
|
130
|
+
this.note,
|
|
131
|
+
h('div', { class: 've-actions' },
|
|
132
|
+
h('button', { class: 'btn primary', type: 'button', onclick: () => this.copy() }, icon('copy'), t('feed.dialog.copy')),
|
|
133
|
+
this.openLink,
|
|
134
|
+
h('button', { class: 'btn', type: 'button', ...symbolOnly(t('feed.dialog.close'), t('feed.dialog.close.title')), onclick: () => this.close() }, icon('close'))));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The feed's options from the fields, or null with the problem shown.
|
|
138
|
+
options() {
|
|
139
|
+
let size;
|
|
140
|
+
try {
|
|
141
|
+
size = this.sizes.read();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.note.textContent = err.message;
|
|
144
|
+
this.note.classList.add('error');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
this.note.classList.remove('error');
|
|
148
|
+
this.note.textContent = t('feed.dialog.size_note', { width: size.width, height: size.height });
|
|
149
|
+
return {
|
|
150
|
+
source: this.source.value,
|
|
151
|
+
width: size.width,
|
|
152
|
+
height: size.height,
|
|
153
|
+
scale: size.scale,
|
|
154
|
+
safe: size.safe,
|
|
155
|
+
bg: this.background.value,
|
|
156
|
+
fps: Number(this.fps.value),
|
|
157
|
+
camera: this.camera.value,
|
|
158
|
+
layer: this.graphicsOnly.checked ? 'graphics' : 'full',
|
|
159
|
+
audio: this.audio.checked,
|
|
160
|
+
loop: this.loop.checked,
|
|
161
|
+
captions: this.captions.checked,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
address() {
|
|
166
|
+
const options = this.options();
|
|
167
|
+
return options ? feedAddress(`${location.origin}${location.pathname.replace(/[^/]*$/, '')}`, options) : '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
update() {
|
|
171
|
+
const url = this.address();
|
|
172
|
+
this.output.value = url;
|
|
173
|
+
this.openLink.href = url || '#';
|
|
174
|
+
this.openLink.toggleAttribute('aria-disabled', !url);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async copy() {
|
|
178
|
+
const url = this.address();
|
|
179
|
+
if (!url) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await navigator.clipboard.writeText(url);
|
|
184
|
+
this.note.textContent = t('feed.dialog.copied');
|
|
185
|
+
} catch {
|
|
186
|
+
this.output.focus();
|
|
187
|
+
this.output.select();
|
|
188
|
+
this.note.replaceChildren(...tn('feed.dialog.copy_by_hand'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|