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,257 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The installable page. On the loopback address of the server, in a window of its own rather than
|
|
5
|
+
// a notebook's frame, the page registers its service worker, sw.js, with the build of the page
|
|
6
|
+
// that the server names, and Chrome and Edge then offer to install it as an app. The Help page
|
|
7
|
+
// shows whether the page is installed and offers Install where the browser allows it, Update once
|
|
8
|
+
// the server holds a newer build, and the removal of the copy that the worker keeps. Safari on a
|
|
9
|
+
// Mac adds the page to the Dock from its File menu, which Help then names, and every browser
|
|
10
|
+
// reaches an app window through the command cuju render app, which Help names as well. The
|
|
11
|
+
// browser's offer comes from install-state.js, which the card on Home shares. Scripts reach it as
|
|
12
|
+
// window.cuju.install.
|
|
13
|
+
|
|
14
|
+
import { t, tn } from '../core/i18n.js';
|
|
15
|
+
import { icon } from '../core/icons.js';
|
|
16
|
+
import { heldPrompt, installRoute, installedNow, onInstallChange, promptInstall, runsAsApp } from './install-state.js';
|
|
17
|
+
|
|
18
|
+
const LOOPBACK = ['127.0.0.1', 'localhost', '[::1]'];
|
|
19
|
+
const PREFIX = 'cuju-page-';
|
|
20
|
+
|
|
21
|
+
function h(tag, attributes = {}, ...children) {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
24
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
25
|
+
node.addEventListener(key.slice(2), value);
|
|
26
|
+
} else if (value !== null && value !== undefined && value !== false) {
|
|
27
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
node.append(...children.flat().filter((child) => child !== null && child !== undefined));
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whether this page may register its worker, on http at a loopback host and at the top.
|
|
35
|
+
export function installable(where = location, framed = null) {
|
|
36
|
+
let top = framed;
|
|
37
|
+
if (top === null) {
|
|
38
|
+
try {
|
|
39
|
+
top = window.top !== window;
|
|
40
|
+
} catch {
|
|
41
|
+
top = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return where.protocol === 'http:' && LOOPBACK.includes(where.hostname) && !top
|
|
45
|
+
&& typeof navigator !== 'undefined' && 'serviceWorker' in navigator;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The build of a worker, from its address.
|
|
49
|
+
export function workerBuild(scriptURL) {
|
|
50
|
+
try {
|
|
51
|
+
return new URL(scriptURL).searchParams.get('build');
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function serverBuild() {
|
|
58
|
+
const res = await fetch('/api/help/app', { cache: 'no-store' });
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(t('pwa.error.status', { status: res.status }));
|
|
61
|
+
}
|
|
62
|
+
return (await res.json()).build;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class AppInstall {
|
|
66
|
+
constructor(app, host) {
|
|
67
|
+
this.app = app;
|
|
68
|
+
this.host = host;
|
|
69
|
+
this.supported = installable();
|
|
70
|
+
this.registration = null;
|
|
71
|
+
this.build = null;
|
|
72
|
+
this.latest = null;
|
|
73
|
+
this.error = null;
|
|
74
|
+
this.route = installRoute({
|
|
75
|
+
hasEvent: 'onbeforeinstallprompt' in window, userAgent: navigator.userAgent, vendor: navigator.vendor,
|
|
76
|
+
});
|
|
77
|
+
this.status = h('p', { class: 'muted small', role: 'status', 'aria-live': 'polite' });
|
|
78
|
+
this.installButton = h('button', { type: 'button', class: 'btn primary', hidden: true, onclick: () => this.install() }, icon('download'), t('pwa.install'));
|
|
79
|
+
this.updateButton = h('button', { type: 'button', class: 'btn', hidden: true, onclick: () => this.update(true) }, icon('refresh'), t('pwa.update'));
|
|
80
|
+
this.forgetButton = h('button', { type: 'button', class: 'btn', hidden: true, onclick: () => this.unregister() }, icon('trash'), t('pwa.remove'));
|
|
81
|
+
const code = (text) => h('code', {}, text);
|
|
82
|
+
host.replaceChildren(
|
|
83
|
+
h('h2', { id: 'help-install-title' }, t('pwa.title')),
|
|
84
|
+
h('p', {}, ...tn('pwa.text')),
|
|
85
|
+
h('div', { class: 'help-row' }, this.installButton, this.updateButton, this.forgetButton),
|
|
86
|
+
this.status,
|
|
87
|
+
// The command that opens cuju in a window of its own shows the symbol of a terminal.
|
|
88
|
+
h('p', { id: 'help-install-command' }, icon('terminal', 'icon text-symbol'), tn('pwa.command.text', {
|
|
89
|
+
command: code('cuju render app'), install: code('cuju render app --install'),
|
|
90
|
+
})));
|
|
91
|
+
onInstallChange(() => this.render());
|
|
92
|
+
if (this.supported) {
|
|
93
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => this.render());
|
|
94
|
+
}
|
|
95
|
+
const page = document.getElementById('page-help');
|
|
96
|
+
if (page) {
|
|
97
|
+
new MutationObserver(() => {
|
|
98
|
+
if (!page.hidden) {
|
|
99
|
+
this.check();
|
|
100
|
+
}
|
|
101
|
+
}).observe(page, { attributes: true, attributeFilter: ['hidden'] });
|
|
102
|
+
}
|
|
103
|
+
this.ready = this.register();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async register() {
|
|
107
|
+
if (!this.supported) {
|
|
108
|
+
this.render();
|
|
109
|
+
return this.state();
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
this.build = await serverBuild();
|
|
113
|
+
this.latest = this.build;
|
|
114
|
+
this.registration = await navigator.serviceWorker.register(`sw.js?build=${encodeURIComponent(this.build)}`, {
|
|
115
|
+
scope: '../', updateViaCache: 'none',
|
|
116
|
+
});
|
|
117
|
+
this.registration.addEventListener('updatefound', () => this.render());
|
|
118
|
+
this.error = null;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
this.error = err.message;
|
|
121
|
+
}
|
|
122
|
+
this.render();
|
|
123
|
+
return this.state();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Asks the server for its build, which differs from the page's once a new version is there.
|
|
127
|
+
async check() {
|
|
128
|
+
if (!this.supported) {
|
|
129
|
+
return this.state();
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
this.latest = await serverBuild();
|
|
133
|
+
} catch {
|
|
134
|
+
// The server is away, the page stays as it is.
|
|
135
|
+
}
|
|
136
|
+
this.render();
|
|
137
|
+
return this.state();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async install() {
|
|
141
|
+
const result = await promptInstall();
|
|
142
|
+
this.render();
|
|
143
|
+
if (result.outcome === 'unavailable') {
|
|
144
|
+
this.status.textContent = t(this.route === 'browser' ? 'pwa.unavailable' : `pwa.route.${this.route}`);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Takes the newest build, the worker of the server's build, and with reload shows it at once.
|
|
150
|
+
async update(reload = false) {
|
|
151
|
+
await this.check();
|
|
152
|
+
if (this.supported && this.latest && this.latest !== this.build) {
|
|
153
|
+
this.build = this.latest;
|
|
154
|
+
this.registration = await navigator.serviceWorker.register(`sw.js?build=${encodeURIComponent(this.build)}`, {
|
|
155
|
+
scope: '../', updateViaCache: 'none',
|
|
156
|
+
});
|
|
157
|
+
if (reload) {
|
|
158
|
+
location.reload();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.render();
|
|
162
|
+
return this.state();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Removes the worker and the copy it keeps, until the page registers it again on its next load.
|
|
166
|
+
async unregister() {
|
|
167
|
+
let removed = false;
|
|
168
|
+
if (this.supported) {
|
|
169
|
+
for (const registration of await navigator.serviceWorker.getRegistrations()) {
|
|
170
|
+
removed = (await registration.unregister()) || removed;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (typeof caches !== 'undefined') {
|
|
174
|
+
for (const name of await caches.keys()) {
|
|
175
|
+
if (name.startsWith(PREFIX)) {
|
|
176
|
+
await caches.delete(name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this.registration = null;
|
|
181
|
+
this.render();
|
|
182
|
+
this.status.textContent = t('pwa.removed');
|
|
183
|
+
return removed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
state() {
|
|
187
|
+
const controller = this.supported ? navigator.serviceWorker.controller : null;
|
|
188
|
+
const standalone = runsAsApp();
|
|
189
|
+
return {
|
|
190
|
+
supported: this.supported,
|
|
191
|
+
registered: !!this.registration,
|
|
192
|
+
build: this.build,
|
|
193
|
+
latest: this.latest,
|
|
194
|
+
controlled: !!controller,
|
|
195
|
+
controllerBuild: controller ? workerBuild(controller.scriptURL) : null,
|
|
196
|
+
waiting: !!(this.registration && (this.registration.waiting || this.registration.installing)),
|
|
197
|
+
updateReady: !!(this.latest && this.build && this.latest !== this.build),
|
|
198
|
+
installable: !!heldPrompt(),
|
|
199
|
+
installed: installedNow(),
|
|
200
|
+
standalone,
|
|
201
|
+
route: this.route,
|
|
202
|
+
error: this.error,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render() {
|
|
207
|
+
const state = this.state();
|
|
208
|
+
this.installButton.hidden = !state.installable || state.standalone;
|
|
209
|
+
this.updateButton.hidden = !state.updateReady;
|
|
210
|
+
this.forgetButton.hidden = !state.registered;
|
|
211
|
+
// The line opens with the symbol of its state where one reads clearly, a ticked circle for
|
|
212
|
+
// an installed app, a warning for an error and a terminal for the command.
|
|
213
|
+
let text;
|
|
214
|
+
let symbol = null;
|
|
215
|
+
if (!this.supported) {
|
|
216
|
+
text = tn('pwa.state.unsupported');
|
|
217
|
+
} else if (state.error) {
|
|
218
|
+
text = tn('pwa.state.error', { error: state.error });
|
|
219
|
+
symbol = 'warning';
|
|
220
|
+
} else if (state.standalone) {
|
|
221
|
+
text = tn('pwa.state.standalone');
|
|
222
|
+
symbol = 'done';
|
|
223
|
+
} else if (state.installable) {
|
|
224
|
+
text = tn('pwa.state.installable');
|
|
225
|
+
} else if (state.installed) {
|
|
226
|
+
text = tn('pwa.state.installed');
|
|
227
|
+
symbol = 'done';
|
|
228
|
+
} else if (this.route !== 'browser') {
|
|
229
|
+
// Safari names its own menu, and another browser the command that makes the app.
|
|
230
|
+
text = tn(`pwa.route.${this.route}`);
|
|
231
|
+
symbol = this.route === 'command' ? 'terminal' : null;
|
|
232
|
+
} else if (state.controlled) {
|
|
233
|
+
text = tn('pwa.state.keptMenu');
|
|
234
|
+
} else {
|
|
235
|
+
text = tn('pwa.state.menu');
|
|
236
|
+
}
|
|
237
|
+
// A second sentence follows the first, as its own message, and each keeps its marks.
|
|
238
|
+
if (state.updateReady) {
|
|
239
|
+
text.push(' ', ...tn('pwa.state.updateReady'));
|
|
240
|
+
} else if (state.controlled && (state.standalone || state.installable || state.installed || this.route !== 'browser')) {
|
|
241
|
+
text.push(' ', ...tn('pwa.state.kept'));
|
|
242
|
+
}
|
|
243
|
+
this.status.replaceChildren(...(symbol ? [icon(symbol, 'icon text-symbol')] : []), ...text);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The functions of window.cuju.install.
|
|
247
|
+
script() {
|
|
248
|
+
return {
|
|
249
|
+
state: () => this.state(),
|
|
250
|
+
install: () => this.install(),
|
|
251
|
+
update: () => this.update(false),
|
|
252
|
+
check: () => this.check(),
|
|
253
|
+
unregister: () => this.unregister(),
|
|
254
|
+
ready: () => this.ready,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The steps of the guided tour and the rules that move through them. Each step names a page, the
|
|
5
|
+
// control it points at as a selector, a title and a line of text, and what it needs to show, a
|
|
6
|
+
// recording that plays or a team of the registry, which the tour offers to add first, and what
|
|
7
|
+
// its Try it button does, with the reason written beside each step. The browser keeps the step
|
|
8
|
+
// reached, so a tour left midway resumes there. Nothing here touches the page, so
|
|
9
|
+
// the rules hold the same in a test. The title and text of a step are the messages
|
|
10
|
+
// tour.step.<id>.title and tour.step.<id>.text.
|
|
11
|
+
|
|
12
|
+
import { t } from '../core/i18n.js';
|
|
13
|
+
|
|
14
|
+
export const TOUR_KEY = 'cuju.tour';
|
|
15
|
+
|
|
16
|
+
export const TOUR_STEPS = [
|
|
17
|
+
{
|
|
18
|
+
// None, the logo leads to Home, the page this step already shows.
|
|
19
|
+
id: 'home', page: 'home', target: '#home-btn', title: t('tour.step.home.title'),
|
|
20
|
+
text: t('tour.step.home.text'), tryIt: null,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// Open, the button opens the command palette, which takes the focus to its search field.
|
|
24
|
+
id: 'palette', page: 'home', target: '#open-palette', title: t('tour.step.palette.title'),
|
|
25
|
+
text: t('tour.step.palette.text'), tryIt: 'open',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
// Open, the button opens the Match menu, and the focus moves to its first item.
|
|
29
|
+
id: 'match', page: 'watch', target: '#match-menu-btn', title: t('tour.step.match.title'),
|
|
30
|
+
text: t('tour.step.match.text'), tryIt: 'open',
|
|
31
|
+
needs: 'recording',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// Focus, the timeline is a slider moved by keys, so the focus moves to it and the keys
|
|
35
|
+
// that seek are read out.
|
|
36
|
+
id: 'timeline', page: 'watch', target: '#timeline', title: t('tour.step.timeline.title'),
|
|
37
|
+
text: t('tour.step.timeline.text'), tryIt: 'focus',
|
|
38
|
+
needs: 'recording',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
// Open, the button opens the View menu, and the focus moves to its first item.
|
|
42
|
+
id: 'view', page: 'watch', target: '#view-menu-btn', title: t('tour.step.view.title'),
|
|
43
|
+
text: t('tour.step.view.text'), tryIt: 'open',
|
|
44
|
+
needs: 'recording',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
// None, the Play button starts a match, which leaves the tour behind, and the setup it
|
|
48
|
+
// belongs to already shows on this page.
|
|
49
|
+
id: 'play', page: 'play', target: '.play-setup .play-row .btn.primary', title: t('tour.step.play.title'),
|
|
50
|
+
text: t('tour.step.play.text'), tryIt: null,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// Open, the Leaderboard button of the first challenge opens its leaderboard on the card,
|
|
54
|
+
// which the same button closes again. Play it yourself and Run a model beside it start
|
|
55
|
+
// matches, so the step points past them.
|
|
56
|
+
id: 'challenges', page: 'challenges', target: '#page-challenges .challenge-item [data-action="board"]',
|
|
57
|
+
title: t('tour.step.challenges.title'), text: t('tour.step.challenges.text'), tryIt: 'open',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
// None, the target is the list of teams, and adding a team uploads a folder.
|
|
61
|
+
id: 'teams', page: 'teams', target: '#teams-side', title: t('tour.step.teams.title'),
|
|
62
|
+
text: t('tour.step.teams.text'), tryIt: null,
|
|
63
|
+
needs: 'team',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// Open, New tournament opens the wizard beside the list on this page, a draft that
|
|
67
|
+
// nothing saves before its last step and that Cancel puts away.
|
|
68
|
+
id: 'tournaments', page: 'tournaments', target: '.cup-side-head .btn', title: t('tour.step.tournaments.title'),
|
|
69
|
+
text: t('tour.step.tournaments.text'), tryIt: 'open',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
// Open, the button opens the settings panel, and the focus moves to its first field.
|
|
73
|
+
id: 'settings', page: 'home', target: '#open-settings', title: t('tour.step.settings.title'),
|
|
74
|
+
text: t('tour.step.settings.text'), tryIt: 'open',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
// None, the target is the heading of the Learn by doing part, and its lessons start
|
|
78
|
+
// tasks of their own.
|
|
79
|
+
id: 'help', page: 'help', target: '#help-learn-title', title: t('tour.step.help.title'),
|
|
80
|
+
text: t('tour.step.help.text'), tryIt: null,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// What Try it does on a step. Open clicks the control as a person would, so whatever it opens
|
|
85
|
+
// behaves as usual and keeps the focus where it puts it, and focus moves the focus to a control
|
|
86
|
+
// used by keys and says which keys move it, the message tour.try.<id>. A step with neither shows
|
|
87
|
+
// no Try it.
|
|
88
|
+
export const TRY_KINDS = ['open', 'focus'];
|
|
89
|
+
|
|
90
|
+
// The offer of a step whose need is missing.
|
|
91
|
+
export const NEED_OFFERS = {
|
|
92
|
+
recording: { label: t('tour.need.recording.label'), text: t('tour.need.recording.text') },
|
|
93
|
+
team: { label: t('tour.need.team.label'), text: t('tour.need.team.text') },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// The stored state of the tour, the step reached and whether the tour ended, kept in range.
|
|
97
|
+
export function tourState(stored, count = TOUR_STEPS.length) {
|
|
98
|
+
const source = stored && typeof stored === 'object' ? stored : {};
|
|
99
|
+
const step = Number.isInteger(source.step) ? Math.max(0, Math.min(count - 1, source.step)) : 0;
|
|
100
|
+
return { step, done: source.done === true, started: source.started === true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The state after moving by delta steps. Moving past the last step ends the tour, moving before
|
|
104
|
+
// the first stays there.
|
|
105
|
+
export function moveTour(state, delta, count = TOUR_STEPS.length) {
|
|
106
|
+
const clean = tourState(state, count);
|
|
107
|
+
const next = clean.step + delta;
|
|
108
|
+
if (next >= count) {
|
|
109
|
+
return { step: 0, done: true, started: false };
|
|
110
|
+
}
|
|
111
|
+
return { step: Math.max(0, next), done: false, started: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The step where the tour starts, the one reached when a tour was left midway.
|
|
115
|
+
export function resumeStep(state, count = TOUR_STEPS.length) {
|
|
116
|
+
const clean = tourState(state, count);
|
|
117
|
+
return clean.started && !clean.done ? clean.step : 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Whether a step can show as it stands, given what the page holds.
|
|
121
|
+
export function needMet(step, facts) {
|
|
122
|
+
if (!step || !step.needs) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return !!(facts && facts[step.needs]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The words a screen reader hears for a step.
|
|
129
|
+
export function stepAnnouncement(step, index, count = TOUR_STEPS.length, found = true) {
|
|
130
|
+
return t(found ? 'tour.announce' : 'tour.announce.missing', { index: index + 1, count, title: step.title, text: step.text });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The edges of a box given by its left and top and either its right and bottom or its size.
|
|
134
|
+
function edges(box) {
|
|
135
|
+
const right = Number.isFinite(box.right) ? box.right : box.left + (box.width || 0);
|
|
136
|
+
const bottom = Number.isFinite(box.bottom) ? box.bottom : box.top + (box.height || 0);
|
|
137
|
+
return { left: box.left, top: box.top, right, bottom };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Whether two boxes share some area, and boxes that only touch do not.
|
|
141
|
+
const overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
142
|
+
|
|
143
|
+
// The distance between two boxes, zero when they overlap or touch.
|
|
144
|
+
const apart = (a, b) => Math.hypot(Math.max(0, b.left - a.right, a.left - b.right), Math.max(0, b.top - a.bottom, a.top - b.bottom));
|
|
145
|
+
|
|
146
|
+
// The smallest the card becomes when it docks in a corner.
|
|
147
|
+
const DOCK_WIDTH = 240;
|
|
148
|
+
const DOCK_HEIGHT = 120;
|
|
149
|
+
|
|
150
|
+
// The card docked in the corner of the window farthest from the boxes, at its full size when a
|
|
151
|
+
// corner has room for it, else made narrower or shorter until it clears them, or null when no
|
|
152
|
+
// corner has room for the smallest card.
|
|
153
|
+
function dock(boxes, width, height, view, margin) {
|
|
154
|
+
const minWidth = Math.min(width, DOCK_WIDTH);
|
|
155
|
+
const minHeight = Math.min(height, DOCK_HEIGHT);
|
|
156
|
+
const at = (right, bottom, w, h) => {
|
|
157
|
+
const left = right ? view.width - margin - w : margin;
|
|
158
|
+
const top = bottom ? view.height - margin - h : margin;
|
|
159
|
+
return { left, top, right: left + w, bottom: top + h };
|
|
160
|
+
};
|
|
161
|
+
let best = null;
|
|
162
|
+
for (const [right, bottom] of [[false, false], [true, false], [false, true], [true, true]]) {
|
|
163
|
+
let w = width;
|
|
164
|
+
let h = height;
|
|
165
|
+
let clear = false;
|
|
166
|
+
for (let pass = 0; pass <= boxes.length; pass++) {
|
|
167
|
+
const hit = boxes.find((box) => overlaps(at(right, bottom, w, h), box));
|
|
168
|
+
if (!hit) {
|
|
169
|
+
clear = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
// The card keeps clear of the box by its width or by its height, whichever keeps more
|
|
173
|
+
// of the card.
|
|
174
|
+
const freeWidth = Math.floor(right ? view.width - margin - (hit.right + margin) : hit.left - 2 * margin);
|
|
175
|
+
const freeHeight = Math.floor(bottom ? view.height - margin - (hit.bottom + margin) : hit.top - 2 * margin);
|
|
176
|
+
const byWidth = freeWidth >= minWidth ? Math.min(w, freeWidth) * h : -1;
|
|
177
|
+
const byHeight = freeHeight >= minHeight ? w * Math.min(h, freeHeight) : -1;
|
|
178
|
+
if (byWidth < 0 && byHeight < 0) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (byWidth >= byHeight) {
|
|
182
|
+
w = Math.min(w, freeWidth);
|
|
183
|
+
} else {
|
|
184
|
+
h = Math.min(h, freeHeight);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!clear) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const spot = at(right, bottom, w, h);
|
|
191
|
+
const area = w * h;
|
|
192
|
+
const distance = Math.min(...boxes.map((box) => apart(spot, box)));
|
|
193
|
+
if (!best || area > best.area || (area === best.area && distance > best.distance)) {
|
|
194
|
+
best = { spot, w, h, area, distance };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return best ? {
|
|
198
|
+
left: Math.round(best.spot.left), top: Math.round(best.spot.top), width: best.w, height: best.h, side: 'dock',
|
|
199
|
+
} : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Where the card of a step goes beside its target, below it when there is room, above it when
|
|
203
|
+
// not, then to its right or left, each only where the card covers neither the target nor any of
|
|
204
|
+
// the boxes to avoid, such as a menu or a panel open over the page. When no side is clear the
|
|
205
|
+
// card docks in the corner of the window farthest from them, smaller when it must, and when no
|
|
206
|
+
// corner is clear either it goes over the target at the bottom of the window. Without a target
|
|
207
|
+
// the card goes to the bottom of the window, or docks when a box covers that spot. Every value is
|
|
208
|
+
// a pixel position of the window, inside its margins, and a docked card also has its width and
|
|
209
|
+
// height.
|
|
210
|
+
export function placeCard(target, card, view, avoid = [], margin = 12) {
|
|
211
|
+
const width = Math.min(card.width, view.width - 2 * margin);
|
|
212
|
+
const boxes = [target, ...(avoid || [])].filter(Boolean).map(edges);
|
|
213
|
+
const clear = (left, top) => !boxes.some((box) => overlaps({ left, top, right: left + width, bottom: top + card.height }, box));
|
|
214
|
+
const docked = () => dock(boxes, width, Math.min(card.height, view.height - 2 * margin), view, margin);
|
|
215
|
+
if (!target) {
|
|
216
|
+
const spot = { left: Math.round((view.width - width) / 2), top: Math.round(view.height - card.height - margin), side: 'none' };
|
|
217
|
+
return clear(spot.left, spot.top) ? spot : docked() || spot;
|
|
218
|
+
}
|
|
219
|
+
const box = edges(target);
|
|
220
|
+
const left = Math.round(Math.max(margin, Math.min(view.width - width - margin, box.left + (box.right - box.left) / 2 - width / 2)));
|
|
221
|
+
const sides = [];
|
|
222
|
+
if (box.bottom + margin + card.height <= view.height - margin) {
|
|
223
|
+
sides.push({ left, top: Math.round(box.bottom + margin), side: 'below' });
|
|
224
|
+
}
|
|
225
|
+
if (box.top - margin - card.height >= margin) {
|
|
226
|
+
sides.push({ left, top: Math.round(box.top - margin - card.height), side: 'above' });
|
|
227
|
+
}
|
|
228
|
+
// A tall target leaves room at its side more often than above or below it.
|
|
229
|
+
const top = Math.round(Math.max(margin, Math.min(view.height - card.height - margin, box.top)));
|
|
230
|
+
if (box.right + margin + width <= view.width - margin) {
|
|
231
|
+
sides.push({ left: Math.round(box.right + margin), top, side: 'right' });
|
|
232
|
+
}
|
|
233
|
+
if (box.left - margin - width >= margin) {
|
|
234
|
+
sides.push({ left: Math.round(box.left - margin - width), top, side: 'left' });
|
|
235
|
+
}
|
|
236
|
+
const side = sides.find((spot) => clear(spot.left, spot.top));
|
|
237
|
+
if (side) {
|
|
238
|
+
return side;
|
|
239
|
+
}
|
|
240
|
+
return docked() || { left, top: Math.round(Math.max(margin, view.height - card.height - margin)), side: 'over' };
|
|
241
|
+
}
|