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,327 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Post passes of team packages, the features post.layer and post.fullscreen.
|
|
5
|
+
//
|
|
6
|
+
// A team's layer pass works on the team's own objects alone. The page draws the whole scene into a
|
|
7
|
+
// small target writing depth only, then the team's players and effects with their own materials,
|
|
8
|
+
// so the target holds what of the team the camera sees, and a pass of the team draws from it onto
|
|
9
|
+
// the finished picture, a glow, an outline, pixels, a tint, a colour split, a blur or the team's
|
|
10
|
+
// own shader. A full screen pass works on the whole picture, only during the team's own moment,
|
|
11
|
+
// at most three seconds, at high quality, and never under reduced motion. Every pass's strength
|
|
12
|
+
// goes through the flash governor. The degrade ladder drops a team's passes at its rung noPasses.
|
|
13
|
+
|
|
14
|
+
import * as THREE from 'three';
|
|
15
|
+
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
16
|
+
|
|
17
|
+
export const TEAM_LAYERS = [10, 11];
|
|
18
|
+
export const PASSES = ['glow', 'outline', 'pixelate', 'tint', 'chromatic', 'blur', 'custom'];
|
|
19
|
+
// The passes that move the picture, which reduced motion leaves out.
|
|
20
|
+
export const DISTORTING = ['pixelate', 'chromatic'];
|
|
21
|
+
export const FULLSCREEN_SECONDS = 3;
|
|
22
|
+
// The events that make a team's own moment, when a full screen pass may play.
|
|
23
|
+
export const MOMENTS = ['goal', 'celebration.begin', 'winner.reveal', 'mvp.reveal', 'save', 'woodwork', 'reaction.begin', 'lineup.begin'];
|
|
24
|
+
|
|
25
|
+
const PASS_INDEX = { glow: 0, outline: 1, pixelate: 2, tint: 3, chromatic: 4, blur: 5 };
|
|
26
|
+
|
|
27
|
+
const VERTEX = `
|
|
28
|
+
varying vec2 vUv;
|
|
29
|
+
void main() {
|
|
30
|
+
vUv = uv;
|
|
31
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
32
|
+
}`;
|
|
33
|
+
|
|
34
|
+
const FRAGMENT = `
|
|
35
|
+
uniform sampler2D tSource;
|
|
36
|
+
uniform vec2 uTexel;
|
|
37
|
+
uniform int uPass;
|
|
38
|
+
uniform vec3 uColor;
|
|
39
|
+
uniform float uStrength;
|
|
40
|
+
uniform float uLayer;
|
|
41
|
+
varying vec2 vUv;
|
|
42
|
+
vec4 at(vec2 uv) { return texture2D(tSource, uv); }
|
|
43
|
+
vec4 blur(vec2 uv, float r) {
|
|
44
|
+
vec4 s = vec4(0.0);
|
|
45
|
+
float w = 0.0;
|
|
46
|
+
for (int x = -2; x <= 2; x++) {
|
|
47
|
+
for (int y = -2; y <= 2; y++) {
|
|
48
|
+
float k = exp(-float(x * x + y * y) / 4.0);
|
|
49
|
+
s += at(uv + vec2(float(x), float(y)) * uTexel * r) * k;
|
|
50
|
+
w += k;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return s / w;
|
|
54
|
+
}
|
|
55
|
+
void main() {
|
|
56
|
+
vec4 c = at(vUv);
|
|
57
|
+
vec4 o = vec4(0.0);
|
|
58
|
+
if (uPass == 0) {
|
|
59
|
+
vec4 b = blur(vUv, 2.5);
|
|
60
|
+
o = vec4(uColor * b.a * uStrength, 0.0);
|
|
61
|
+
} else if (uPass == 1) {
|
|
62
|
+
float edge = 0.0;
|
|
63
|
+
for (int i = 0; i < 8; i++) {
|
|
64
|
+
float a = float(i) * 0.785398;
|
|
65
|
+
edge = max(edge, at(vUv + vec2(cos(a), sin(a)) * uTexel * 1.5).a);
|
|
66
|
+
}
|
|
67
|
+
edge = clamp(edge - c.a, 0.0, 1.0);
|
|
68
|
+
o = vec4(uColor * edge * uStrength, 0.0);
|
|
69
|
+
} else if (uPass == 2) {
|
|
70
|
+
vec2 cell = uTexel * (4.0 + 6.0 * uStrength);
|
|
71
|
+
vec4 p = at((floor(vUv / cell) + 0.5) * cell);
|
|
72
|
+
o = vec4(p.rgb, p.a * (uLayer > 0.5 ? 1.0 : clamp(uStrength, 0.0, 1.0)));
|
|
73
|
+
} else if (uPass == 3) {
|
|
74
|
+
o = vec4(mix(c.rgb, c.rgb * uColor * 1.6, clamp(uStrength, 0.0, 1.0)), c.a);
|
|
75
|
+
} else if (uPass == 4) {
|
|
76
|
+
vec2 d = uTexel * 3.0 * uStrength;
|
|
77
|
+
vec4 r = at(vUv + d);
|
|
78
|
+
vec4 b = at(vUv - d);
|
|
79
|
+
o = uLayer > 0.5 ? vec4(r.r * r.a, 0.0, b.b * b.a, 0.0) : vec4(r.r, c.g, b.b, 1.0);
|
|
80
|
+
} else {
|
|
81
|
+
vec4 b = blur(vUv, 1.0 + 2.0 * uStrength);
|
|
82
|
+
o = vec4(b.rgb, b.a);
|
|
83
|
+
}
|
|
84
|
+
// The layer target holds linear light without tone mapping, the picture is in sRGB.
|
|
85
|
+
if (uLayer > 0.5 && (uPass == 2 || uPass == 3 || uPass == 5)) {
|
|
86
|
+
o.rgb = pow(max(o.rgb, 0.0), vec3(0.4545));
|
|
87
|
+
}
|
|
88
|
+
gl_FragColor = o;
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
function passMaterial(blending) {
|
|
92
|
+
return new THREE.ShaderMaterial({
|
|
93
|
+
uniforms: {
|
|
94
|
+
tSource: { value: null }, uTexel: { value: new THREE.Vector2() }, uPass: { value: 0 },
|
|
95
|
+
uColor: { value: new THREE.Color(1, 1, 1) }, uStrength: { value: 1 }, uLayer: { value: 1 },
|
|
96
|
+
},
|
|
97
|
+
vertexShader: VERTEX, fragmentShader: FRAGMENT, depthTest: false, depthWrite: false, transparent: true,
|
|
98
|
+
blending,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The blending each pass of a layer takes over the picture, additive for light it adds, normal
|
|
103
|
+
// for a look that covers the team's own.
|
|
104
|
+
function blendingOf(pass) {
|
|
105
|
+
return pass === 'glow' || pass === 'outline' || pass === 'chromatic' ? THREE.AdditiveBlending : THREE.NormalBlending;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class Post {
|
|
109
|
+
constructor(presentation) {
|
|
110
|
+
this.presentation = presentation;
|
|
111
|
+
this.layers = [null, null];
|
|
112
|
+
this.moments = [];
|
|
113
|
+
this.target = null;
|
|
114
|
+
this.frame = null;
|
|
115
|
+
this.quad = null;
|
|
116
|
+
this.depthOnly = new THREE.MeshBasicMaterial({ colorWrite: false });
|
|
117
|
+
this.materials = { additive: passMaterial(THREE.AdditiveBlending), normal: passMaterial(THREE.NormalBlending), opaque: passMaterial(THREE.NoBlending) };
|
|
118
|
+
this.drawn = 0;
|
|
119
|
+
this.refused = [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The persistent layer passes of the packages on screen.
|
|
123
|
+
attach(packages) {
|
|
124
|
+
this.layers = [null, null];
|
|
125
|
+
this.moments = [];
|
|
126
|
+
for (const pkg of packages) {
|
|
127
|
+
if (pkg && pkg.manifest.post && pkg.manifest.post.layer) {
|
|
128
|
+
this.layers[pkg.side] = { pkg, spec: pkg.manifest.post.layer, transient: false };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
allowed(pkg, scope, pass) {
|
|
134
|
+
const p = this.presentation;
|
|
135
|
+
const quality = pkg.budget.quality;
|
|
136
|
+
if (pkg.budget.past('noPasses') || quality === 'low') {
|
|
137
|
+
return 'the team is at a rung without passes or at low quality';
|
|
138
|
+
}
|
|
139
|
+
if (p.app.reducedMotion && (scope === 'fullscreen' || DISTORTING.includes(pass))) {
|
|
140
|
+
return 'reduced motion';
|
|
141
|
+
}
|
|
142
|
+
if (scope === 'fullscreen' && (quality !== 'high' || !(pkg.budget.limits.fullscreen > 0))) {
|
|
143
|
+
return 'a full screen pass needs high quality';
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A pass an effect of type post starts, from a reflex or a worker. A full screen pass plays on
|
|
149
|
+
// the team's own moment alone, one at a time. Returns whether it was taken.
|
|
150
|
+
spawn(pkg, spec, c, event) {
|
|
151
|
+
const scope = spec.scope || 'layer';
|
|
152
|
+
const refusal = this.allowed(pkg, scope, spec.pass);
|
|
153
|
+
const own = event && event.team === pkg.side && MOMENTS.includes(event.kind);
|
|
154
|
+
if (refusal || (scope === 'fullscreen' && (!own || this.moments.some((m) => m.scope === 'fullscreen')))) {
|
|
155
|
+
this.refused.push(refusal || 'a full screen pass plays on the team’s own moment, one at a time');
|
|
156
|
+
this.refused = this.refused.slice(-8);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const now = this.presentation.clocks.cosmetic;
|
|
160
|
+
const duration = Math.min(spec.duration || 1.5, scope === 'fullscreen' ? FULLSCREEN_SECONDS : 30);
|
|
161
|
+
this.moments.push({ pkg, spec, scope, from: now, to: now + duration, id: c.id });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
endTransient() {
|
|
166
|
+
this.moments = [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
active() {
|
|
170
|
+
return this.layers.some(Boolean) || this.moments.length > 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ensure(renderer) {
|
|
174
|
+
const size = renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
175
|
+
const w = Math.max(1, Math.floor(size.x / 2));
|
|
176
|
+
const h = Math.max(1, Math.floor(size.y / 2));
|
|
177
|
+
if (!this.target || this.target.width !== w || this.target.height !== h) {
|
|
178
|
+
if (this.target) {
|
|
179
|
+
this.target.dispose();
|
|
180
|
+
}
|
|
181
|
+
this.target = new THREE.WebGLRenderTarget(w, h, { depthBuffer: true });
|
|
182
|
+
}
|
|
183
|
+
if (!this.frame || this.frame.image.width !== size.x || this.frame.image.height !== size.y) {
|
|
184
|
+
if (this.frame) {
|
|
185
|
+
this.frame.dispose();
|
|
186
|
+
}
|
|
187
|
+
this.frame = new THREE.FramebufferTexture(size.x, size.y);
|
|
188
|
+
}
|
|
189
|
+
if (!this.quad) {
|
|
190
|
+
this.quad = new FullScreenQuad(null);
|
|
191
|
+
}
|
|
192
|
+
return size;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Lights light every layer, so the team's own objects keep their light in their own pass.
|
|
196
|
+
lightLayers(scene) {
|
|
197
|
+
const now = this.presentation.clocks.cosmetic;
|
|
198
|
+
if (this.lit && now - this.lit < 2) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.lit = now;
|
|
202
|
+
scene.traverse((o) => {
|
|
203
|
+
if (o.isLight) {
|
|
204
|
+
o.layers.enable(TEAM_LAYERS[0]);
|
|
205
|
+
o.layers.enable(TEAM_LAYERS[1]);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Draws the passes after the picture, called by the view once it rendered.
|
|
211
|
+
render(renderer, scene, camera) {
|
|
212
|
+
const p = this.presentation;
|
|
213
|
+
const now = p.clocks.cosmetic;
|
|
214
|
+
this.moments = this.moments.filter((m) => m.to > now);
|
|
215
|
+
if (!this.active()) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const size = this.ensure(renderer);
|
|
219
|
+
const governor = p.flashFactor();
|
|
220
|
+
const passes = [];
|
|
221
|
+
for (const layer of this.layers) {
|
|
222
|
+
if (layer && !this.allowed(layer.pkg, 'layer', layer.spec.pass)) {
|
|
223
|
+
passes.push({ ...layer, scope: 'layer', fade: 1 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const m of this.moments) {
|
|
227
|
+
const fade = Math.min(1, (now - m.from) / 0.3, (m.to - now) / 0.3);
|
|
228
|
+
passes.push({ ...m, fade: Math.max(0, fade) });
|
|
229
|
+
}
|
|
230
|
+
const autoClear = renderer.autoClear;
|
|
231
|
+
const clear = renderer.getClearColor(new THREE.Color());
|
|
232
|
+
const alpha = renderer.getClearAlpha();
|
|
233
|
+
const mask = camera.layers.mask;
|
|
234
|
+
const background = scene.background;
|
|
235
|
+
renderer.autoClear = false;
|
|
236
|
+
try {
|
|
237
|
+
for (const pass of passes) {
|
|
238
|
+
const env = p.flash.envelope(`${pass.pkg.side}:post:${pass.scope}`);
|
|
239
|
+
const strength = (pass.spec.strength ?? 1) * env.apply(now, pass.fade) * governor;
|
|
240
|
+
if (strength <= 0.001) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (pass.scope === 'layer') {
|
|
244
|
+
this.lightLayers(scene);
|
|
245
|
+
scene.background = null;
|
|
246
|
+
renderer.setRenderTarget(this.target);
|
|
247
|
+
renderer.setClearColor(0x000000, 0);
|
|
248
|
+
renderer.clear();
|
|
249
|
+
scene.overrideMaterial = this.depthOnly;
|
|
250
|
+
renderer.render(scene, camera);
|
|
251
|
+
scene.overrideMaterial = null;
|
|
252
|
+
camera.layers.set(TEAM_LAYERS[pass.pkg.side]);
|
|
253
|
+
renderer.render(scene, camera);
|
|
254
|
+
camera.layers.mask = mask;
|
|
255
|
+
scene.background = background;
|
|
256
|
+
renderer.setRenderTarget(null);
|
|
257
|
+
this.draw(renderer, pass, this.target.texture, 1 / this.target.width, 1 / this.target.height, strength, true);
|
|
258
|
+
} else {
|
|
259
|
+
renderer.setRenderTarget(null);
|
|
260
|
+
renderer.copyFramebufferToTexture(this.frame);
|
|
261
|
+
this.draw(renderer, pass, this.frame, 1 / size.x, 1 / size.y, strength, false);
|
|
262
|
+
}
|
|
263
|
+
this.drawn += 1;
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
scene.overrideMaterial = null;
|
|
267
|
+
camera.layers.mask = mask;
|
|
268
|
+
scene.background = background;
|
|
269
|
+
renderer.setRenderTarget(null);
|
|
270
|
+
renderer.setClearColor(clear, alpha);
|
|
271
|
+
renderer.autoClear = autoClear;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
draw(renderer, pass, texture, tx, ty, strength, layer) {
|
|
276
|
+
const spec = pass.spec;
|
|
277
|
+
let material;
|
|
278
|
+
if (spec.pass === 'custom') {
|
|
279
|
+
material = this.presentation.customMaterial(pass.pkg, spec.shader, { blending: layer ? 'normal' : 'normal', depthWrite: false });
|
|
280
|
+
if (!material) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
material.depthTest = false;
|
|
284
|
+
material.uniforms.uLayer.value = layer ? texture : material.uniforms.uLayer.value;
|
|
285
|
+
material.uniforms.uSceneColor.value = layer ? material.uniforms.uSceneColor.value : texture;
|
|
286
|
+
material.uniforms.uIntensity.value = strength;
|
|
287
|
+
if (!layer) {
|
|
288
|
+
material.blending = THREE.NoBlending;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
material = layer ? (blendingOf(spec.pass) === THREE.AdditiveBlending ? this.materials.additive : this.materials.normal) : this.materials.opaque;
|
|
292
|
+
const u = material.uniforms;
|
|
293
|
+
u.tSource.value = texture;
|
|
294
|
+
u.uTexel.value.set(tx, ty);
|
|
295
|
+
u.uPass.value = PASS_INDEX[spec.pass] ?? 0;
|
|
296
|
+
const c = Array.isArray(spec.color) && Array.isArray(spec.color[0]) ? spec.color[0] : spec.color || [255, 255, 255];
|
|
297
|
+
u.uColor.value.setRGB(c[0] / 255, c[1] / 255, c[2] / 255, THREE.SRGBColorSpace);
|
|
298
|
+
u.uStrength.value = layer ? strength : Math.min(1, strength);
|
|
299
|
+
u.uLayer.value = layer ? 1 : 0;
|
|
300
|
+
}
|
|
301
|
+
this.quad.material = material;
|
|
302
|
+
this.quad.render(renderer);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
snapshot() {
|
|
306
|
+
return {
|
|
307
|
+
layers: this.layers.map((l) => (l ? l.spec.pass : null)),
|
|
308
|
+
moments: this.moments.map((m) => ({ side: m.pkg.side, pass: m.spec.pass, scope: m.scope, left: m.to - this.presentation.clocks.cosmetic })),
|
|
309
|
+
drawn: this.drawn, refused: this.refused.slice(),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
dispose() {
|
|
314
|
+
if (this.target) {
|
|
315
|
+
this.target.dispose();
|
|
316
|
+
}
|
|
317
|
+
if (this.frame) {
|
|
318
|
+
this.frame.dispose();
|
|
319
|
+
}
|
|
320
|
+
if (this.quad) {
|
|
321
|
+
this.quad.dispose();
|
|
322
|
+
}
|
|
323
|
+
this.target = null;
|
|
324
|
+
this.frame = null;
|
|
325
|
+
this.quad = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The preview of a team package, a panel over the match view for authors. It opens with
|
|
5
|
+
// `preview=left` or `preview=right` in the page's address, on that side, as `cuju render pack
|
|
6
|
+
// preview` opens it, with `preview=1` on the side whose package loads, or from
|
|
7
|
+
// `cuju.presentation.preview.open()`. Buttons send events of the package's own team at once, so
|
|
8
|
+
// each reflex and each effect plays on demand, and meters show the team's budgets, the flash
|
|
9
|
+
// governor, the loudness of the master and of each team bus, and the worker's log. The panel
|
|
10
|
+
// reads what `cuju.presentation.status()` gives, which an agent may read the same way.
|
|
11
|
+
|
|
12
|
+
import { formatNumber, has, t } from '../core/i18n.js';
|
|
13
|
+
import { h } from '../teams/library.js';
|
|
14
|
+
import { icon, symbolOnly } from '../core/icons.js';
|
|
15
|
+
|
|
16
|
+
export const EVENTS = ['touch', 'pass', 'shot', 'header', 'save', 'slide', 'goal', 'celebration.begin', 'winner.reveal', 'mvp.reveal', 'halftime'];
|
|
17
|
+
const KINDS = ['particles', 'lights', 'localLights', 'shaders', 'voices', 'worklets', 'drawCalls', 'triangles', 'textureBytes', 'geometryBytes', 'audioBytes'];
|
|
18
|
+
|
|
19
|
+
// The side that the value of preview= in the page's address names, left or right, or null for
|
|
20
|
+
// the side whose package loads, as `preview=1` asks.
|
|
21
|
+
export function previewSide(value) {
|
|
22
|
+
const v = String(value || '').toLowerCase();
|
|
23
|
+
return v === 'left' ? 0 : v === 'right' ? 1 : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A number in the page's language with a fixed count of decimals.
|
|
27
|
+
const fixed = (x, digits) => formatNumber(Number(x) || 0, { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
28
|
+
const db = (x) => (x == null || !Number.isFinite(x) ? t('presentation.preview.none') : fixed(x, 1));
|
|
29
|
+
const count = (x) => formatNumber(Math.round(Number(x) || 0));
|
|
30
|
+
|
|
31
|
+
// A word of the contract, a level, a worker's status, a rung or a budget, in the page's
|
|
32
|
+
// language, the contract's own name for one the language lacks.
|
|
33
|
+
function word(group, value) {
|
|
34
|
+
const key = `presentation.preview.${group}.${value == null ? 'none' : value}`;
|
|
35
|
+
return has(key) ? t(key) : String(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Preview {
|
|
39
|
+
constructor(app) {
|
|
40
|
+
this.app = app;
|
|
41
|
+
this.panel = null;
|
|
42
|
+
this.side = 0;
|
|
43
|
+
// Whether the side follows the package the page loads, until the viewer picks one.
|
|
44
|
+
this.auto = true;
|
|
45
|
+
this.timer = null;
|
|
46
|
+
const check = () => {
|
|
47
|
+
const raw = location.hash.replace(/^#/, '');
|
|
48
|
+
const value = new URLSearchParams(raw).get('preview');
|
|
49
|
+
if (value && !this.panel) {
|
|
50
|
+
this.open(previewSide(value));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
addEventListener('hashchange', check);
|
|
54
|
+
check();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sends an event of the team on side, at once, to its reflexes and its worker.
|
|
58
|
+
inject(kind) {
|
|
59
|
+
return this.app.presentation.inject(this.side, kind);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
open(side = null) {
|
|
63
|
+
const p = this.app.presentation;
|
|
64
|
+
this.auto = side == null;
|
|
65
|
+
if (side == null) {
|
|
66
|
+
side = p.packages[0] ? 0 : p.packages[1] ? 1 : 0;
|
|
67
|
+
}
|
|
68
|
+
this.side = side;
|
|
69
|
+
if (this.panel) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const buttons = EVENTS.map((kind) => {
|
|
73
|
+
const b = h('button', { type: 'button', class: 'bar-btn', 'data-event': kind }, word('event', kind));
|
|
74
|
+
b.addEventListener('click', () => this.inject(kind));
|
|
75
|
+
return b;
|
|
76
|
+
});
|
|
77
|
+
const close = h('button', { type: 'button', class: 'bar-btn', ...symbolOnly(t('presentation.preview.closeLabel')) }, icon('close'));
|
|
78
|
+
close.addEventListener('click', () => this.close());
|
|
79
|
+
const sideButton = h('button', { type: 'button', class: 'bar-btn' }, icon('swap'), t('presentation.preview.otherSide'));
|
|
80
|
+
sideButton.addEventListener('click', () => {
|
|
81
|
+
this.side = 1 - this.side;
|
|
82
|
+
this.auto = false;
|
|
83
|
+
this.render();
|
|
84
|
+
});
|
|
85
|
+
this.body = h('div', { class: 'preview-body' });
|
|
86
|
+
this.panel = h('section', { id: 'presentation-preview', 'aria-label': t('presentation.preview.label'), class: 'panel',
|
|
87
|
+
style: 'position:fixed;right:12px;top:64px;width:340px;max-height:80vh;overflow:auto;z-index:40;padding:10px;font-size:12px;background:var(--panel-bg, rgba(16,20,28,0.92));color:var(--text, #eee);border-radius:8px' },
|
|
88
|
+
h('h2', { style: 'font-size:14px;margin:0 0 6px' }, t('presentation.preview.heading')),
|
|
89
|
+
h('div', { class: 'preview-events', style: 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px' }, ...buttons),
|
|
90
|
+
h('div', { style: 'display:flex;gap:4px;margin-bottom:8px' }, sideButton, close),
|
|
91
|
+
this.body);
|
|
92
|
+
document.body.appendChild(this.panel);
|
|
93
|
+
this.timer = setInterval(() => this.render(), 250);
|
|
94
|
+
this.render();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
close() {
|
|
98
|
+
clearInterval(this.timer);
|
|
99
|
+
if (this.panel) {
|
|
100
|
+
this.panel.remove();
|
|
101
|
+
}
|
|
102
|
+
this.panel = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Moves the panel below the offer of the 2D view while the offer shows, which sits in the
|
|
106
|
+
// same corner of the view.
|
|
107
|
+
avoidOffer() {
|
|
108
|
+
const offer = document.getElementById('flat-suggest');
|
|
109
|
+
const top = offer && !offer.hidden ? Math.round(offer.getBoundingClientRect().bottom) + 8 : 64;
|
|
110
|
+
this.panel.style.top = `${Math.max(64, top)}px`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The panel's figures, from the presentation's status.
|
|
114
|
+
render() {
|
|
115
|
+
if (!this.panel) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const s = this.app.presentation.status();
|
|
119
|
+
// A panel opened before the packages loaded follows the side that has one.
|
|
120
|
+
if (this.auto && !s.packages[this.side] && s.packages[1 - this.side]) {
|
|
121
|
+
this.side = 1 - this.side;
|
|
122
|
+
}
|
|
123
|
+
this.avoidOffer();
|
|
124
|
+
const pkg = s.packages[this.side];
|
|
125
|
+
const lines = [];
|
|
126
|
+
if (!pkg) {
|
|
127
|
+
lines.push([t('presentation.preview.package'), t('presentation.preview.noPackage', { side: word('side', this.side ? 'right' : 'left') })]);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push([t('presentation.preview.package'), t('presentation.preview.packageLine', {
|
|
130
|
+
name: pkg.name || pkg.team, level: word('level', pkg.level), status: word('status', pkg.status), rung: word('rung', pkg.budget.level),
|
|
131
|
+
})]);
|
|
132
|
+
lines.push([t('presentation.preview.mainThread'), t('presentation.preview.ofMs', { used: fixed(pkg.budget.mainMs || 0, 2), limit: fixed(pkg.budget.limits.mainMs, 2) })]);
|
|
133
|
+
for (const k of KINDS) {
|
|
134
|
+
if (pkg.budget.limits[k] != null) {
|
|
135
|
+
lines.push([word('kind', k), t('presentation.preview.of', { used: count(pkg.budget.used[k]), limit: count(pkg.budget.limits[k]) })]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const none = t('presentation.preview.none');
|
|
140
|
+
const fx = s.effects[this.side];
|
|
141
|
+
if (fx) {
|
|
142
|
+
lines.push([t('presentation.preview.effects'), t('presentation.preview.effectsLine', {
|
|
143
|
+
instances: count(fx.instances), particles: count(fx.particles), decals: count(fx.decals), lit: count(fx.poolLit), lights: count(fx.poolLights), local: count(fx.localLights),
|
|
144
|
+
})]);
|
|
145
|
+
}
|
|
146
|
+
lines.push([t('presentation.preview.flashes'), t(s.flash.clamped ? 'presentation.preview.flashesClamped' : 'presentation.preview.flashesLine', {
|
|
147
|
+
general: count(s.flash.counts.general), red: count(s.flash.counts.red), factor: fixed(s.flash.factor, 2),
|
|
148
|
+
})]);
|
|
149
|
+
const m = s.audio.master;
|
|
150
|
+
lines.push([t('presentation.preview.master'), m ? t('presentation.preview.masterLine', {
|
|
151
|
+
momentary: db(m.momentary), shortTerm: db(m.shortTerm), integrated: db(m.integrated), truePeak: db(m.truePeak),
|
|
152
|
+
}) : t('presentation.preview.notMetered')]);
|
|
153
|
+
const bus = s.audio.teams[this.side];
|
|
154
|
+
if (bus) {
|
|
155
|
+
const e = bus.meters.effects;
|
|
156
|
+
lines.push([t('presentation.preview.bus'), e ? t(bus.penalized ? 'presentation.preview.busPenalized' : 'presentation.preview.busLine', {
|
|
157
|
+
momentary: db(e.momentary), shortTerm: db(e.shortTerm), down: db(e.reduction),
|
|
158
|
+
}) : t('presentation.preview.quiet')]);
|
|
159
|
+
lines.push([t('presentation.preview.voices'), t('presentation.preview.voicesLine', {
|
|
160
|
+
voices: count(bus.voices), music: bus.music.join(', ') || none, processors: bus.processors.join(', ') || none,
|
|
161
|
+
})]);
|
|
162
|
+
}
|
|
163
|
+
lines.push([t('presentation.preview.moments'), t('presentation.preview.momentsLine', {
|
|
164
|
+
camera: count(s.moments.accepted.camera), time: count(s.moments.accepted.time), environment: count(s.moments.accepted.environment),
|
|
165
|
+
})]);
|
|
166
|
+
if (s.moments.refused.length) {
|
|
167
|
+
lines.push([t('presentation.preview.refused'), s.moments.refused.slice(-3).join(', ')]);
|
|
168
|
+
}
|
|
169
|
+
lines.push([t('presentation.preview.passes'), t('presentation.preview.passesLine', {
|
|
170
|
+
layers: s.post.layers.filter(Boolean).join(', ') || t('presentation.preview.noLayer'), moments: count(s.post.moments.length), drawn: count(s.post.drawn),
|
|
171
|
+
})]);
|
|
172
|
+
if (pkg) {
|
|
173
|
+
lines.push([t('presentation.preview.log'), (pkg.log || []).slice(-5).join(', ') || t('presentation.preview.empty')]);
|
|
174
|
+
if (pkg.errors.length) {
|
|
175
|
+
lines.push([t('presentation.preview.errors'), pkg.errors.slice(-3).join(', ')]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.body.replaceChildren(...lines.map(([k, v]) => h('div', { class: 'preview-line' }, h('strong', {}, `${k} `), v)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The messages between the renderer and a team's worker, section 11 of the contract.
|
|
5
|
+
//
|
|
6
|
+
// The renderer sends a Hello once, then a Frame on every drawing while no reply is outstanding,
|
|
7
|
+
// with the state of the drawings in between coalesced and their events accumulated, never
|
|
8
|
+
// dropped. The worker answers each Frame with a Response that names the Frame's instance, epoch
|
|
9
|
+
// and frame id. A reply of another instance or an older epoch is dropped, and every command in a
|
|
10
|
+
// reply is checked and held to its ranges before it takes effect.
|
|
11
|
+
|
|
12
|
+
import { checkBehaviour } from './behaviors.js';
|
|
13
|
+
import {
|
|
14
|
+
checkAudio, checkCamera, checkClaim, checkEffect, checkOverlay, checkResource, checkTime,
|
|
15
|
+
} from './commands.js';
|
|
16
|
+
import { CONTRACT } from './contract.js';
|
|
17
|
+
import { checkTrack } from './tracks.js';
|
|
18
|
+
import { clamp } from './values.js';
|
|
19
|
+
|
|
20
|
+
// The most commands of each kind one reply may carry, and the largest reply in characters.
|
|
21
|
+
export const MAXIMUM_COMMANDS = 64;
|
|
22
|
+
export const MAXIMUM_REPLY = 512 * 1024;
|
|
23
|
+
// The lines of a worker's log that the page keeps, the newest last.
|
|
24
|
+
export const LOG_LINES = 50;
|
|
25
|
+
|
|
26
|
+
// The log of a worker with the lines of a new reply added, the oldest dropped past max.
|
|
27
|
+
export function appendLog(log, lines, max = LOG_LINES) {
|
|
28
|
+
const out = (log || []).concat(lines || []);
|
|
29
|
+
return out.length > max ? out.slice(out.length - max) : out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const round = (x) => Math.round(x * 1e4) / 1e4;
|
|
33
|
+
const roundAll = (v) => (Array.isArray(v) ? v.map(round) : v);
|
|
34
|
+
|
|
35
|
+
// The Hello of an instance, section 11.2.
|
|
36
|
+
export function helloMessage(h) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'hello',
|
|
39
|
+
contract: CONTRACT,
|
|
40
|
+
features: h.features,
|
|
41
|
+
lacking: h.lacking || [],
|
|
42
|
+
limits: h.limits,
|
|
43
|
+
quality: h.quality,
|
|
44
|
+
reducedMotion: !!h.reducedMotion,
|
|
45
|
+
instanceId: h.instanceId,
|
|
46
|
+
team: h.team,
|
|
47
|
+
attack: h.attack,
|
|
48
|
+
roster: h.roster,
|
|
49
|
+
header: h.header,
|
|
50
|
+
seedBase: h.seedBase,
|
|
51
|
+
slots: h.slots || [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A Frame of the protocol, section 11.3, from the renderer's snapshot of one drawing.
|
|
56
|
+
export function frameMessage(s) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'frame',
|
|
59
|
+
instanceId: s.instanceId,
|
|
60
|
+
epoch: s.epoch,
|
|
61
|
+
frameId: s.frameId,
|
|
62
|
+
reset: s.reset || null,
|
|
63
|
+
clocks: s.clocks,
|
|
64
|
+
playback: s.playback,
|
|
65
|
+
quality: s.quality,
|
|
66
|
+
reducedMotion: !!s.reducedMotion,
|
|
67
|
+
audible: !!s.audible,
|
|
68
|
+
players: s.players.map((p) => ({
|
|
69
|
+
...p,
|
|
70
|
+
root: roundAll(p.root),
|
|
71
|
+
facing: round(p.facing),
|
|
72
|
+
velocity: roundAll(p.velocity),
|
|
73
|
+
speed: round(p.speed),
|
|
74
|
+
distance: round(p.distance),
|
|
75
|
+
})),
|
|
76
|
+
ball: s.ball ? { ...s.ball, pos: roundAll(s.ball.pos), velocity: roundAll(s.ball.velocity) } : null,
|
|
77
|
+
game: s.game,
|
|
78
|
+
events: s.events,
|
|
79
|
+
camera: s.camera,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function list(value) {
|
|
84
|
+
return Array.isArray(value) ? value.slice(0, MAXIMUM_COMMANDS) : [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Checks a reply against what the renderer expects. expect holds instanceId, epoch, own, the set
|
|
88
|
+
// of the team's own entity ids, envelope, a function from an entity id to its conservative bounds,
|
|
89
|
+
// and files, the set of the package's files. Returns {ok, reason, response} with every command
|
|
90
|
+
// checked, or ok false with the reason, stale, foreign or malformed.
|
|
91
|
+
export function checkResponse(message, expect) {
|
|
92
|
+
if (!message || typeof message !== 'object' || message.type !== 'response') {
|
|
93
|
+
return { ok: false, reason: 'malformed' };
|
|
94
|
+
}
|
|
95
|
+
if (message.instanceId !== expect.instanceId) {
|
|
96
|
+
return { ok: false, reason: 'foreign' };
|
|
97
|
+
}
|
|
98
|
+
if (message.epoch !== expect.epoch) {
|
|
99
|
+
return { ok: false, reason: 'stale' };
|
|
100
|
+
}
|
|
101
|
+
const own = expect.own;
|
|
102
|
+
const r = {
|
|
103
|
+
instanceId: message.instanceId, epoch: message.epoch, sourceFrameId: Number(message.sourceFrameId) || 0,
|
|
104
|
+
ms: clamp(Number(message.ms) || 0, 0, 1e6), resources: [], behaviours: [], tracks: [], effects: [],
|
|
105
|
+
claims: [], audio: [], camera: null, time: null, overlays: [], bounds: [], log: [], rejected: 0,
|
|
106
|
+
};
|
|
107
|
+
const keep = (target, value) => {
|
|
108
|
+
if (value) {
|
|
109
|
+
target.push(value);
|
|
110
|
+
} else {
|
|
111
|
+
r.rejected++;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
for (const c of list(message.resources)) {
|
|
115
|
+
keep(r.resources, checkResource(c, expect.files));
|
|
116
|
+
}
|
|
117
|
+
for (const b of list(message.behaviours)) {
|
|
118
|
+
const op = b && (b.op || 'set');
|
|
119
|
+
const entity = b && b.entity != null ? String(b.entity) : null;
|
|
120
|
+
if (!b || !['set', 'update', 'stop'].includes(op) || (entity !== null && !own.has(entity))) {
|
|
121
|
+
r.rejected++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
if (op === 'set') {
|
|
126
|
+
r.behaviours.push({ op, entity, behaviour: checkBehaviour({ ...b.behaviour, id: b.id ?? (b.behaviour && b.behaviour.id) }) });
|
|
127
|
+
} else if (op === 'update') {
|
|
128
|
+
r.behaviours.push({ op, entity, id: String(b.id), patch: { ...(b.behaviour || {}) } });
|
|
129
|
+
} else {
|
|
130
|
+
r.behaviours.push({ op, entity, id: String(b.id), fadeOut: clamp(Number(b.fadeOut ?? 0.3) || 0, 0, 10) });
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
r.rejected++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const t of list(message.tracks)) {
|
|
137
|
+
keep(r.tracks, checkTrack({ ...t, source: 'worker' }, own));
|
|
138
|
+
}
|
|
139
|
+
for (const e of list(message.effects)) {
|
|
140
|
+
keep(r.effects, checkEffect(e, own));
|
|
141
|
+
}
|
|
142
|
+
for (const c of list(message.claims)) {
|
|
143
|
+
keep(r.claims, checkClaim(c));
|
|
144
|
+
}
|
|
145
|
+
for (const a of list(message.audio)) {
|
|
146
|
+
keep(r.audio, checkAudio(a, own));
|
|
147
|
+
}
|
|
148
|
+
for (const o of list(message.overlays)) {
|
|
149
|
+
keep(r.overlays, checkOverlay(o));
|
|
150
|
+
}
|
|
151
|
+
if (message.camera != null) {
|
|
152
|
+
r.camera = checkCamera(message.camera);
|
|
153
|
+
r.rejected += r.camera ? 0 : 1;
|
|
154
|
+
}
|
|
155
|
+
if (message.time != null) {
|
|
156
|
+
r.time = checkTime(message.time);
|
|
157
|
+
r.rejected += r.time ? 0 : 1;
|
|
158
|
+
}
|
|
159
|
+
for (const b of list(message.bounds)) {
|
|
160
|
+
const entity = b && String(b.entity);
|
|
161
|
+
const envelope = own.has(entity) && expect.envelope ? expect.envelope(entity) : null;
|
|
162
|
+
const current = b && b.current;
|
|
163
|
+
if (!envelope || !current) {
|
|
164
|
+
r.rejected++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Current bounds lie inside the conservative envelope, which framing may always trust.
|
|
168
|
+
r.bounds.push({ entity, current: { radius: clamp(Number(current.radius) || 0, 0.05, envelope.radius), height: clamp(Number(current.height) || 0, 0.2, envelope.height) } });
|
|
169
|
+
}
|
|
170
|
+
for (const line of list(message.log)) {
|
|
171
|
+
r.log.push(String(line).slice(0, 300));
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, response: r };
|
|
174
|
+
}
|