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,275 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Frame storage and the playback clock, shared by recordings and live streams.
|
|
5
|
+
// Positions on the timeline are absolute frame numbers, so live frames dropped from the front
|
|
6
|
+
// do not move anything, the first kept frame is just no longer zero.
|
|
7
|
+
|
|
8
|
+
import { Annotations } from './annotation-data.js';
|
|
9
|
+
import { findGoals, isCut } from './format.js';
|
|
10
|
+
|
|
11
|
+
// A recording keeps all its frames. A live store is a ring of at most limit frames, so a long
|
|
12
|
+
// stream drops its oldest frames in constant time. annotations, the records of a recording or []
|
|
13
|
+
// for a stream that sends them, become the store's Annotations, null without them.
|
|
14
|
+
export class FrameStore {
|
|
15
|
+
constructor(header, frames = [], limit = Infinity, annotations = null) {
|
|
16
|
+
this.header = header;
|
|
17
|
+
this.limit = limit;
|
|
18
|
+
this.annotations = annotations ? new Annotations(annotations) : null;
|
|
19
|
+
this.ring = Number.isFinite(limit) ? new Array(limit) : frames;
|
|
20
|
+
this.head = 0;
|
|
21
|
+
this.count = frames.length;
|
|
22
|
+
this.base = 0;
|
|
23
|
+
if (this.ring !== frames) {
|
|
24
|
+
frames.slice(-limit).forEach((f, i) => {
|
|
25
|
+
this.ring[i] = f;
|
|
26
|
+
});
|
|
27
|
+
this.count = Math.min(frames.length, limit);
|
|
28
|
+
this.base = frames.length - this.count;
|
|
29
|
+
}
|
|
30
|
+
this.goals = findGoals(frames).filter((g) => g.index >= this.base);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get first() {
|
|
34
|
+
return this.base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get last() {
|
|
38
|
+
return this.base + this.count - 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get size() {
|
|
42
|
+
return this.count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(index) {
|
|
46
|
+
const i = index - this.base;
|
|
47
|
+
if (i < 0 || i >= this.count) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return Number.isFinite(this.limit) ? this.ring[(this.head + i) % this.limit] : this.ring[i];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
push(frame) {
|
|
54
|
+
const previous = this.count ? this.get(this.last) : null;
|
|
55
|
+
if (!Number.isFinite(this.limit)) {
|
|
56
|
+
this.ring.push(frame);
|
|
57
|
+
this.count++;
|
|
58
|
+
} else if (this.count < this.limit) {
|
|
59
|
+
this.ring[(this.head + this.count) % this.limit] = frame;
|
|
60
|
+
this.count++;
|
|
61
|
+
} else {
|
|
62
|
+
this.ring[this.head] = frame;
|
|
63
|
+
this.head = (this.head + 1) % this.limit;
|
|
64
|
+
this.base++;
|
|
65
|
+
if (this.goals.length && this.goals[0].index < this.base) {
|
|
66
|
+
this.goals = this.goals.filter((g) => g.index >= this.base);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (previous && (frame.scoreLeft > previous.scoreLeft || frame.scoreRight > previous.scoreRight)) {
|
|
70
|
+
this.goals.push({
|
|
71
|
+
index: this.last,
|
|
72
|
+
team: frame.scoreLeft > previous.scoreLeft ? 0 : 1,
|
|
73
|
+
step: frame.step,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A live view plays three seconds behind the newest frame, which absorbs network jitter and
|
|
80
|
+
// leaves the restart transitions frames to look ahead into, the taker and the first step.
|
|
81
|
+
export const LIVE_DELAY_FRAMES = 30;
|
|
82
|
+
|
|
83
|
+
export class Timeline {
|
|
84
|
+
constructor(store, live) {
|
|
85
|
+
this.store = store;
|
|
86
|
+
this.live = live;
|
|
87
|
+
this.sps = store.header.steps_per_second || 10;
|
|
88
|
+
this.t = store.first;
|
|
89
|
+
this.playing = false;
|
|
90
|
+
this.direction = 1;
|
|
91
|
+
this.speed = 1;
|
|
92
|
+
this.following = live;
|
|
93
|
+
this.onEnd = null;
|
|
94
|
+
this.jumps = 0;
|
|
95
|
+
// Frames the view keeps behind the newest one, how strongly it leans towards that, and
|
|
96
|
+
// the gap past which it jumps. A game that a person plays keeps a small delay.
|
|
97
|
+
this.delay = LIVE_DELAY_FRAMES;
|
|
98
|
+
this.lean = 0.12;
|
|
99
|
+
this.jump = 90;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get first() {
|
|
103
|
+
return this.store.first;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get last() {
|
|
107
|
+
return Math.max(this.store.first, this.store.last);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get index() {
|
|
111
|
+
return Math.floor(this.t);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Seconds between the newest frame and the frame on screen.
|
|
115
|
+
get behindLive() {
|
|
116
|
+
return Math.max(0, (this.store.last - this.t) / this.sps);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
update(dt) {
|
|
120
|
+
if (this.store.size === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Up to a second per update, so playback keeps real time even at a few frames a second.
|
|
124
|
+
dt = Math.min(Math.max(0, dt), 1);
|
|
125
|
+
if (this.live && this.following) {
|
|
126
|
+
this.followLive(dt);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!this.playing) {
|
|
130
|
+
this.clamp();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.t += this.direction * this.speed * this.sps * dt;
|
|
134
|
+
if (this.t >= this.last) {
|
|
135
|
+
this.t = this.last;
|
|
136
|
+
if (this.live) {
|
|
137
|
+
this.following = true;
|
|
138
|
+
this.speed = 1;
|
|
139
|
+
} else {
|
|
140
|
+
this.playing = false;
|
|
141
|
+
if (this.onEnd) {
|
|
142
|
+
this.onEnd();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else if (this.t <= this.first) {
|
|
146
|
+
this.t = this.first;
|
|
147
|
+
this.playing = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Plays at real time a few frames behind the newest one, which absorbs network jitter.
|
|
152
|
+
// The rate leans towards the target so bursts and gaps even out without visible jumps.
|
|
153
|
+
followLive(dt) {
|
|
154
|
+
this.playing = true;
|
|
155
|
+
this.direction = 1;
|
|
156
|
+
const target = this.last - this.delay;
|
|
157
|
+
// A restart's transition and pre-roll hold play for a few seconds, which the lean below
|
|
158
|
+
// makes up for, only a longer gap jumps.
|
|
159
|
+
if (target - this.t > this.jump || this.t < this.first) {
|
|
160
|
+
this.t = Math.max(this.first, target);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const rate = Math.min(1.6, Math.max(0.6, 1 + (target - this.t) * this.lean));
|
|
164
|
+
this.t = Math.min(this.last, this.t + rate * this.sps * dt);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
clamp() {
|
|
168
|
+
this.t = Math.min(this.last, Math.max(this.first, this.t));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Returns the two frames around t and the blend weight, zero across a cut.
|
|
172
|
+
sample() {
|
|
173
|
+
this.clamp();
|
|
174
|
+
const i = Math.floor(this.t);
|
|
175
|
+
const a = this.store.get(i);
|
|
176
|
+
const b = i + 1 <= this.store.last ? this.store.get(i + 1) : null;
|
|
177
|
+
let w = this.t - i;
|
|
178
|
+
if (!b || isCut(a, b)) {
|
|
179
|
+
w = 0;
|
|
180
|
+
}
|
|
181
|
+
return { index: i, a, b: w > 0 ? b : null, w };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
play(direction = 1) {
|
|
185
|
+
if (this.store.size === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.direction = direction;
|
|
189
|
+
if (!this.live && direction > 0 && this.t >= this.last) {
|
|
190
|
+
this.t = this.first;
|
|
191
|
+
}
|
|
192
|
+
if (direction < 0 && this.t <= this.first) {
|
|
193
|
+
this.t = this.last;
|
|
194
|
+
}
|
|
195
|
+
if (this.live && direction < 0) {
|
|
196
|
+
this.following = false;
|
|
197
|
+
}
|
|
198
|
+
this.playing = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pause() {
|
|
202
|
+
this.playing = false;
|
|
203
|
+
this.following = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
toggle() {
|
|
207
|
+
if (this.playing) {
|
|
208
|
+
this.pause();
|
|
209
|
+
} else {
|
|
210
|
+
this.play(this.direction);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
seek(t) {
|
|
215
|
+
const from = this.t;
|
|
216
|
+
this.t = t;
|
|
217
|
+
this.clamp();
|
|
218
|
+
// Seeks that move more than two seconds count as jumps, which the lessons read.
|
|
219
|
+
if (Math.abs(this.t - from) > 2 * this.sps) {
|
|
220
|
+
this.jumps += 1;
|
|
221
|
+
}
|
|
222
|
+
if (this.live && this.t < this.last - this.delay - 1) {
|
|
223
|
+
this.following = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Steps whole frames from where the view is. Forwards counts from the frame shown and
|
|
228
|
+
// backwards from the next one, so a position between two frames lands on the nearer one in
|
|
229
|
+
// the direction of the step and never skips a frame.
|
|
230
|
+
stepFrames(n) {
|
|
231
|
+
this.pause();
|
|
232
|
+
const from = n > 0 ? Math.floor(this.t + 1e-6) : Math.ceil(this.t - 1e-6);
|
|
233
|
+
this.seek(from + n);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
skipSeconds(seconds) {
|
|
237
|
+
const wasFollowing = this.following;
|
|
238
|
+
this.seek(this.t + seconds * this.sps);
|
|
239
|
+
if (this.live && !wasFollowing && this.t >= this.last - this.delay) {
|
|
240
|
+
this.goLive();
|
|
241
|
+
} else if (wasFollowing && seconds < 0) {
|
|
242
|
+
this.following = false;
|
|
243
|
+
this.playing = true;
|
|
244
|
+
this.direction = 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
goLive() {
|
|
249
|
+
this.following = true;
|
|
250
|
+
this.playing = true;
|
|
251
|
+
this.direction = 1;
|
|
252
|
+
this.speed = 1;
|
|
253
|
+
this.t = Math.max(this.first, this.last - this.delay);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
restart() {
|
|
257
|
+
this.seek(this.first);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Goal before or after the current position, by frame number. Lead is how far before a goal
|
|
261
|
+
// the jumps land, so a press right after a jump moves on to the next goal.
|
|
262
|
+
goalNear(direction, lead = 0) {
|
|
263
|
+
const goals = this.store.goals;
|
|
264
|
+
const here = this.t + lead;
|
|
265
|
+
if (direction > 0) {
|
|
266
|
+
return goals.find((g) => g.index > here + 1) || null;
|
|
267
|
+
}
|
|
268
|
+
for (let k = goals.length - 1; k >= 0; k--) {
|
|
269
|
+
if (goals[k].index < here - 1) {
|
|
270
|
+
return goals[k];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Team artwork from the server, the flat shirt front and back, and the four sponsor boards.
|
|
5
|
+
|
|
6
|
+
import { api } from '../core/api.js';
|
|
7
|
+
|
|
8
|
+
export const SHIRT_SLOTS = ['shirt-front', 'shirt-back'];
|
|
9
|
+
export const SPONSOR_SLOTS = ['sponsor-1', 'sponsor-2', 'sponsor-3', 'sponsor-4'];
|
|
10
|
+
export const SPONSOR_SIZE = { width: 1024, height: 128 };
|
|
11
|
+
// The shirt panels cover half the kit texture across and 0.41 of it down, in kit UV space.
|
|
12
|
+
export const SHIRT_SIZE = { width: 512, height: 420 };
|
|
13
|
+
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
let generation = 0;
|
|
16
|
+
|
|
17
|
+
// Forgets loaded artwork, after an edit.
|
|
18
|
+
export function invalidateArt() {
|
|
19
|
+
generation++;
|
|
20
|
+
cache.clear();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function bitmap(id, slot) {
|
|
24
|
+
try {
|
|
25
|
+
return await createImageBitmap(await api.image(id, slot));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn('team image not loaded', id, slot, err);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Returns the front, back and sponsors of a registry team, missing pieces null or left out.
|
|
33
|
+
export function loadTeamArt(team) {
|
|
34
|
+
if (!team || !Array.isArray(team.images) || team.images.length === 0) {
|
|
35
|
+
return Promise.resolve({ front: null, back: null, sponsors: [] });
|
|
36
|
+
}
|
|
37
|
+
const key = `${generation}:${team.id}:${team.images.slice().sort().join(',')}`;
|
|
38
|
+
if (!cache.has(key)) {
|
|
39
|
+
const has = (slot) => team.images.includes(slot);
|
|
40
|
+
cache.set(key, (async () => {
|
|
41
|
+
const [front, back, ...sponsors] = await Promise.all([
|
|
42
|
+
has('shirt-front') ? bitmap(team.id, 'shirt-front') : null,
|
|
43
|
+
has('shirt-back') ? bitmap(team.id, 'shirt-back') : null,
|
|
44
|
+
...SPONSOR_SLOTS.map((s) => (has(s) ? bitmap(team.id, s) : null)),
|
|
45
|
+
]);
|
|
46
|
+
return { front, back, sponsors: sponsors.filter(Boolean) };
|
|
47
|
+
})());
|
|
48
|
+
}
|
|
49
|
+
return cache.get(key);
|
|
50
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The team editor's part for celebration clips, the list of a team's clips under Celebrations,
|
|
5
|
+
// their upload and removal, which save at once as sounds do, and the joint mapping of each clip.
|
|
6
|
+
// The animations of the clips join the moves of the section, where Play previews them on the
|
|
7
|
+
// team's model, and the roster, where a player may take one as his own.
|
|
8
|
+
|
|
9
|
+
import { api, formatBytes } from '../core/api.js';
|
|
10
|
+
import {
|
|
11
|
+
MAX_CLIPS, MAX_CLIP_BYTES, MAX_TEAM_CLIP_BYTES, clipNameOf, loadTeamClips, qualify, readClip,
|
|
12
|
+
uploadMoveLabel, uploadMoves,
|
|
13
|
+
} from '../players/clips.js';
|
|
14
|
+
import { NODE_NAMES } from '../recording/format.js';
|
|
15
|
+
import { h } from './library.js';
|
|
16
|
+
import { uploadDuration } from '../players/retarget.js';
|
|
17
|
+
import { t, tn } from '../core/i18n.js';
|
|
18
|
+
import { icon, symbolOnly } from '../core/icons.js';
|
|
19
|
+
|
|
20
|
+
// The moves a team's clips add, by their names in the style.
|
|
21
|
+
export function clipMoves(editor) {
|
|
22
|
+
return uploadMoves(editor.team).map(([value]) => value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The animation's name of a move of a clip, or null for another move.
|
|
26
|
+
export function clipMoveLabel(editor, value) {
|
|
27
|
+
return uploadMoveLabel(editor.team, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// What the preview plays for a move, the loaded clip's own name for a move of a clip, and how long
|
|
31
|
+
// it plays, a clip at least once up to 15 seconds.
|
|
32
|
+
export function previewMove(editor, value) {
|
|
33
|
+
const pose = qualify(value, editor.clipTag);
|
|
34
|
+
const span = uploadDuration(pose);
|
|
35
|
+
return { pose, length: span > 0 ? Math.min(15, Math.max(5, span)) : 5 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Loads the clips of the team that the editor opened, then shows their moves.
|
|
39
|
+
export async function openClips(editor, token = editor.token) {
|
|
40
|
+
editor.clipTag = null;
|
|
41
|
+
const team = editor.team;
|
|
42
|
+
if (!team || !Array.isArray(team.clips) || !team.clips.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const tag = await loadTeamClips(team);
|
|
46
|
+
if (token === editor.token && editor.team === team) {
|
|
47
|
+
editor.clipTag = tag;
|
|
48
|
+
refresh(editor);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function refresh(editor) {
|
|
53
|
+
const clean = !editor.dirty;
|
|
54
|
+
editor.invalidate('celebrations', 'roster');
|
|
55
|
+
// A clip saves at once, so the editor's own changes stay as they were.
|
|
56
|
+
if (clean) {
|
|
57
|
+
editor.initial = editor.snapshot();
|
|
58
|
+
editor.markDirty();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function message(editor, text, kind = 'info') {
|
|
63
|
+
const el = editor.dialog.querySelector('.clip-message');
|
|
64
|
+
if (el) {
|
|
65
|
+
el.textContent = text;
|
|
66
|
+
el.dataset.kind = kind;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Uploads a clip from a file the user picked or dropped, a new clip or one of the same name.
|
|
71
|
+
export async function uploadClip(editor, file) {
|
|
72
|
+
if (!file) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const team = editor.team;
|
|
76
|
+
const token = editor.token;
|
|
77
|
+
const clips = Array.isArray(team.clips) ? team.clips : [];
|
|
78
|
+
const name = clipNameOf(file.name);
|
|
79
|
+
const others = clips.filter((c) => c.name !== name);
|
|
80
|
+
const fail = (text) => {
|
|
81
|
+
message(editor, text, 'error');
|
|
82
|
+
editor.app.hud.toast(text, 'error', 7000);
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
if (!/\.glb$/i.test(file.name)) {
|
|
86
|
+
return fail(t('clip-editor.wrongType'));
|
|
87
|
+
}
|
|
88
|
+
if (file.size > MAX_CLIP_BYTES) {
|
|
89
|
+
return fail(t('clip-editor.tooLarge', { size: formatBytes(file.size) }));
|
|
90
|
+
}
|
|
91
|
+
if (others.length >= MAX_CLIPS) {
|
|
92
|
+
return fail(t('clip-editor.tooMany', { max: MAX_CLIPS }));
|
|
93
|
+
}
|
|
94
|
+
if (others.reduce((sum, c) => sum + c.bytes, 0) + file.size > MAX_TEAM_CLIP_BYTES) {
|
|
95
|
+
return fail(t('clip-editor.tooMuch'));
|
|
96
|
+
}
|
|
97
|
+
let read;
|
|
98
|
+
let bytes;
|
|
99
|
+
try {
|
|
100
|
+
bytes = await file.arrayBuffer();
|
|
101
|
+
read = await readClip(bytes);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return fail(t('clip-editor.unusable', { reason: String(err.message || err).replace(/\.$/, '') }));
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const entry = await api.putClip(team.id, name, read.guess, bytes);
|
|
107
|
+
if (token !== editor.token) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
team.clips = entry.clips;
|
|
111
|
+
editor.clipTag = await loadTeamClips(team);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return fail(err.message || String(err));
|
|
114
|
+
}
|
|
115
|
+
if (token !== editor.token) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
refresh(editor);
|
|
119
|
+
const count = read.gltf.animations.length;
|
|
120
|
+
const text = read.guess.body
|
|
121
|
+
? t('clip-editor.saved', { file: file.name, count })
|
|
122
|
+
: t('clip-editor.savedUnmapped', { file: file.name });
|
|
123
|
+
message(editor, text, read.guess.body ? 'info' : 'error');
|
|
124
|
+
editor.app.hud.toast(text);
|
|
125
|
+
if (read.guess.body) {
|
|
126
|
+
editor.playCelebration(`upload:${name}.0`);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Removes a clip after asking, with the team's choices of its animations.
|
|
132
|
+
export async function removeClip(editor, name) {
|
|
133
|
+
if (!confirm(t('clip-editor.removeConfirm', { name }))) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const team = editor.team;
|
|
137
|
+
const token = editor.token;
|
|
138
|
+
try {
|
|
139
|
+
await api.deleteClip(team.id, name);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
editor.app.hud.toast(err.message || String(err), 'error');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (token !== editor.token) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const clean = !editor.dirty;
|
|
148
|
+
// The server took the clip's moves out of the saved style, the editor's copy follows.
|
|
149
|
+
const gone = (v) => typeof v === 'string' && v.startsWith(`upload:${name}.`);
|
|
150
|
+
const s = editor.style;
|
|
151
|
+
s.celebrations = s.celebrations.filter((v) => !gone(v));
|
|
152
|
+
for (const p of s.players) {
|
|
153
|
+
if (gone(p.celebration)) {
|
|
154
|
+
p.celebration = 'team';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (team.style) {
|
|
158
|
+
team.style.celebrations = (team.style.celebrations || []).filter((v) => !gone(v));
|
|
159
|
+
}
|
|
160
|
+
team.clips = (team.clips || []).filter((c) => c.name !== name);
|
|
161
|
+
editor.clipTag = await loadTeamClips(team);
|
|
162
|
+
if (token !== editor.token) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
editor.invalidate('celebrations', 'roster');
|
|
166
|
+
if (clean) {
|
|
167
|
+
editor.initial = editor.snapshot();
|
|
168
|
+
editor.markDirty();
|
|
169
|
+
}
|
|
170
|
+
editor.app.hud.toast(t('clip-editor.removed', { name }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The table that maps the joints of GRF's skeleton to the bones of a clip, read when opened.
|
|
174
|
+
function mappingTable(editor, clip, host) {
|
|
175
|
+
const token = editor.token;
|
|
176
|
+
host.replaceChildren(h('p', { class: 'muted small' }, t('clip-editor.reading')));
|
|
177
|
+
(async () => {
|
|
178
|
+
let read;
|
|
179
|
+
try {
|
|
180
|
+
read = await readClip(await api.clipBytes(editor.team.id, clip.name));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
host.replaceChildren(h('p', { class: 'muted small' }, t('clip-editor.readFailed', { reason: err.message || err })));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (token !== editor.token || !host.isConnected) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const joints = { ...(clip.joints && clip.joints.body ? clip.joints : read.guess) };
|
|
189
|
+
const status = h('span', { class: 'req' });
|
|
190
|
+
const showStatus = () => {
|
|
191
|
+
status.textContent = ` ${joints.body ? t('editor.joints.required') : t('editor.joints.requiredPelvis')}`;
|
|
192
|
+
};
|
|
193
|
+
showStatus();
|
|
194
|
+
const rows = NODE_NAMES.map((node) => {
|
|
195
|
+
const select = h('select', { 'aria-label': t('clip-editor.boneOf', { node: t(`editor.joints.node.${node}`) }) },
|
|
196
|
+
h('option', { value: '' }, node === 'player' ? t('editor.joints.root') : t('editor.joints.unmapped')),
|
|
197
|
+
read.bones.map((b) => h('option', { value: b, selected: joints[node] === b }, b)));
|
|
198
|
+
select.addEventListener('change', () => {
|
|
199
|
+
if (select.value) {
|
|
200
|
+
joints[node] = select.value;
|
|
201
|
+
} else {
|
|
202
|
+
delete joints[node];
|
|
203
|
+
}
|
|
204
|
+
showStatus();
|
|
205
|
+
});
|
|
206
|
+
return h('tr', {}, h('td', {}, t(`editor.joints.node.${node}`), node === 'body' ? status : null), h('td', {}, select));
|
|
207
|
+
});
|
|
208
|
+
const save = async (mapping) => {
|
|
209
|
+
try {
|
|
210
|
+
const entry = await api.clipJoints(editor.team.id, clip.name, mapping);
|
|
211
|
+
if (token !== editor.token) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
editor.team.clips = entry.clips;
|
|
215
|
+
editor.clipTag = await loadTeamClips(editor.team);
|
|
216
|
+
if (token !== editor.token) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
refresh(editor);
|
|
220
|
+
editor.app.hud.toast(t('clip-editor.mappingSaved', { name: clip.name }));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
editor.app.hud.toast(err.message || String(err), 'error', 7000);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
host.replaceChildren(
|
|
226
|
+
h('p', { class: 'muted small' }, tn('clip-editor.mappingNote')),
|
|
227
|
+
h('table', {}, h('tbody', {}, rows)),
|
|
228
|
+
h('div', { class: 'row' },
|
|
229
|
+
h('button', {
|
|
230
|
+
type: 'button',
|
|
231
|
+
class: 'btn small',
|
|
232
|
+
...symbolOnly(t('clip-editor.saveMapping')),
|
|
233
|
+
onclick: () => {
|
|
234
|
+
if (!joints.body) {
|
|
235
|
+
editor.app.hud.toast(t('clip-editor.needPelvis'), 'error');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
save(joints);
|
|
239
|
+
},
|
|
240
|
+
}, icon('save')),
|
|
241
|
+
h('button', { type: 'button', class: 'btn small ghost', onclick: () => save({}) }, t('clip-editor.mapByNames'))));
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The part of the Celebrations section that lists and uploads the team's clips.
|
|
246
|
+
export function clipsPanel(editor) {
|
|
247
|
+
const clips = Array.isArray(editor.team.clips) ? editor.team.clips : [];
|
|
248
|
+
const file = h('input', { type: 'file', accept: '.glb,model/gltf-binary', hidden: true, class: 'clip-file' });
|
|
249
|
+
file.addEventListener('change', () => {
|
|
250
|
+
const f = file.files[0];
|
|
251
|
+
file.value = '';
|
|
252
|
+
uploadClip(editor, f);
|
|
253
|
+
});
|
|
254
|
+
const rows = clips.map((clip) => {
|
|
255
|
+
const count = (clip.animations || []).length;
|
|
256
|
+
const mapping = h('div', { class: 'joints' });
|
|
257
|
+
const details = h('details', { class: 'joints-box' }, h('summary', {}, t('editor.joints.summary')), mapping);
|
|
258
|
+
details.addEventListener('toggle', () => {
|
|
259
|
+
if (details.open && !mapping.childNodes.length) {
|
|
260
|
+
mappingTable(editor, clip, mapping);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const mapped = clip.joints && clip.joints.body ? t('clip-editor.mappedByHand') : t('clip-editor.mappedByNames');
|
|
264
|
+
return h('li', { class: 'sound-row has clip-row', 'data-clip': clip.name },
|
|
265
|
+
h('span', { class: 'sound-text' },
|
|
266
|
+
h('span', { class: 'sound-label' }, clip.name),
|
|
267
|
+
h('span', { class: 'muted small' }, t('clip-editor.rowMeta', { count, size: formatBytes(clip.bytes), mapped }))),
|
|
268
|
+
h('span', { class: 'row sound-actions' },
|
|
269
|
+
h('button', { type: 'button', class: 'btn small ghost danger', ...symbolOnly(t('clip-editor.removeLabel', { name: clip.name })), onclick: () => removeClip(editor, clip.name) }, icon('trash'))),
|
|
270
|
+
details);
|
|
271
|
+
});
|
|
272
|
+
const drop = h('div', { class: 'dropzone small' },
|
|
273
|
+
h('p', {}, t('clip-editor.drop')),
|
|
274
|
+
h('button', { type: 'button', class: 'btn', ...symbolOnly(t('clip-editor.upload')), onclick: () => file.click() }, icon('upload')), file);
|
|
275
|
+
drop.addEventListener('dragover', (e) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
drop.classList.add('over');
|
|
279
|
+
});
|
|
280
|
+
drop.addEventListener('dragleave', () => drop.classList.remove('over'));
|
|
281
|
+
drop.addEventListener('drop', (e) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
e.stopPropagation();
|
|
284
|
+
drop.classList.remove('over');
|
|
285
|
+
uploadClip(editor, e.dataTransfer.files[0]);
|
|
286
|
+
});
|
|
287
|
+
return h('section', { class: 'clips' },
|
|
288
|
+
h('h4', {}, t('clip-editor.heading')),
|
|
289
|
+
h('p', { class: 'muted small' }, tn('clip-editor.note')),
|
|
290
|
+
rows.length ? h('ul', { class: 'sound-list clip-list' }, rows) : null,
|
|
291
|
+
drop,
|
|
292
|
+
h('p', { class: 'clip-message muted small', role: 'status', 'data-kind': 'info' }));
|
|
293
|
+
}
|