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,196 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The pictures of the challenge cards on the Play page. Each card shows a still of its challenge,
|
|
5
|
+
// and its short teaser video plays while the pointer rests on the card or the keyboard focus is
|
|
6
|
+
// inside it, or after a press of its play button. Under reduced motion, from the system or the
|
|
7
|
+
// page's own setting, only the play button starts a teaser. One teaser plays at a time, every
|
|
8
|
+
// teaser is muted, and a card that loses the pointer and the focus goes back to its still. A
|
|
9
|
+
// card without a teaser keeps its still, and a card without either draws a picture of its kind.
|
|
10
|
+
// ci/make_challenge_teasers.py makes the files and their manifest.
|
|
11
|
+
|
|
12
|
+
export const TEASER_FOLDER = 'assets/challenges/';
|
|
13
|
+
|
|
14
|
+
// Whether a name from the manifest is a plain file of the folder.
|
|
15
|
+
function plainName(name, suffix) {
|
|
16
|
+
return typeof name === 'string' && /^[a-z0-9_]+\.[a-z0-9]+$/.test(name) && name.endsWith(suffix);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The teaser and still of each challenge, by identifier, from the manifest's document. Entries
|
|
20
|
+
// whose names are not plain files of the folder are left out.
|
|
21
|
+
export function teaserFiles(document) {
|
|
22
|
+
const files = {};
|
|
23
|
+
const entries = document && typeof document === 'object' ? document.challenges : null;
|
|
24
|
+
if (!entries || typeof entries !== 'object') {
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
27
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
28
|
+
if (!entry || typeof entry !== 'object') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const teaser = plainName(entry.teaser, '.mp4') ? TEASER_FOLDER + entry.teaser : null;
|
|
32
|
+
const still = plainName(entry.still, '.webp') ? TEASER_FOLDER + entry.still : null;
|
|
33
|
+
if (teaser || still) {
|
|
34
|
+
files[id] = { teaser, still };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let loaded = null;
|
|
41
|
+
|
|
42
|
+
// The files of the teasers, read once from the manifest, or none when it cannot be read.
|
|
43
|
+
export function loadTeasers(fetcher = globalThis.fetch) {
|
|
44
|
+
if (!loaded) {
|
|
45
|
+
loaded = (async () => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetcher(`${TEASER_FOLDER}manifest.json`);
|
|
48
|
+
return res.ok ? teaserFiles(await res.json()) : {};
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
}
|
|
54
|
+
return loaded;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// What a card shows. picture is still, or drawn when no still is there to show, button whether
|
|
58
|
+
// it offers its play button, and playing whether its teaser plays. A card plays for a request of
|
|
59
|
+
// its button, or under full motion for the pointer or the focus.
|
|
60
|
+
export function teaserView({ still = false, teaser = false, reduced = false, reasons = [] } = {}) {
|
|
61
|
+
const why = new Set(reasons);
|
|
62
|
+
const playing = !!teaser && (why.has('request') || (!reduced && (why.has('hover') || why.has('focus'))));
|
|
63
|
+
return { picture: still ? 'still' : 'drawn', button: !!teaser, playing };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The teasers of a list of cards. A card is an object with an id, whether it has a still and a
|
|
67
|
+
// teaser, and show(view), which draws what teaserView returns. The controller keeps the reasons
|
|
68
|
+
// each card has to play and lets one card play at a time.
|
|
69
|
+
export class TeaserCards {
|
|
70
|
+
constructor({ reduced = () => false } = {}) {
|
|
71
|
+
this.reduced = reduced;
|
|
72
|
+
this.cards = new Map();
|
|
73
|
+
this.current = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
add(card) {
|
|
77
|
+
this.cards.set(card.id, { card, reasons: new Set() });
|
|
78
|
+
this.update(card.id);
|
|
79
|
+
return card;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
clear() {
|
|
83
|
+
for (const entry of this.cards.values()) {
|
|
84
|
+
entry.reasons.clear();
|
|
85
|
+
this.draw(entry);
|
|
86
|
+
}
|
|
87
|
+
this.cards.clear();
|
|
88
|
+
this.current = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The page leaves the screen, and every card goes back to its still.
|
|
92
|
+
stop() {
|
|
93
|
+
for (const entry of this.cards.values()) {
|
|
94
|
+
entry.reasons.clear();
|
|
95
|
+
}
|
|
96
|
+
this.refresh();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
view(id) {
|
|
100
|
+
const entry = this.cards.get(id);
|
|
101
|
+
return entry ? this.viewOf(entry) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
viewOf(entry) {
|
|
105
|
+
const { card } = entry;
|
|
106
|
+
return teaserView({ still: !!card.still, teaser: !!card.teaser, reduced: this.reduced(), reasons: [...entry.reasons] });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The pointer rests on the card, or the focus moves into it.
|
|
110
|
+
enter(id, reason) {
|
|
111
|
+
this.reason(id, reason, true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The pointer or the focus leaves the card. Once neither is left, the card goes back to its
|
|
115
|
+
// still, a request of its button included.
|
|
116
|
+
leave(id, reason) {
|
|
117
|
+
const entry = this.cards.get(id);
|
|
118
|
+
if (!entry) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
entry.reasons.delete(reason);
|
|
122
|
+
if (!entry.reasons.has('hover') && !entry.reasons.has('focus')) {
|
|
123
|
+
entry.reasons.delete('request');
|
|
124
|
+
}
|
|
125
|
+
this.update(id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The play button of the card, which starts the teaser or stops it.
|
|
129
|
+
toggle(id) {
|
|
130
|
+
const entry = this.cards.get(id);
|
|
131
|
+
if (!entry) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (this.viewOf(entry).playing) {
|
|
135
|
+
entry.reasons.clear();
|
|
136
|
+
} else {
|
|
137
|
+
entry.reasons.add('request');
|
|
138
|
+
}
|
|
139
|
+
this.update(id);
|
|
140
|
+
return this.viewOf(entry).playing;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A teaser that fails to load stays off, and its card keeps its still.
|
|
144
|
+
fail(id) {
|
|
145
|
+
const entry = this.cards.get(id);
|
|
146
|
+
if (entry) {
|
|
147
|
+
entry.card.teaser = null;
|
|
148
|
+
entry.reasons.clear();
|
|
149
|
+
this.update(id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The motion setting changed, so every card draws again.
|
|
154
|
+
refresh() {
|
|
155
|
+
for (const id of this.cards.keys()) {
|
|
156
|
+
this.update(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
reason(id, reason, on) {
|
|
161
|
+
const entry = this.cards.get(id);
|
|
162
|
+
if (!entry) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (on) {
|
|
166
|
+
entry.reasons.add(reason);
|
|
167
|
+
} else {
|
|
168
|
+
entry.reasons.delete(reason);
|
|
169
|
+
}
|
|
170
|
+
this.update(id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
update(id) {
|
|
174
|
+
const entry = this.cards.get(id);
|
|
175
|
+
if (!entry) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (this.viewOf(entry).playing && this.current !== id) {
|
|
179
|
+
// Another card that plays stops, and forgets why it played.
|
|
180
|
+
const before = this.current && this.cards.get(this.current);
|
|
181
|
+
this.current = id;
|
|
182
|
+
if (before) {
|
|
183
|
+
before.reasons.clear();
|
|
184
|
+
this.draw(before);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!this.viewOf(entry).playing && this.current === id) {
|
|
188
|
+
this.current = null;
|
|
189
|
+
}
|
|
190
|
+
this.draw(entry);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
draw(entry) {
|
|
194
|
+
entry.card.show(this.viewOf(entry));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Touch controls. During a game on Play, a touch screen shows an on-screen stick and the buttons
|
|
5
|
+
// of GRF's layout, short pass, long pass, high pass, shot, sprint and dribble, each held by a
|
|
6
|
+
// finger of its own, so several fingers play at once. They press the same inputs as the keys and
|
|
7
|
+
// the buttons of a controller, through a device of their own that drives the person's side as the
|
|
8
|
+
// keyboard does. A setting shows them always, never, or once a touch screen is used. On Watch,
|
|
9
|
+
// the controls grow to the size of a finger on a touch screen, and a swipe along the timeline
|
|
10
|
+
// moves through the match from where it stands, while a tap seeks to the place tapped. The rules
|
|
11
|
+
// at the top of the file touch nothing of the page, so they hold the same in a test.
|
|
12
|
+
|
|
13
|
+
import { t } from '../core/i18n.js';
|
|
14
|
+
import { icon, symbolOnly } from '../core/icons.js';
|
|
15
|
+
|
|
16
|
+
export const TOUCH_KEY = 'cuju.touchControls';
|
|
17
|
+
export const TOUCH_CHOICES = ['auto', 'on', 'off'].map((choice) => [choice, t(`touch.choice.${choice}`)]);
|
|
18
|
+
|
|
19
|
+
// The buttons of the pad, each with the inputs it presses, the same as the keys of play send.
|
|
20
|
+
// High pass also slides when defending, as GRF reads one input for both.
|
|
21
|
+
export const TOUCH_BUTTONS = [
|
|
22
|
+
{ id: 'long_pass', names: ['long_pass'] },
|
|
23
|
+
{ id: 'high_pass', names: ['high_pass', 'sliding'] },
|
|
24
|
+
{ id: 'shot', names: ['shot'] },
|
|
25
|
+
{ id: 'short_pass', names: ['short_pass'] },
|
|
26
|
+
{ id: 'sprint', names: ['sprint'] },
|
|
27
|
+
{ id: 'dribble', names: ['dribble'] },
|
|
28
|
+
].map((button) => ({ ...button, label: t(`touch.button.${button.id}`), short: t(`touch.button.${button.id}.short`) }));
|
|
29
|
+
|
|
30
|
+
// The stored choice, auto for anything else.
|
|
31
|
+
export function touchChoice(value) {
|
|
32
|
+
return TOUCH_CHOICES.some(([choice]) => choice === value) ? value : 'auto';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Whether the pad shows for a choice, once a touch screen has been seen or not.
|
|
36
|
+
export function touchShown(choice, touched) {
|
|
37
|
+
const chosen = touchChoice(choice);
|
|
38
|
+
return chosen === 'on' || (chosen === 'auto' && !!touched);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The directions that a stick pushed by dx and dy from its centre holds, as a controller's stick
|
|
42
|
+
// counts them, each axis past the dead zone as a share of the radius. Screen y grows downwards.
|
|
43
|
+
export function stickNames(dx, dy, radius, dead = 0.35) {
|
|
44
|
+
const reach = Math.max(1, radius) * dead;
|
|
45
|
+
const names = [];
|
|
46
|
+
if (dx <= -reach) {
|
|
47
|
+
names.push('left');
|
|
48
|
+
} else if (dx >= reach) {
|
|
49
|
+
names.push('right');
|
|
50
|
+
}
|
|
51
|
+
if (dy <= -reach) {
|
|
52
|
+
names.push('up');
|
|
53
|
+
} else if (dy >= reach) {
|
|
54
|
+
names.push('down');
|
|
55
|
+
}
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The frame that a swipe along the timeline reaches, the width of the timeline being the whole
|
|
60
|
+
// match, kept within the frames held.
|
|
61
|
+
export function swipeFrame(from, dx, width, first, last) {
|
|
62
|
+
const frame = from + (dx / Math.max(1, width)) * (last - first);
|
|
63
|
+
return Math.max(first, Math.min(last, frame));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function h(tag, attributes = {}, ...children) {
|
|
67
|
+
const node = document.createElement(tag);
|
|
68
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
69
|
+
if (value !== null && value !== undefined && value !== false) {
|
|
70
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
node.append(...children.flat().filter((child) => child !== null && child !== undefined));
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function load() {
|
|
78
|
+
try {
|
|
79
|
+
return touchChoice(JSON.parse(localStorage.getItem(TOUCH_KEY)));
|
|
80
|
+
} catch {
|
|
81
|
+
return 'auto';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class TouchControls {
|
|
86
|
+
constructor(app) {
|
|
87
|
+
this.app = app;
|
|
88
|
+
this.choice = load();
|
|
89
|
+
this.touched = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
|
|
90
|
+
this.pad = null;
|
|
91
|
+
this.device = null;
|
|
92
|
+
this.stickHeld = [];
|
|
93
|
+
window.addEventListener('pointerdown', (e) => {
|
|
94
|
+
if (e.pointerType === 'touch' && !this.touched) {
|
|
95
|
+
this.touched = true;
|
|
96
|
+
this.update();
|
|
97
|
+
}
|
|
98
|
+
}, true);
|
|
99
|
+
this.bindSetting();
|
|
100
|
+
this.bindTimeline();
|
|
101
|
+
setInterval(() => this.update(), 250);
|
|
102
|
+
this.update();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
bindSetting() {
|
|
106
|
+
const anchor = document.getElementById('pointer-lock')?.closest('label');
|
|
107
|
+
if (!anchor) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.select = h('select', { id: 'touch-controls', 'aria-label': t('touch.setting') },
|
|
111
|
+
TOUCH_CHOICES.map(([value, label]) => h('option', { value }, label)));
|
|
112
|
+
this.select.value = this.choice;
|
|
113
|
+
this.select.addEventListener('change', () => {
|
|
114
|
+
this.choice = touchChoice(this.select.value);
|
|
115
|
+
try {
|
|
116
|
+
localStorage.setItem(TOUCH_KEY, JSON.stringify(this.choice));
|
|
117
|
+
} catch {
|
|
118
|
+
// Storage can be unavailable, the setting then lasts for this visit only.
|
|
119
|
+
}
|
|
120
|
+
this.update();
|
|
121
|
+
});
|
|
122
|
+
anchor.after(h('label', {}, t('touch.setting'), this.select));
|
|
123
|
+
import('../core/menu.js').then((menu) => menu.enhanceSelect(this.select, { className: 'in-panel', align: 'right' }))
|
|
124
|
+
.catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The pad of the Play page, built into its stage the first time the stage exists.
|
|
128
|
+
build(play) {
|
|
129
|
+
this.play = play;
|
|
130
|
+
this.device = new play.keyboard.constructor('touch', t('touch.device'));
|
|
131
|
+
play.touch = this.device;
|
|
132
|
+
this.knob = h('div', { class: 'touch-knob' });
|
|
133
|
+
this.stick = h('div', { class: 'touch-stick' }, this.knob);
|
|
134
|
+
this.buttons = TOUCH_BUTTONS.map((button) => {
|
|
135
|
+
const node = h('button', {
|
|
136
|
+
type: 'button', class: `touch-btn touch-${button.id}`, tabindex: '-1',
|
|
137
|
+
'data-action': button.id, title: button.label,
|
|
138
|
+
}, button.short);
|
|
139
|
+
this.hold(node, (down) => this.press(button.names, down));
|
|
140
|
+
return node;
|
|
141
|
+
});
|
|
142
|
+
this.pause = h('button', { type: 'button', class: 'touch-pause', tabindex: '-1', ...symbolOnly(t('touch.pause'), t('touch.pause.title')) }, icon('pause'));
|
|
143
|
+
this.pause.addEventListener('click', () => {
|
|
144
|
+
if (play.game && !play.game.ended) {
|
|
145
|
+
play.pause(true);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Keys and screen readers have the keys of play, so the pad stays out of their way.
|
|
149
|
+
this.pad = h('div', { class: 'touch-pad', 'aria-hidden': 'true', hidden: true },
|
|
150
|
+
this.stick, h('div', { class: 'touch-cluster' }, this.buttons), this.pause);
|
|
151
|
+
this.pad.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
152
|
+
this.bindStick();
|
|
153
|
+
play.stage.append(this.pad);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A button held by one finger, pressed until that finger lifts or leaves the screen.
|
|
157
|
+
hold(node, change) {
|
|
158
|
+
let finger = null;
|
|
159
|
+
node.addEventListener('pointerdown', (e) => {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
if (finger !== null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
finger = e.pointerId;
|
|
165
|
+
node.setPointerCapture?.(e.pointerId);
|
|
166
|
+
node.classList.add('held');
|
|
167
|
+
change(true);
|
|
168
|
+
});
|
|
169
|
+
const up = (e) => {
|
|
170
|
+
if (e.pointerId !== finger) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
finger = null;
|
|
174
|
+
node.classList.remove('held');
|
|
175
|
+
change(false);
|
|
176
|
+
};
|
|
177
|
+
node.addEventListener('pointerup', up);
|
|
178
|
+
node.addEventListener('pointercancel', up);
|
|
179
|
+
node.addEventListener('lostpointercapture', up);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
bindStick() {
|
|
183
|
+
let finger = null;
|
|
184
|
+
const move = (e) => {
|
|
185
|
+
const rect = this.stick.getBoundingClientRect();
|
|
186
|
+
const radius = rect.width / 2;
|
|
187
|
+
let dx = e.clientX - (rect.left + radius);
|
|
188
|
+
let dy = e.clientY - (rect.top + radius);
|
|
189
|
+
const length = Math.hypot(dx, dy);
|
|
190
|
+
if (length > radius) {
|
|
191
|
+
dx *= radius / length;
|
|
192
|
+
dy *= radius / length;
|
|
193
|
+
}
|
|
194
|
+
this.knob.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
195
|
+
this.steer(stickNames(dx, dy, radius));
|
|
196
|
+
};
|
|
197
|
+
this.stick.addEventListener('pointerdown', (e) => {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
if (finger !== null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
finger = e.pointerId;
|
|
203
|
+
this.stick.setPointerCapture?.(e.pointerId);
|
|
204
|
+
this.stick.classList.add('held');
|
|
205
|
+
move(e);
|
|
206
|
+
});
|
|
207
|
+
this.stick.addEventListener('pointermove', (e) => {
|
|
208
|
+
if (e.pointerId === finger) {
|
|
209
|
+
move(e);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const up = (e) => {
|
|
213
|
+
if (e.pointerId !== finger) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
finger = null;
|
|
217
|
+
this.stick.classList.remove('held');
|
|
218
|
+
this.knob.style.transform = '';
|
|
219
|
+
this.steer([]);
|
|
220
|
+
};
|
|
221
|
+
this.stick.addEventListener('pointerup', up);
|
|
222
|
+
this.stick.addEventListener('pointercancel', up);
|
|
223
|
+
this.stick.addEventListener('lostpointercapture', up);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Holds the directions of the stick, releasing those it left and pressing those it reached.
|
|
227
|
+
steer(names) {
|
|
228
|
+
const off = this.stickHeld.filter((name) => !names.includes(name));
|
|
229
|
+
const on = names.filter((name) => !this.stickHeld.includes(name));
|
|
230
|
+
this.stickHeld = names;
|
|
231
|
+
if (off.length) {
|
|
232
|
+
this.press(off, false);
|
|
233
|
+
}
|
|
234
|
+
if (on.length) {
|
|
235
|
+
this.press(on, true);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sends inputs as a key of play does, while a game runs and its pause menu is closed.
|
|
240
|
+
press(names, down) {
|
|
241
|
+
const play = this.play;
|
|
242
|
+
const game = play && play.game;
|
|
243
|
+
if (!game || game.ended || !game.started || (down && !play.menu.hidden)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
play.use(this.device);
|
|
247
|
+
this.device.set(names, down);
|
|
248
|
+
play.flush();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Lets every finger go, as a key of play does when the window loses focus.
|
|
252
|
+
releaseAll() {
|
|
253
|
+
if (this.device && this.device.held.size) {
|
|
254
|
+
this.device.held.clear();
|
|
255
|
+
this.play.flush();
|
|
256
|
+
}
|
|
257
|
+
this.stickHeld = [];
|
|
258
|
+
for (const node of this.pad ? this.pad.querySelectorAll('.held') : []) {
|
|
259
|
+
node.classList.remove('held');
|
|
260
|
+
}
|
|
261
|
+
if (this.knob) {
|
|
262
|
+
this.knob.style.transform = '';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
update() {
|
|
267
|
+
const app = this.app;
|
|
268
|
+
document.getElementById('app')?.classList.toggle('touch', this.touched);
|
|
269
|
+
const play = app.play;
|
|
270
|
+
if (!this.pad && play && play.stage) {
|
|
271
|
+
this.build(play);
|
|
272
|
+
}
|
|
273
|
+
if (!this.pad) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const game = play.game;
|
|
277
|
+
const show = touchShown(this.choice, this.touched) && app.page === 'play' && !play.stage.hidden
|
|
278
|
+
&& !!game && game.started && !game.ended && play.menu.hidden && play.end.hidden;
|
|
279
|
+
if (this.pad.hidden === show) {
|
|
280
|
+
this.pad.hidden = !show;
|
|
281
|
+
if (!show) {
|
|
282
|
+
this.releaseAll();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A swipe along the timeline by touch moves through the match from where it stands, and a
|
|
288
|
+
// tap seeks to the place tapped. Mouse and pen keep the timeline's own drag.
|
|
289
|
+
bindTimeline() {
|
|
290
|
+
const track = document.getElementById('timeline');
|
|
291
|
+
const controls = document.getElementById('controls');
|
|
292
|
+
if (!track || !controls) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
let swipe = null;
|
|
296
|
+
controls.addEventListener('pointerdown', (e) => {
|
|
297
|
+
const tl = this.app.timeline;
|
|
298
|
+
if (e.pointerType !== 'touch' || !tl || !track.contains(e.target)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
e.stopPropagation();
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
swipe = { id: e.pointerId, x: e.clientX, from: tl.t, moved: false, playing: tl.playing && !tl.following };
|
|
304
|
+
tl.playing = false;
|
|
305
|
+
tl.following = false;
|
|
306
|
+
track.setPointerCapture?.(e.pointerId);
|
|
307
|
+
}, true);
|
|
308
|
+
const seek = (frame) => {
|
|
309
|
+
const tl = this.app.timeline;
|
|
310
|
+
tl.seek(frame);
|
|
311
|
+
this.app.afterSeek();
|
|
312
|
+
};
|
|
313
|
+
controls.addEventListener('pointermove', (e) => {
|
|
314
|
+
const tl = this.app.timeline;
|
|
315
|
+
if (!swipe || e.pointerId !== swipe.id || !tl) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
e.stopPropagation();
|
|
319
|
+
const dx = e.clientX - swipe.x;
|
|
320
|
+
swipe.moved ||= Math.abs(dx) > 8;
|
|
321
|
+
if (swipe.moved) {
|
|
322
|
+
seek(swipeFrame(swipe.from, dx, track.getBoundingClientRect().width, tl.first, tl.last));
|
|
323
|
+
}
|
|
324
|
+
}, true);
|
|
325
|
+
const end = (e) => {
|
|
326
|
+
const tl = this.app.timeline;
|
|
327
|
+
if (!swipe || e.pointerId !== swipe.id) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
const done = swipe;
|
|
332
|
+
swipe = null;
|
|
333
|
+
if (!tl) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!done.moved && e.type === 'pointerup') {
|
|
337
|
+
const rect = track.getBoundingClientRect();
|
|
338
|
+
const share = Math.max(0, Math.min(1, (done.x - rect.left) / Math.max(1, rect.width)));
|
|
339
|
+
seek(tl.first + share * (tl.last - tl.first));
|
|
340
|
+
}
|
|
341
|
+
if (done.playing) {
|
|
342
|
+
tl.playing = true;
|
|
343
|
+
}
|
|
344
|
+
this.app.updateHash();
|
|
345
|
+
this.app.updatePlayUi();
|
|
346
|
+
};
|
|
347
|
+
controls.addEventListener('pointerup', end, true);
|
|
348
|
+
controls.addEventListener('pointercancel', end, true);
|
|
349
|
+
}
|
|
350
|
+
}
|