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,1314 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Photo mode of the Watch page. It holds the match still, hands the view to a free camera with
|
|
5
|
+
// depth of field, film looks, a vignette and grain, draws frame guides over the picture and saves
|
|
6
|
+
// the picture as a PNG at the window's size or a chosen one. The page's own bars give way to a
|
|
7
|
+
// panel of its own, which a key hides, and a controller flies the camera too. Scripts reach it as
|
|
8
|
+
// window.cuju.photo, with open, close, set, camera, capture, save and state.
|
|
9
|
+
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
12
|
+
import { CAMERA_BOX, confine } from '../world/cameras.js';
|
|
13
|
+
import { OWN_KEYS } from '../core/comfort.js';
|
|
14
|
+
import { el, menuItem } from '../core/menu.js';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULTS, FOCUS_MAX, FOCUS_MIN, GUIDES, LOOKS, MAX_SIDE, MIN_SIDE, RANGES, RESOLUTIONS, captureFov, focusToSlider,
|
|
17
|
+
guideGeometry, lookById, nextGuide, nextLook, normalizeSettings, photoFileName, readPad,
|
|
18
|
+
resolveResolution, sliderToFocus,
|
|
19
|
+
} from './photo-core.js';
|
|
20
|
+
import { formatNumber, t, tn } from '../core/i18n.js';
|
|
21
|
+
import { presetLabel } from './size-fields.js';
|
|
22
|
+
|
|
23
|
+
// Samples of the depth of field, on a spiral of the golden angle.
|
|
24
|
+
const SAMPLES = 48;
|
|
25
|
+
// The largest blur, as a share of the picture's height, so a photo of any size looks the same.
|
|
26
|
+
const MAX_BLUR = 0.02;
|
|
27
|
+
// Changes of moving grain per second, as on film.
|
|
28
|
+
const GRAIN_RATE = 24;
|
|
29
|
+
// Turning, rolling and zooming speeds of held keys, in radians and degrees per second.
|
|
30
|
+
const TURN = 1.2;
|
|
31
|
+
const ROLL = 30;
|
|
32
|
+
const ZOOM = 20;
|
|
33
|
+
|
|
34
|
+
// The name of a look in the language of the page.
|
|
35
|
+
const lookLabel = (id) => t(`photo.look.${lookById(id).id}`);
|
|
36
|
+
// A number as String writes it, with the decimal separator of the language.
|
|
37
|
+
const decimal = (v) => {
|
|
38
|
+
const text = String(v);
|
|
39
|
+
return /^-?\d+\.\d+$/.test(text) ? text.replace('.', formatNumber(0.5).charAt(1)) : text;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const VERTEX = /* glsl */`
|
|
43
|
+
varying vec2 vUv;
|
|
44
|
+
void main() {
|
|
45
|
+
vUv = uv;
|
|
46
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
// The picture as drawn, already toned and in display values, blurred by its depth around the
|
|
50
|
+
// focus distance, then graded and vignetted. The grain follows in a pass of its own, so grain
|
|
51
|
+
// that moves redraws only that pass.
|
|
52
|
+
const FRAGMENT = /* glsl */`
|
|
53
|
+
#include <packing>
|
|
54
|
+
#define SAMPLES ${SAMPLES}
|
|
55
|
+
uniform sampler2D tColor;
|
|
56
|
+
uniform sampler2D tDepth;
|
|
57
|
+
uniform vec2 resolution;
|
|
58
|
+
uniform float cameraNear;
|
|
59
|
+
uniform float cameraFar;
|
|
60
|
+
uniform float focus;
|
|
61
|
+
uniform float autoFocus;
|
|
62
|
+
uniform float aperture;
|
|
63
|
+
uniform float maxBlur;
|
|
64
|
+
uniform float exposure;
|
|
65
|
+
uniform float temperature;
|
|
66
|
+
uniform float tint;
|
|
67
|
+
uniform float contrast;
|
|
68
|
+
uniform float saturation;
|
|
69
|
+
uniform float fade;
|
|
70
|
+
uniform float mono;
|
|
71
|
+
uniform vec3 shadows;
|
|
72
|
+
uniform vec3 highlights;
|
|
73
|
+
uniform vec3 gain;
|
|
74
|
+
uniform float vignette;
|
|
75
|
+
varying vec2 vUv;
|
|
76
|
+
|
|
77
|
+
const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);
|
|
78
|
+
|
|
79
|
+
float distanceAt(vec2 uv) {
|
|
80
|
+
return -perspectiveDepthToViewZ(texture2D(tDepth, uv).x, cameraNear, cameraFar);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The blur of a point at distance z, from 0 to 1 of the largest.
|
|
84
|
+
float blurAt(float z, float f) {
|
|
85
|
+
return clamp(abs(1.0 - f / max(z, 0.01)) * aperture * 2.0, 0.0, 1.0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
float focusDistance() {
|
|
89
|
+
if (autoFocus < 0.5) {
|
|
90
|
+
return focus;
|
|
91
|
+
}
|
|
92
|
+
float d = 0.4 * distanceAt(vec2(0.5));
|
|
93
|
+
d += 0.15 * distanceAt(vec2(0.49, 0.5)) + 0.15 * distanceAt(vec2(0.51, 0.5));
|
|
94
|
+
d += 0.15 * distanceAt(vec2(0.5, 0.49)) + 0.15 * distanceAt(vec2(0.5, 0.51));
|
|
95
|
+
return clamp(d, ${FOCUS_MIN.toFixed(1)}, ${FOCUS_MAX.toFixed(1)});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
vec3 depthOfField(vec2 uv) {
|
|
99
|
+
vec3 color = texture2D(tColor, uv).rgb;
|
|
100
|
+
if (aperture <= 0.0) {
|
|
101
|
+
return color;
|
|
102
|
+
}
|
|
103
|
+
float f = focusDistance();
|
|
104
|
+
float zc = distanceAt(uv);
|
|
105
|
+
float cc = blurAt(zc, f) * maxBlur;
|
|
106
|
+
float spacing = 0.5 * maxBlur / sqrt(float(SAMPLES));
|
|
107
|
+
vec3 sum = color;
|
|
108
|
+
float total = 1.0;
|
|
109
|
+
for (int i = 0; i < SAMPLES; i++) {
|
|
110
|
+
float r = sqrt((float(i) + 0.5) / float(SAMPLES)) * maxBlur;
|
|
111
|
+
float a = float(i) * 2.39996323;
|
|
112
|
+
vec2 suv = uv + vec2(cos(a), sin(a)) * r / resolution;
|
|
113
|
+
vec3 sc = texture2D(tColor, suv).rgb;
|
|
114
|
+
float zs = distanceAt(suv);
|
|
115
|
+
float cs = blurAt(zs, f) * maxBlur;
|
|
116
|
+
// A sharper point in front keeps its edge against the blurred background behind it.
|
|
117
|
+
if (zs > zc) {
|
|
118
|
+
cs = min(cs, cc * 2.0);
|
|
119
|
+
}
|
|
120
|
+
float m = smoothstep(r - spacing, r + spacing, cs);
|
|
121
|
+
sum += mix(sum / total, sc, m);
|
|
122
|
+
total += 1.0;
|
|
123
|
+
}
|
|
124
|
+
return sum / total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
vec3 grade(vec3 c) {
|
|
128
|
+
c *= exp2(exposure);
|
|
129
|
+
c *= vec3(1.0 + 0.12 * temperature - 0.03 * tint, 1.0 + 0.05 * tint, 1.0 - 0.12 * temperature - 0.03 * tint);
|
|
130
|
+
float l = dot(c, LUMA);
|
|
131
|
+
c = mix(vec3(l), c, saturation);
|
|
132
|
+
c = max((c - 0.5) * contrast + 0.5, 0.0);
|
|
133
|
+
l = clamp(dot(c, LUMA), 0.0, 1.0);
|
|
134
|
+
c += shadows * (1.0 - l) * (1.0 - l) + highlights * l * l;
|
|
135
|
+
c *= gain;
|
|
136
|
+
c = mix(c, vec3(clamp(dot(c, LUMA), 0.0, 1.0)), mono);
|
|
137
|
+
return fade + c * (1.0 - fade);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void main() {
|
|
141
|
+
vec3 c = grade(depthOfField(vUv));
|
|
142
|
+
float aspect = resolution.x / resolution.y;
|
|
143
|
+
vec2 p = (vUv - 0.5) * vec2(aspect, 1.0);
|
|
144
|
+
float d = length(p) / length(vec2(aspect, 1.0) * 0.5);
|
|
145
|
+
c *= 1.0 - vignette * 0.85 * smoothstep(0.25, 1.05, d);
|
|
146
|
+
gl_FragColor = vec4(clamp(c, 0.0, 1.0), 1.0);
|
|
147
|
+
}`;
|
|
148
|
+
|
|
149
|
+
// Film grain over the graded picture, stronger in the shadows.
|
|
150
|
+
const GRAIN = /* glsl */`
|
|
151
|
+
uniform sampler2D tGraded;
|
|
152
|
+
uniform float grain;
|
|
153
|
+
uniform float seed;
|
|
154
|
+
varying vec2 vUv;
|
|
155
|
+
|
|
156
|
+
float hash(vec2 p) {
|
|
157
|
+
vec3 q = fract(vec3(p.xyx) * 0.1031);
|
|
158
|
+
q += dot(q, q.yzx + 33.33);
|
|
159
|
+
return fract((q.x + q.y) * q.z);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
void main() {
|
|
163
|
+
vec3 c = texture2D(tGraded, vUv).rgb;
|
|
164
|
+
float l = clamp(dot(c, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0);
|
|
165
|
+
float n = hash(gl_FragCoord.xy + seed * 61.7) + hash(gl_FragCoord.xy * 1.37 + seed * 17.3) - 1.0;
|
|
166
|
+
c += n * grain * 0.14 * (0.35 + 0.65 * (1.0 - l));
|
|
167
|
+
gl_FragColor = vec4(clamp(c, 0.0, 1.0), 1.0);
|
|
168
|
+
}`;
|
|
169
|
+
|
|
170
|
+
const STYLE = `
|
|
171
|
+
html.cuju-photo #topbar,
|
|
172
|
+
html.cuju-photo #watch > :not(#stage):not(.photo-ui),
|
|
173
|
+
html.cuju-photo .player-labels { display: none !important; }
|
|
174
|
+
.photo-guides { position: absolute; inset: 0; z-index: 4; width: 100%; height: 100%; pointer-events: none; }
|
|
175
|
+
.photo-guides .shade { fill: rgba(0, 0, 0, 0.45); fill-rule: evenodd; }
|
|
176
|
+
.photo-guides line, .photo-guides rect { fill: none; vector-effect: non-scaling-stroke; }
|
|
177
|
+
.photo-guides .halo { stroke: rgba(0, 0, 0, 0.5); stroke-width: 3; }
|
|
178
|
+
.photo-guides .ink { stroke: rgba(255, 255, 255, 0.85); stroke-width: 1; }
|
|
179
|
+
#photo-panel { position: absolute; top: 12px; right: 12px; z-index: 8; width: 300px;
|
|
180
|
+
max-height: calc(100% - 24px); overflow: auto; padding: 12px 14px; border: 1px solid var(--border);
|
|
181
|
+
background: var(--overlay); color: var(--text); font-size: 13px; }
|
|
182
|
+
#photo-panel[hidden] { display: none; }
|
|
183
|
+
#photo-panel:focus { outline: none; }
|
|
184
|
+
#photo-panel h2 { margin: 0 0 8px; font-size: 16px; font-weight: normal; }
|
|
185
|
+
#photo-panel .photo-row { display: grid; grid-template-columns: 1fr auto; align-items: center;
|
|
186
|
+
gap: 2px 8px; margin: 6px 0; }
|
|
187
|
+
#photo-panel .photo-row > :is(input[type="range"], select) { grid-column: 1 / -1; width: 100%; }
|
|
188
|
+
#photo-panel .photo-size { grid-template-columns: 1fr auto 1fr; }
|
|
189
|
+
#photo-panel .photo-size[hidden] { display: none; }
|
|
190
|
+
#photo-panel .photo-size input { width: 100%; min-width: 0; }
|
|
191
|
+
#photo-panel output { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
192
|
+
#photo-panel .photo-check { display: flex; align-items: center; gap: 6px; margin: 6px 0; }
|
|
193
|
+
#photo-panel .photo-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
194
|
+
#photo-panel .photo-keys { margin: 10px 0 0; color: var(--muted); line-height: 1.5; }
|
|
195
|
+
#photo-panel .photo-status { min-height: 1.4em; margin: 6px 0 0; color: var(--muted); }
|
|
196
|
+
@media (max-width: 600px) {
|
|
197
|
+
#photo-panel { top: auto; bottom: 8px; left: 8px; right: 8px; width: auto; max-height: 45%; }
|
|
198
|
+
}`;
|
|
199
|
+
|
|
200
|
+
function injectStyle() {
|
|
201
|
+
if (document.getElementById('cuju-photo-style')) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const style = document.createElement('style');
|
|
205
|
+
style.id = 'cuju-photo-style';
|
|
206
|
+
style.textContent = STYLE;
|
|
207
|
+
document.head.appendChild(style);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const SVG = 'http://www.w3.org/2000/svg';
|
|
211
|
+
|
|
212
|
+
function svg(tag, attrs = {}) {
|
|
213
|
+
const node = document.createElementNS(SVG, tag);
|
|
214
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
215
|
+
node.setAttribute(k, String(v));
|
|
216
|
+
}
|
|
217
|
+
return node;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function vector(value, name) {
|
|
221
|
+
if (Array.isArray(value) && value.length === 3 && value.every(Number.isFinite)) {
|
|
222
|
+
return new THREE.Vector3(value[0], value[1], value[2]);
|
|
223
|
+
}
|
|
224
|
+
if (value && [value.x, value.y, value.z].every(Number.isFinite)) {
|
|
225
|
+
return new THREE.Vector3(value.x, value.y, value.z);
|
|
226
|
+
}
|
|
227
|
+
throw new TypeError(`The camera ${name} is [x, y, z] or { x, y, z } in meters, y up.`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const round = (v, digits = 3) => Math.round(v * 10 ** digits) / 10 ** digits;
|
|
231
|
+
|
|
232
|
+
// Keys that a focused control of the panel uses itself.
|
|
233
|
+
const CONTROL_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', ' ', 'Enter', 'Tab']);
|
|
234
|
+
// Keys the Watch page keeps in photo mode, fullscreen and mute.
|
|
235
|
+
const PAGE_KEYS = new Set(['f', 'm']);
|
|
236
|
+
|
|
237
|
+
export class PhotoMode {
|
|
238
|
+
constructor(app) {
|
|
239
|
+
this.app = app;
|
|
240
|
+
this.isOpen = false;
|
|
241
|
+
this.settings = { ...DEFAULTS };
|
|
242
|
+
this.hidden = false;
|
|
243
|
+
this.held = new Set();
|
|
244
|
+
this.padButtons = [];
|
|
245
|
+
this.saving = false;
|
|
246
|
+
this.saved = null;
|
|
247
|
+
this.lastCapture = null;
|
|
248
|
+
this.seed = 0;
|
|
249
|
+
this.looks = LOOKS.map((l) => l.id);
|
|
250
|
+
this.guides = GUIDES.map((g) => g.id);
|
|
251
|
+
this.resolutions = RESOLUTIONS.map((r) => r.id);
|
|
252
|
+
injectStyle();
|
|
253
|
+
this.buildMenuItem();
|
|
254
|
+
window.addEventListener('keydown', (e) => this.onKey(e), true);
|
|
255
|
+
window.addEventListener('keyup', (e) => this.onKeyUp(e), true);
|
|
256
|
+
window.addEventListener('blur', () => {
|
|
257
|
+
this.held.clear();
|
|
258
|
+
this.shift = false;
|
|
259
|
+
});
|
|
260
|
+
// A click on the picture skips a show on the Watch page, which photo mode holds still.
|
|
261
|
+
const stage = document.getElementById('stage');
|
|
262
|
+
if (stage) {
|
|
263
|
+
stage.addEventListener('click', (e) => {
|
|
264
|
+
if (this.isOpen) {
|
|
265
|
+
e.stopImmediatePropagation();
|
|
266
|
+
}
|
|
267
|
+
}, true);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// The entry of the View menu, which a fill of the menu leaves alone.
|
|
272
|
+
buildMenuItem() {
|
|
273
|
+
const host = document.getElementById('view-photo');
|
|
274
|
+
if (!host) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
host.replaceChildren(menuItem(t('photo.title'), {
|
|
278
|
+
kbd: 'O', hint: t('photo.menu.hint'),
|
|
279
|
+
onSelect: () => {
|
|
280
|
+
this.open();
|
|
281
|
+
setTimeout(() => this.panel && !this.panel.hidden && this.panel.focus(), 0);
|
|
282
|
+
},
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Opening and leaving.
|
|
287
|
+
|
|
288
|
+
open() {
|
|
289
|
+
if (this.isOpen) {
|
|
290
|
+
return this.state();
|
|
291
|
+
}
|
|
292
|
+
const app = this.app;
|
|
293
|
+
if (app.page !== 'watch') {
|
|
294
|
+
app.showPage('watch');
|
|
295
|
+
}
|
|
296
|
+
this.snapshot();
|
|
297
|
+
const tl = app.timeline;
|
|
298
|
+
if (tl) {
|
|
299
|
+
tl.pause();
|
|
300
|
+
}
|
|
301
|
+
app.userPaused = true;
|
|
302
|
+
if (app.sound && typeof app.sound.setActive === 'function') {
|
|
303
|
+
app.sound.setActive(false);
|
|
304
|
+
}
|
|
305
|
+
const rig = app.cameras;
|
|
306
|
+
const cam = app.view.camera;
|
|
307
|
+
this.settings = normalizeSettings(this.settings, {
|
|
308
|
+
fov: THREE.MathUtils.clamp(cam.fov, ...RANGES.fov), roll: 0,
|
|
309
|
+
});
|
|
310
|
+
this.picking = app.playerTools ? app.playerTools.picking : false;
|
|
311
|
+
if (app.playerTools) {
|
|
312
|
+
app.playerTools.picking = false;
|
|
313
|
+
}
|
|
314
|
+
rig.setMode('free');
|
|
315
|
+
// The photo starts from the view as it stands, without a glide.
|
|
316
|
+
rig.blend = null;
|
|
317
|
+
rig.flight = null;
|
|
318
|
+
rig.freeFov = this.settings.fov;
|
|
319
|
+
rig.rawFov = this.settings.fov;
|
|
320
|
+
rig.releaseAll();
|
|
321
|
+
this.held.clear();
|
|
322
|
+
this.padButtons = this.readPads().buttons;
|
|
323
|
+
this.isOpen = true;
|
|
324
|
+
this.buildUi();
|
|
325
|
+
document.documentElement.classList.add('cuju-photo');
|
|
326
|
+
this.setHidden(false);
|
|
327
|
+
this.syncPanel();
|
|
328
|
+
this.drawGuides();
|
|
329
|
+
this.place();
|
|
330
|
+
return this.state();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
close() {
|
|
334
|
+
if (!this.isOpen) {
|
|
335
|
+
return this.state();
|
|
336
|
+
}
|
|
337
|
+
this.isOpen = false;
|
|
338
|
+
if (document.pointerLockElement) {
|
|
339
|
+
document.exitPointerLock();
|
|
340
|
+
}
|
|
341
|
+
document.documentElement.classList.remove('cuju-photo');
|
|
342
|
+
this.held.clear();
|
|
343
|
+
this.app.cameras.releaseAll();
|
|
344
|
+
if (this.app.playerTools) {
|
|
345
|
+
this.app.playerTools.picking = this.picking;
|
|
346
|
+
}
|
|
347
|
+
this.restore();
|
|
348
|
+
// Focus inside the panel goes back to the page with it.
|
|
349
|
+
if (this.panel && this.panel.contains(document.activeElement)) {
|
|
350
|
+
document.activeElement.blur();
|
|
351
|
+
}
|
|
352
|
+
this.destroyUi();
|
|
353
|
+
this.freePasses();
|
|
354
|
+
return this.state();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
toggle() {
|
|
358
|
+
return this.isOpen ? this.close() : this.open();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The camera, the springs of the framed shots, the free camera and playback as they were.
|
|
362
|
+
snapshot() {
|
|
363
|
+
const app = this.app;
|
|
364
|
+
const rig = app.cameras;
|
|
365
|
+
const cam = app.view.camera;
|
|
366
|
+
const spring = (s) => ({ x: Array.from(s.x), v: Array.from(s.v) });
|
|
367
|
+
const tl = app.timeline;
|
|
368
|
+
this.saved = {
|
|
369
|
+
mode: rig.mode,
|
|
370
|
+
position: cam.position.clone(),
|
|
371
|
+
quaternion: cam.quaternion.clone(),
|
|
372
|
+
fov: cam.fov,
|
|
373
|
+
near: cam.near,
|
|
374
|
+
target: rig.target.clone(),
|
|
375
|
+
rawPos: rig.rawPos.clone(),
|
|
376
|
+
rawQuat: rig.rawQuat.clone(),
|
|
377
|
+
rawFov: rig.rawFov,
|
|
378
|
+
shotPos: spring(rig.shotPos),
|
|
379
|
+
shotTarget: spring(rig.shotTarget),
|
|
380
|
+
shotFov: spring(rig.shotFov),
|
|
381
|
+
followSpring: spring(rig.followSpring),
|
|
382
|
+
offset: rig.offset.clone(),
|
|
383
|
+
freePos: rig.freePos.clone(),
|
|
384
|
+
yaw: rig.yaw,
|
|
385
|
+
pitch: rig.pitch,
|
|
386
|
+
viewYaw: rig.viewYaw,
|
|
387
|
+
viewPitch: rig.viewPitch,
|
|
388
|
+
freeFov: rig.freeFov,
|
|
389
|
+
speed: rig.speed,
|
|
390
|
+
fppPos: rig.fppPos.clone(),
|
|
391
|
+
fppQuat: rig.fppQuat.clone(),
|
|
392
|
+
rigFov: rig.fov,
|
|
393
|
+
timeline: tl,
|
|
394
|
+
playing: tl ? tl.playing : false,
|
|
395
|
+
following: tl ? tl.following : false,
|
|
396
|
+
direction: tl ? tl.direction : 1,
|
|
397
|
+
speedOfPlay: tl ? tl.speed : 1,
|
|
398
|
+
userPaused: app.userPaused,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
restore() {
|
|
403
|
+
const s = this.saved;
|
|
404
|
+
if (!s) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.saved = null;
|
|
408
|
+
const app = this.app;
|
|
409
|
+
const rig = app.cameras;
|
|
410
|
+
const cam = app.view.camera;
|
|
411
|
+
const set = (spring, saved) => {
|
|
412
|
+
spring.x.set(saved.x);
|
|
413
|
+
spring.v.set(saved.v);
|
|
414
|
+
};
|
|
415
|
+
// The camera goes back first, so a mode that starts from the camera starts from its view.
|
|
416
|
+
cam.position.copy(s.position);
|
|
417
|
+
cam.quaternion.copy(s.quaternion);
|
|
418
|
+
cam.fov = s.fov;
|
|
419
|
+
cam.near = s.near;
|
|
420
|
+
cam.updateProjectionMatrix();
|
|
421
|
+
rig.target.copy(s.target);
|
|
422
|
+
rig.freePos.copy(s.freePos);
|
|
423
|
+
rig.setMode(s.mode);
|
|
424
|
+
rig.blend = null;
|
|
425
|
+
rig.shotCut = false;
|
|
426
|
+
rig.flight = null;
|
|
427
|
+
set(rig.shotPos, s.shotPos);
|
|
428
|
+
set(rig.shotTarget, s.shotTarget);
|
|
429
|
+
set(rig.shotFov, s.shotFov);
|
|
430
|
+
set(rig.followSpring, s.followSpring);
|
|
431
|
+
rig.offset.copy(s.offset);
|
|
432
|
+
rig.freePos.copy(s.freePos);
|
|
433
|
+
rig.freeVel.set(0, 0, 0);
|
|
434
|
+
rig.yaw = s.yaw;
|
|
435
|
+
rig.pitch = s.pitch;
|
|
436
|
+
rig.viewYaw = s.viewYaw;
|
|
437
|
+
rig.viewPitch = s.viewPitch;
|
|
438
|
+
rig.freeFov = s.freeFov;
|
|
439
|
+
rig.speed = s.speed;
|
|
440
|
+
rig.fppPos.copy(s.fppPos);
|
|
441
|
+
rig.fppQuat.copy(s.fppQuat);
|
|
442
|
+
rig.fov = s.rigFov;
|
|
443
|
+
rig.rawPos.copy(s.rawPos);
|
|
444
|
+
rig.rawQuat.copy(s.rawQuat);
|
|
445
|
+
rig.rawFov = s.rawFov;
|
|
446
|
+
rig.camVel.set(0, 0, 0);
|
|
447
|
+
rig.lastCamPos.copy(s.position);
|
|
448
|
+
rig.idle = 0;
|
|
449
|
+
const tl = app.timeline;
|
|
450
|
+
if (tl && tl === s.timeline) {
|
|
451
|
+
tl.direction = s.direction;
|
|
452
|
+
tl.speed = s.speedOfPlay;
|
|
453
|
+
if (s.following) {
|
|
454
|
+
tl.following = true;
|
|
455
|
+
tl.playing = true;
|
|
456
|
+
} else if (s.playing) {
|
|
457
|
+
tl.playing = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
app.userPaused = s.userPaused;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The frame loop of photo mode, in place of the page's own while it is open. Returns false
|
|
464
|
+
// when the page left the Watch page and photo mode closed.
|
|
465
|
+
frame(dt) {
|
|
466
|
+
const app = this.app;
|
|
467
|
+
if (app.page !== 'watch') {
|
|
468
|
+
this.close();
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
const rig = app.cameras;
|
|
472
|
+
if (rig.mode !== 'free') {
|
|
473
|
+
rig.setMode('free');
|
|
474
|
+
rig.blend = null;
|
|
475
|
+
}
|
|
476
|
+
// Grain moves at the pace of film, and holds still under reduced motion.
|
|
477
|
+
this.grainClock = (this.grainClock || 0) + dt;
|
|
478
|
+
if (!app.reducedMotion && this.settings.grain > 0 && this.grainClock >= 1 / GRAIN_RATE) {
|
|
479
|
+
this.grainClock = 0;
|
|
480
|
+
this.seed = (this.seed + 1) % 1000;
|
|
481
|
+
}
|
|
482
|
+
this.pollPad(dt);
|
|
483
|
+
this.steer(dt);
|
|
484
|
+
rig.idleSeconds = 0;
|
|
485
|
+
rig.freeFov = this.settings.fov;
|
|
486
|
+
rig.update(dt, app.ball.mesh.position, app.players, app.pose || { players: 1 }, app.view.aspect, null);
|
|
487
|
+
this.roll();
|
|
488
|
+
this.draw();
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Held arrows turn, Z and X roll, minus and plus zoom.
|
|
493
|
+
steer(dt) {
|
|
494
|
+
if (!this.held.size) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const rig = this.app.cameras;
|
|
498
|
+
const fast = this.shift ? 3 : 1;
|
|
499
|
+
const turn = TURN * fast * dt;
|
|
500
|
+
const yaw = (this.held.has('ArrowLeft') ? 1 : 0) - (this.held.has('ArrowRight') ? 1 : 0);
|
|
501
|
+
const pitch = (this.held.has('ArrowUp') ? 1 : 0) - (this.held.has('ArrowDown') ? 1 : 0);
|
|
502
|
+
if (yaw || pitch) {
|
|
503
|
+
rig.touch();
|
|
504
|
+
rig.yaw += yaw * turn;
|
|
505
|
+
rig.pitch = THREE.MathUtils.clamp(rig.pitch + pitch * turn, -CAMERA_BOX.pitchLimit, CAMERA_BOX.pitchLimit);
|
|
506
|
+
}
|
|
507
|
+
const roll = (this.held.has('x') ? 1 : 0) - (this.held.has('z') ? 1 : 0);
|
|
508
|
+
const zoom = (this.held.has('-') ? 1 : 0) - (this.held.has('+') ? 1 : 0);
|
|
509
|
+
if (roll || zoom) {
|
|
510
|
+
this.update({
|
|
511
|
+
roll: this.settings.roll + roll * ROLL * fast * dt,
|
|
512
|
+
fov: this.settings.fov + zoom * ZOOM * fast * dt,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
roll() {
|
|
518
|
+
if (this.settings.roll) {
|
|
519
|
+
this.app.view.camera.rotateZ(THREE.MathUtils.degToRad(this.settings.roll));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Sets the camera from the free camera at once, as it settles, for a capture right after a
|
|
524
|
+
// change from a script.
|
|
525
|
+
place() {
|
|
526
|
+
const rig = this.app.cameras;
|
|
527
|
+
const cam = this.app.view.camera;
|
|
528
|
+
rig.rawPos.copy(rig.freePos);
|
|
529
|
+
rig.rawQuat.setFromEuler(new THREE.Euler(rig.viewPitch, rig.viewYaw, 0, 'YXZ'));
|
|
530
|
+
rig.rawFov = this.settings.fov;
|
|
531
|
+
cam.position.copy(rig.rawPos);
|
|
532
|
+
cam.quaternion.copy(rig.rawQuat);
|
|
533
|
+
cam.fov = this.settings.fov;
|
|
534
|
+
cam.updateProjectionMatrix();
|
|
535
|
+
rig.target.copy(rig.rawPos).addScaledVector(new THREE.Vector3(0, 0, -1).applyQuaternion(rig.rawQuat), 20);
|
|
536
|
+
this.roll();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Drawing.
|
|
540
|
+
|
|
541
|
+
ensurePasses(width, height) {
|
|
542
|
+
if (!this.material) {
|
|
543
|
+
const uniforms = {
|
|
544
|
+
tColor: { value: null },
|
|
545
|
+
tDepth: { value: null },
|
|
546
|
+
resolution: { value: new THREE.Vector2(1, 1) },
|
|
547
|
+
cameraNear: { value: 0.1 },
|
|
548
|
+
cameraFar: { value: 2500 },
|
|
549
|
+
focus: { value: 30 },
|
|
550
|
+
autoFocus: { value: 1 },
|
|
551
|
+
aperture: { value: 0 },
|
|
552
|
+
maxBlur: { value: 10 },
|
|
553
|
+
vignette: { value: 0 },
|
|
554
|
+
};
|
|
555
|
+
for (const name of ['exposure', 'temperature', 'tint', 'contrast', 'saturation', 'fade', 'mono']) {
|
|
556
|
+
uniforms[name] = { value: 0 };
|
|
557
|
+
}
|
|
558
|
+
for (const name of ['shadows', 'highlights', 'gain']) {
|
|
559
|
+
uniforms[name] = { value: new THREE.Vector3() };
|
|
560
|
+
}
|
|
561
|
+
this.material = new THREE.ShaderMaterial({
|
|
562
|
+
vertexShader: VERTEX, fragmentShader: FRAGMENT, uniforms, depthTest: false, depthWrite: false,
|
|
563
|
+
});
|
|
564
|
+
this.grainMaterial = new THREE.ShaderMaterial({
|
|
565
|
+
vertexShader: VERTEX,
|
|
566
|
+
fragmentShader: GRAIN,
|
|
567
|
+
depthTest: false,
|
|
568
|
+
depthWrite: false,
|
|
569
|
+
uniforms: { tGraded: { value: null }, grain: { value: 0 }, seed: { value: 0 } },
|
|
570
|
+
});
|
|
571
|
+
this.quad = new FullScreenQuad(this.material);
|
|
572
|
+
}
|
|
573
|
+
if (!this.colorTexture || this.colorTexture.image.width !== width || this.colorTexture.image.height !== height) {
|
|
574
|
+
if (this.colorTexture) {
|
|
575
|
+
this.colorTexture.dispose();
|
|
576
|
+
}
|
|
577
|
+
this.colorTexture = new THREE.FramebufferTexture(width, height);
|
|
578
|
+
}
|
|
579
|
+
if (!this.gradeTarget || this.gradeTarget.width !== width || this.gradeTarget.height !== height) {
|
|
580
|
+
if (this.gradeTarget) {
|
|
581
|
+
this.gradeTarget.dispose();
|
|
582
|
+
}
|
|
583
|
+
this.gradeTarget = new THREE.WebGLRenderTarget(width, height, { depthBuffer: false });
|
|
584
|
+
}
|
|
585
|
+
// The depth needs no more than half the size, the blur hides its edges.
|
|
586
|
+
const dw = Math.max(1, Math.ceil(width / 2));
|
|
587
|
+
const dh = Math.max(1, Math.ceil(height / 2));
|
|
588
|
+
if (!this.depthTarget || this.depthTarget.width !== dw || this.depthTarget.height !== dh) {
|
|
589
|
+
if (this.depthTarget) {
|
|
590
|
+
this.depthTarget.depthTexture.dispose();
|
|
591
|
+
this.depthTarget.dispose();
|
|
592
|
+
}
|
|
593
|
+
const depthTexture = new THREE.DepthTexture(dw, dh);
|
|
594
|
+
depthTexture.type = THREE.UnsignedIntType;
|
|
595
|
+
this.depthTarget = new THREE.WebGLRenderTarget(dw, dh, { depthTexture, depthBuffer: true });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// What the picture depends on besides the settings, the camera and the size, so a still view
|
|
600
|
+
// with still grain draws nothing new.
|
|
601
|
+
pictureKey(width, height) {
|
|
602
|
+
const cam = this.app.view.camera;
|
|
603
|
+
cam.updateMatrixWorld();
|
|
604
|
+
const parts = [width, height, cam.fov, cam.aspect, cam.near];
|
|
605
|
+
for (const v of cam.matrixWorld.elements) {
|
|
606
|
+
parts.push(Math.round(v * 1e5));
|
|
607
|
+
}
|
|
608
|
+
return parts.join(',');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Draws the photo onto the canvas at the size of its drawing buffer. The scene and the blur
|
|
612
|
+
// draw again only when the camera, the size or a setting changed, and the grain only when it
|
|
613
|
+
// moved. Force draws everything.
|
|
614
|
+
draw(force = false) {
|
|
615
|
+
const r = this.app.view.renderer;
|
|
616
|
+
const size = r.getDrawingBufferSize(new THREE.Vector2());
|
|
617
|
+
const key = this.pictureKey(size.x, size.y);
|
|
618
|
+
const grain = `${this.settings.grain},${this.seed}`;
|
|
619
|
+
if (force || this.dirty || key !== this.drawnKey || !this.material) {
|
|
620
|
+
this.renderScene(size.x, size.y);
|
|
621
|
+
this.drawnKey = key;
|
|
622
|
+
this.dirty = false;
|
|
623
|
+
} else if (grain === this.drawnGrain) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
this.drawnGrain = grain;
|
|
627
|
+
const g = this.grainMaterial;
|
|
628
|
+
g.uniforms.tGraded.value = this.gradeTarget.texture;
|
|
629
|
+
g.uniforms.grain.value = this.settings.grain;
|
|
630
|
+
g.uniforms.seed.value = this.seed;
|
|
631
|
+
this.quad.material = g;
|
|
632
|
+
r.setRenderTarget(null);
|
|
633
|
+
this.quad.render(r);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// The scene as the page draws it, its depth, and the blur, the grade and the vignette into a
|
|
637
|
+
// target of their own.
|
|
638
|
+
renderScene(width, height) {
|
|
639
|
+
const view = this.app.view;
|
|
640
|
+
const r = view.renderer;
|
|
641
|
+
const cam = view.camera;
|
|
642
|
+
view.render();
|
|
643
|
+
this.ensurePasses(width, height);
|
|
644
|
+
r.setRenderTarget(null);
|
|
645
|
+
r.copyFramebufferToTexture(this.colorTexture);
|
|
646
|
+
const s = this.settings;
|
|
647
|
+
if (s.aperture > 0) {
|
|
648
|
+
// The scene once more, small, for its depth, with the sun's shadow left as it was.
|
|
649
|
+
const shadows = r.shadowMap.autoUpdate;
|
|
650
|
+
r.shadowMap.autoUpdate = false;
|
|
651
|
+
r.setRenderTarget(this.depthTarget);
|
|
652
|
+
r.clear();
|
|
653
|
+
r.render(view.scene, cam);
|
|
654
|
+
r.shadowMap.autoUpdate = shadows;
|
|
655
|
+
}
|
|
656
|
+
const u = this.material.uniforms;
|
|
657
|
+
const L = lookById(s.look);
|
|
658
|
+
u.tColor.value = this.colorTexture;
|
|
659
|
+
u.tDepth.value = this.depthTarget.depthTexture;
|
|
660
|
+
u.resolution.value.set(width, height);
|
|
661
|
+
u.cameraNear.value = cam.near;
|
|
662
|
+
u.cameraFar.value = cam.far;
|
|
663
|
+
u.autoFocus.value = s.focus === 'auto' ? 1 : 0;
|
|
664
|
+
u.focus.value = s.focus === 'auto' ? 30 : s.focus;
|
|
665
|
+
u.aperture.value = s.aperture;
|
|
666
|
+
u.maxBlur.value = MAX_BLUR * height;
|
|
667
|
+
for (const name of ['exposure', 'temperature', 'tint', 'contrast', 'saturation', 'fade', 'mono']) {
|
|
668
|
+
u[name].value = L[name];
|
|
669
|
+
}
|
|
670
|
+
for (const name of ['shadows', 'highlights', 'gain']) {
|
|
671
|
+
u[name].value.fromArray(L[name]);
|
|
672
|
+
}
|
|
673
|
+
u.vignette.value = s.vignette;
|
|
674
|
+
this.quad.material = this.material;
|
|
675
|
+
r.setRenderTarget(this.gradeTarget);
|
|
676
|
+
this.quad.render(r);
|
|
677
|
+
r.setRenderTarget(null);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
freePasses() {
|
|
681
|
+
if (this.quad) {
|
|
682
|
+
this.quad.dispose();
|
|
683
|
+
this.quad = null;
|
|
684
|
+
}
|
|
685
|
+
for (const name of ['material', 'grainMaterial', 'gradeTarget']) {
|
|
686
|
+
if (this[name]) {
|
|
687
|
+
this[name].dispose();
|
|
688
|
+
this[name] = null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
this.drawnKey = null;
|
|
692
|
+
this.drawnGrain = null;
|
|
693
|
+
if (this.colorTexture) {
|
|
694
|
+
this.colorTexture.dispose();
|
|
695
|
+
this.colorTexture = null;
|
|
696
|
+
}
|
|
697
|
+
if (this.depthTarget) {
|
|
698
|
+
this.depthTarget.depthTexture.dispose();
|
|
699
|
+
this.depthTarget.dispose();
|
|
700
|
+
this.depthTarget = null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The size of the picture in device pixels, the size a photo at Window size has.
|
|
705
|
+
windowSize() {
|
|
706
|
+
const canvas = this.app.view.renderer.domElement;
|
|
707
|
+
const ratio = window.devicePixelRatio || 1;
|
|
708
|
+
return {
|
|
709
|
+
width: Math.max(16, Math.round(canvas.clientWidth * ratio)),
|
|
710
|
+
height: Math.max(16, Math.round(canvas.clientHeight * ratio)),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
maxSide() {
|
|
715
|
+
const gl = this.app.view.renderer.getContext();
|
|
716
|
+
const dims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
|
|
717
|
+
return Math.min(gl.getParameter(gl.MAX_RENDERBUFFER_SIZE), gl.getParameter(gl.MAX_TEXTURE_SIZE), dims[0], dims[1]);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Draws the photo at its size off the view and returns its pixels as a canvas. The drawing
|
|
721
|
+
// buffer takes the photo's size for one draw, inside one task, so the window never shows it.
|
|
722
|
+
renderPhoto(width, height) {
|
|
723
|
+
const app = this.app;
|
|
724
|
+
const view = app.view;
|
|
725
|
+
const r = view.renderer;
|
|
726
|
+
const cam = view.camera;
|
|
727
|
+
const gl = r.getContext();
|
|
728
|
+
const ratio = r.getPixelRatio();
|
|
729
|
+
const viewAspect = cam.aspect;
|
|
730
|
+
const fov = this.settings.fov;
|
|
731
|
+
let pixels = null;
|
|
732
|
+
try {
|
|
733
|
+
this.place();
|
|
734
|
+
r.setPixelRatio(1);
|
|
735
|
+
r.setSize(width, height, false);
|
|
736
|
+
if (gl.drawingBufferWidth !== width || gl.drawingBufferHeight !== height) {
|
|
737
|
+
throw new RangeError(t('photo.error.device', { width: gl.drawingBufferWidth, height: gl.drawingBufferHeight }));
|
|
738
|
+
}
|
|
739
|
+
cam.aspect = width / height;
|
|
740
|
+
cam.fov = captureFov(fov, viewAspect, width / height);
|
|
741
|
+
cam.updateProjectionMatrix();
|
|
742
|
+
if (view.bloom) {
|
|
743
|
+
view.bloom.setSize(width, height);
|
|
744
|
+
if (view.frame) {
|
|
745
|
+
view.frame.dispose();
|
|
746
|
+
}
|
|
747
|
+
view.frame = new THREE.FramebufferTexture(width, height);
|
|
748
|
+
}
|
|
749
|
+
this.draw(true);
|
|
750
|
+
pixels = new Uint8Array(width * height * 4);
|
|
751
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
752
|
+
} finally {
|
|
753
|
+
r.setPixelRatio(ratio);
|
|
754
|
+
cam.fov = fov;
|
|
755
|
+
view.resize();
|
|
756
|
+
this.place();
|
|
757
|
+
this.draw(true);
|
|
758
|
+
}
|
|
759
|
+
const canvas = document.createElement('canvas');
|
|
760
|
+
canvas.width = width;
|
|
761
|
+
canvas.height = height;
|
|
762
|
+
const ctx = canvas.getContext('2d');
|
|
763
|
+
const image = ctx.createImageData(width, height);
|
|
764
|
+
const row = width * 4;
|
|
765
|
+
for (let y = 0; y < height; y++) {
|
|
766
|
+
image.data.set(pixels.subarray((height - 1 - y) * row, (height - y) * row), y * row);
|
|
767
|
+
}
|
|
768
|
+
for (let i = 3; i < image.data.length; i += 4) {
|
|
769
|
+
image.data[i] = 255;
|
|
770
|
+
}
|
|
771
|
+
ctx.putImageData(image, 0, 0);
|
|
772
|
+
return canvas;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// The photo as a PNG. Options are width and height, or resolution, as for set, as, 'dataURL'
|
|
776
|
+
// by default or 'blob', and download, true to save the file as well.
|
|
777
|
+
async capture(options = {}) {
|
|
778
|
+
if (!this.isOpen) {
|
|
779
|
+
throw new Error('Photo mode is closed. Open it first with cuju.photo.open().');
|
|
780
|
+
}
|
|
781
|
+
const o = options || {};
|
|
782
|
+
const choice = o.width != null || o.height != null ? { width: o.width, height: o.height } : (o.resolution ?? this.settings.resolution);
|
|
783
|
+
const win = this.windowSize();
|
|
784
|
+
const { width, height } = resolveResolution(choice, win.width, win.height, this.maxSide());
|
|
785
|
+
const canvas = this.renderPhoto(width, height);
|
|
786
|
+
const blob = await new Promise((resolve, reject) => canvas.toBlob((b) => (b ? resolve(b) : reject(new Error(t('photo.error.encode')))), 'image/png'));
|
|
787
|
+
const name = this.fileName();
|
|
788
|
+
this.lastCapture = { width, height, name, bytes: blob.size };
|
|
789
|
+
if (o.download) {
|
|
790
|
+
this.download(blob, name);
|
|
791
|
+
}
|
|
792
|
+
if (o.as === 'blob') {
|
|
793
|
+
return blob;
|
|
794
|
+
}
|
|
795
|
+
return await new Promise((resolve, reject) => {
|
|
796
|
+
const reader = new FileReader();
|
|
797
|
+
reader.onload = () => resolve(reader.result);
|
|
798
|
+
reader.onerror = () => reject(reader.error);
|
|
799
|
+
reader.readAsDataURL(blob);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Saves the photo through a download link, at the chosen resolution.
|
|
804
|
+
async save(options = {}) {
|
|
805
|
+
if (this.saving) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
this.saving = true;
|
|
809
|
+
this.status(t('photo.status.saving'));
|
|
810
|
+
try {
|
|
811
|
+
await this.capture({ ...options, as: 'blob', download: true });
|
|
812
|
+
const c = this.lastCapture;
|
|
813
|
+
this.status(t('photo.status.saved', { name: c.name, width: c.width, height: c.height }));
|
|
814
|
+
return { ...c };
|
|
815
|
+
} catch (err) {
|
|
816
|
+
this.status(t('photo.status.failed', { reason: err.message }));
|
|
817
|
+
throw err;
|
|
818
|
+
} finally {
|
|
819
|
+
this.saving = false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
download(blob, name) {
|
|
824
|
+
const url = URL.createObjectURL(blob);
|
|
825
|
+
const link = document.createElement('a');
|
|
826
|
+
link.href = url;
|
|
827
|
+
link.download = name;
|
|
828
|
+
link.hidden = true;
|
|
829
|
+
document.body.append(link);
|
|
830
|
+
link.click();
|
|
831
|
+
link.remove();
|
|
832
|
+
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
fileName() {
|
|
836
|
+
const app = this.app;
|
|
837
|
+
const source = app.source;
|
|
838
|
+
const id = source ? (source.id || (source.live && source.live.id) || source.kind) : null;
|
|
839
|
+
const frame = app.timeline && source ? app.timeline.index : null;
|
|
840
|
+
return photoFileName(id, frame);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Settings.
|
|
844
|
+
|
|
845
|
+
// Changes settings, each checked. Returns the state.
|
|
846
|
+
set(patch) {
|
|
847
|
+
this.update(patch, true);
|
|
848
|
+
return this.state();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Changes settings. A script's change is strict, a number out of range throws, and the keys,
|
|
852
|
+
// the controller and the panel hold theirs to the range.
|
|
853
|
+
update(patch, strict = false) {
|
|
854
|
+
this.settings = normalizeSettings(this.settings, patch, { strict });
|
|
855
|
+
this.dirty = true;
|
|
856
|
+
if (this.isOpen) {
|
|
857
|
+
this.syncPanel();
|
|
858
|
+
this.drawGuides();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Sets the camera from a script, at once. Takes position, target, fov and roll, any of them,
|
|
863
|
+
// and returns the camera.
|
|
864
|
+
camera(view) {
|
|
865
|
+
if (!this.isOpen) {
|
|
866
|
+
throw new Error('Photo mode is closed. Open it first with cuju.photo.open().');
|
|
867
|
+
}
|
|
868
|
+
const rig = this.app.cameras;
|
|
869
|
+
if (view && typeof view === 'object') {
|
|
870
|
+
const position = view.position != null ? vector(view.position, 'position') : null;
|
|
871
|
+
const target = view.target != null ? vector(view.target, 'target') : null;
|
|
872
|
+
const patch = {};
|
|
873
|
+
if (view.fov != null) {
|
|
874
|
+
patch.fov = view.fov;
|
|
875
|
+
}
|
|
876
|
+
if (view.roll != null) {
|
|
877
|
+
patch.roll = view.roll;
|
|
878
|
+
}
|
|
879
|
+
this.update(patch);
|
|
880
|
+
rig.touch();
|
|
881
|
+
rig.blend = null;
|
|
882
|
+
rig.flight = null;
|
|
883
|
+
rig.freeVel.set(0, 0, 0);
|
|
884
|
+
if (position) {
|
|
885
|
+
rig.freePos.copy(confine(position));
|
|
886
|
+
}
|
|
887
|
+
if (target) {
|
|
888
|
+
const dir = new THREE.Vector3().subVectors(target, rig.freePos);
|
|
889
|
+
if (dir.lengthSq() > 1e-8) {
|
|
890
|
+
rig.yaw = Math.atan2(-dir.x, -dir.z);
|
|
891
|
+
rig.pitch = THREE.MathUtils.clamp(Math.atan2(dir.y, Math.hypot(dir.x, dir.z)), -CAMERA_BOX.pitchLimit, CAMERA_BOX.pitchLimit);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
rig.viewYaw = rig.yaw;
|
|
895
|
+
rig.viewPitch = rig.pitch;
|
|
896
|
+
this.place();
|
|
897
|
+
}
|
|
898
|
+
return this.cameraState();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
cameraState() {
|
|
902
|
+
const rig = this.app.cameras;
|
|
903
|
+
const forward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(rig.viewPitch, rig.viewYaw, 0, 'YXZ'));
|
|
904
|
+
const p = rig.freePos;
|
|
905
|
+
return {
|
|
906
|
+
position: [round(p.x), round(p.y), round(p.z)],
|
|
907
|
+
target: [round(p.x + forward.x * 20), round(p.y + forward.y * 20), round(p.z + forward.z * 20)],
|
|
908
|
+
fov: round(this.settings.fov, 2),
|
|
909
|
+
roll: round(this.settings.roll, 2),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Everything a script needs to know.
|
|
914
|
+
state() {
|
|
915
|
+
const s = this.settings;
|
|
916
|
+
return {
|
|
917
|
+
open: this.isOpen,
|
|
918
|
+
interface: this.isOpen && !this.hidden,
|
|
919
|
+
look: s.look,
|
|
920
|
+
vignette: s.vignette,
|
|
921
|
+
grain: s.grain,
|
|
922
|
+
focus: s.focus,
|
|
923
|
+
aperture: s.aperture,
|
|
924
|
+
fov: round(s.fov, 2),
|
|
925
|
+
roll: round(s.roll, 2),
|
|
926
|
+
guide: s.guide,
|
|
927
|
+
resolution: s.resolution,
|
|
928
|
+
windowSize: this.isOpen ? this.windowSize() : null,
|
|
929
|
+
camera: this.isOpen ? this.cameraState() : null,
|
|
930
|
+
lastCapture: this.lastCapture ? { ...this.lastCapture } : null,
|
|
931
|
+
looks: this.looks.slice(),
|
|
932
|
+
guides: this.guides.slice(),
|
|
933
|
+
resolutions: this.resolutions.slice(),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Hides or shows the panel, true hides it. Showing it gives the pointer back.
|
|
938
|
+
hideInterface(hidden = true) {
|
|
939
|
+
if (this.isOpen) {
|
|
940
|
+
this.setHidden(!!hidden);
|
|
941
|
+
}
|
|
942
|
+
return this.state();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
setHidden(hidden) {
|
|
946
|
+
this.hidden = hidden;
|
|
947
|
+
if (!this.panel) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
this.panel.hidden = hidden;
|
|
951
|
+
if (hidden) {
|
|
952
|
+
this.panel.removeAttribute('data-prompt');
|
|
953
|
+
if (this.panel.contains(document.activeElement)) {
|
|
954
|
+
document.activeElement.blur();
|
|
955
|
+
}
|
|
956
|
+
this.app.hud.toast(t('photo.hidden'), 'info', 1800);
|
|
957
|
+
} else {
|
|
958
|
+
// A panel that asks for the pointer frees it, as every panel of the page does.
|
|
959
|
+
this.panel.setAttribute('data-prompt', '');
|
|
960
|
+
if (document.pointerLockElement) {
|
|
961
|
+
document.exitPointerLock();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
status(text) {
|
|
967
|
+
if (this.statusLine) {
|
|
968
|
+
this.statusLine.textContent = text;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Keys.
|
|
973
|
+
|
|
974
|
+
onKey(e) {
|
|
975
|
+
const app = this.app;
|
|
976
|
+
this.shift = e.shiftKey;
|
|
977
|
+
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const lower = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
981
|
+
const field = e.target.closest && e.target.closest('input:not([type="range"]):not([type="checkbox"]), select, textarea, [contenteditable]');
|
|
982
|
+
if (!this.isOpen) {
|
|
983
|
+
if (lower === 'o' && app.page === 'watch' && !field && !document.querySelector('dialog[open]')
|
|
984
|
+
&& !(e.target.closest && e.target.closest(OWN_KEYS))) {
|
|
985
|
+
e.preventDefault();
|
|
986
|
+
e.stopPropagation();
|
|
987
|
+
this.open();
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (app.page !== 'watch') {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (e.key === 'Escape') {
|
|
995
|
+
e.preventDefault();
|
|
996
|
+
e.stopPropagation();
|
|
997
|
+
if (this.hidden) {
|
|
998
|
+
this.setHidden(false);
|
|
999
|
+
} else {
|
|
1000
|
+
this.close();
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const control = e.target.closest && e.target.closest('#photo-panel input, #photo-panel select, #photo-panel button');
|
|
1005
|
+
if (field || (control && CONTROL_KEYS.has(e.key))) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (e.key === 'Tab' || PAGE_KEYS.has(lower) || e.key === 'Shift') {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const s = this.settings;
|
|
1012
|
+
const step = e.shiftKey ? -1 : 1;
|
|
1013
|
+
let handled = true;
|
|
1014
|
+
if ('wasdqec'.includes(lower) && lower.length === 1) {
|
|
1015
|
+
app.cameras.press(lower, true, e.shiftKey);
|
|
1016
|
+
} else if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
|
1017
|
+
this.held.add(e.key);
|
|
1018
|
+
} else if (lower === 'z' || lower === 'x') {
|
|
1019
|
+
this.held.add(lower);
|
|
1020
|
+
} else if (e.key === '-' || e.key === '_') {
|
|
1021
|
+
this.held.add('-');
|
|
1022
|
+
} else if (e.key === '+' || e.key === '=') {
|
|
1023
|
+
this.held.add('+');
|
|
1024
|
+
} else if (e.code === 'BracketLeft' || e.code === 'BracketRight') {
|
|
1025
|
+
const sign = e.code === 'BracketLeft' ? -1 : 1;
|
|
1026
|
+
if (e.shiftKey) {
|
|
1027
|
+
this.update({ aperture: Math.round((s.aperture + sign * 0.05) * 100) / 100 });
|
|
1028
|
+
} else {
|
|
1029
|
+
const from = s.focus === 'auto' ? this.focusGuess() : s.focus;
|
|
1030
|
+
this.update({ focus: Math.round(from * (sign > 0 ? 1.1 : 1 / 1.1) * 10) / 10 });
|
|
1031
|
+
}
|
|
1032
|
+
} else if (lower === 'r') {
|
|
1033
|
+
this.update({ roll: 0, fov: DEFAULTS.fov, focus: 'auto' });
|
|
1034
|
+
} else if (lower === 'l') {
|
|
1035
|
+
this.update({ look: nextLook(s.look, step) });
|
|
1036
|
+
app.hud.toast(lookLabel(this.settings.look), 'info', 1000);
|
|
1037
|
+
} else if (lower === 'g') {
|
|
1038
|
+
this.update({ guide: nextGuide(s.guide, step) });
|
|
1039
|
+
} else if (lower === 'u') {
|
|
1040
|
+
this.setHidden(!this.hidden);
|
|
1041
|
+
} else if (lower === 'p') {
|
|
1042
|
+
if (!e.repeat) {
|
|
1043
|
+
this.save().catch((err) => console.warn('photo not saved', err));
|
|
1044
|
+
}
|
|
1045
|
+
} else if (lower === 'o') {
|
|
1046
|
+
this.close();
|
|
1047
|
+
} else {
|
|
1048
|
+
// The Watch page's other keys rest while photo mode holds the moment.
|
|
1049
|
+
handled = e.key.length === 1 || ['Home', 'End', 'Enter', 'PageUp', 'PageDown', 'Backspace', 'Delete'].includes(e.key);
|
|
1050
|
+
}
|
|
1051
|
+
if (handled) {
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
e.stopPropagation();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
onKeyUp(e) {
|
|
1058
|
+
this.shift = e.shiftKey;
|
|
1059
|
+
const lower = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
1060
|
+
this.held.delete(e.key);
|
|
1061
|
+
this.held.delete(lower);
|
|
1062
|
+
if (e.key === '_') {
|
|
1063
|
+
this.held.delete('-');
|
|
1064
|
+
} else if (e.key === '=') {
|
|
1065
|
+
this.held.delete('+');
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// A focus distance to start a change from while focus follows the center, the distance
|
|
1070
|
+
// along the view to the ground, or 30 meters for a view at the sky.
|
|
1071
|
+
focusGuess() {
|
|
1072
|
+
const cam = this.app.view.camera;
|
|
1073
|
+
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion);
|
|
1074
|
+
if (dir.y < -1e-3 && cam.position.y > 0) {
|
|
1075
|
+
return THREE.MathUtils.clamp(cam.position.y / -dir.y, FOCUS_MIN, FOCUS_MAX);
|
|
1076
|
+
}
|
|
1077
|
+
return 30;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Controllers.
|
|
1081
|
+
|
|
1082
|
+
readPads() {
|
|
1083
|
+
let list = [];
|
|
1084
|
+
try {
|
|
1085
|
+
list = navigator.getGamepads ? Array.from(navigator.getGamepads() || []) : [];
|
|
1086
|
+
} catch {
|
|
1087
|
+
list = [];
|
|
1088
|
+
}
|
|
1089
|
+
const pad = list.find((p) => p && p.connected && p.mapping === 'standard') || list.find((p) => p && p.connected);
|
|
1090
|
+
return pad ? readPad(pad, this.padButtons) : { move: [0, 0], look: [0, 0], rise: 0, zoom: 0, roll: 0, pressed: [], buttons: [] };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// The left stick flies, the right one turns, the triggers lower and raise, the pad zooms and
|
|
1094
|
+
// rolls, the shoulders change the look, the lower face button saves, the right one leaves,
|
|
1095
|
+
// the left one hides the interface and the upper one changes the guide.
|
|
1096
|
+
pollPad(dt) {
|
|
1097
|
+
const p = this.readPads();
|
|
1098
|
+
this.padButtons = p.buttons;
|
|
1099
|
+
const rig = this.app.cameras;
|
|
1100
|
+
const [mx, my] = p.move;
|
|
1101
|
+
const [lx, ly] = p.look;
|
|
1102
|
+
if (mx || my || p.rise) {
|
|
1103
|
+
rig.touch();
|
|
1104
|
+
const forward = new THREE.Vector3(-Math.sin(rig.yaw), 0, -Math.cos(rig.yaw));
|
|
1105
|
+
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
|
1106
|
+
const move = forward.multiplyScalar(-my).addScaledVector(right, mx);
|
|
1107
|
+
move.y += p.rise;
|
|
1108
|
+
rig.freePos.addScaledVector(move, rig.speed * dt);
|
|
1109
|
+
confine(rig.freePos);
|
|
1110
|
+
}
|
|
1111
|
+
if (lx || ly) {
|
|
1112
|
+
rig.touch();
|
|
1113
|
+
rig.yaw -= lx * 2.2 * dt;
|
|
1114
|
+
rig.pitch = THREE.MathUtils.clamp(rig.pitch - ly * 1.6 * dt, -CAMERA_BOX.pitchLimit, CAMERA_BOX.pitchLimit);
|
|
1115
|
+
}
|
|
1116
|
+
if (p.zoom || p.roll) {
|
|
1117
|
+
this.update({ fov: this.settings.fov + p.zoom * ZOOM * dt, roll: this.settings.roll + p.roll * ROLL * dt });
|
|
1118
|
+
}
|
|
1119
|
+
for (const name of p.pressed) {
|
|
1120
|
+
if (name === 'save') {
|
|
1121
|
+
this.save().catch((err) => console.warn('photo not saved', err));
|
|
1122
|
+
} else if (name === 'leave') {
|
|
1123
|
+
this.close();
|
|
1124
|
+
return;
|
|
1125
|
+
} else if (name === 'nextLook' || name === 'previousLook') {
|
|
1126
|
+
this.update({ look: nextLook(this.settings.look, name === 'nextLook' ? 1 : -1) });
|
|
1127
|
+
this.app.hud.toast(lookLabel(this.settings.look), 'info', 1000);
|
|
1128
|
+
} else if (name === 'interface') {
|
|
1129
|
+
this.setHidden(!this.hidden);
|
|
1130
|
+
} else if (name === 'guide') {
|
|
1131
|
+
this.update({ guide: nextGuide(this.settings.guide) });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// The panel and the guides.
|
|
1137
|
+
|
|
1138
|
+
buildUi() {
|
|
1139
|
+
const watch = document.getElementById('watch');
|
|
1140
|
+
if (!watch || this.panel) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
this.guideLayer = svg('svg', { class: 'photo-guides photo-ui', 'aria-hidden': 'true' });
|
|
1144
|
+
const select = (id, entries, label) => el('select', { id }, entries.map((e) => el('option', { value: e.id }, label(e))));
|
|
1145
|
+
const range = (id, min, max, stepValue) => el('input', { id, type: 'range', min, max, step: stepValue });
|
|
1146
|
+
const row = (label, id, control) => el('div', { class: 'photo-row' },
|
|
1147
|
+
el('label', { for: id }, label), el('output', { for: id, id: `${id}-value` }), control);
|
|
1148
|
+
this.controls = {
|
|
1149
|
+
look: select('photo-look', LOOKS, (e) => lookLabel(e.id)),
|
|
1150
|
+
vignette: range('photo-vignette', 0, 1, 0.01),
|
|
1151
|
+
grain: range('photo-grain', 0, 1, 0.01),
|
|
1152
|
+
auto: el('input', { id: 'photo-autofocus', type: 'checkbox' }),
|
|
1153
|
+
focus: range('photo-focus', 0, 1000, 1),
|
|
1154
|
+
aperture: range('photo-aperture', 0, 1, 0.01),
|
|
1155
|
+
fov: range('photo-fov', RANGES.fov[0], RANGES.fov[1], 0.5),
|
|
1156
|
+
roll: range('photo-roll', RANGES.roll[0], RANGES.roll[1], 0.5),
|
|
1157
|
+
guide: select('photo-guide', GUIDES, (e) => t(`photo.guide.${e.id}`)),
|
|
1158
|
+
resolution: select('photo-resolution', [...RESOLUTIONS, { id: 'custom', label: t('photo.resolution.custom') }],
|
|
1159
|
+
(e) => (e.id === 'window' ? t('photo.resolution.window') : e.preset ? presetLabel({ id: e.preset }) : e.label)),
|
|
1160
|
+
width: el('input', { id: 'photo-width', type: 'number', min: '16', max: '8192', step: '1', value: '1920', 'aria-label': t('photo.width') }),
|
|
1161
|
+
height: el('input', { id: 'photo-height', type: 'number', min: '16', max: '8192', step: '1', value: '1080', 'aria-label': t('photo.height') }),
|
|
1162
|
+
};
|
|
1163
|
+
const c = this.controls;
|
|
1164
|
+
this.statusLine = el('p', { class: 'photo-status', role: 'status' });
|
|
1165
|
+
const button = (text, title, action, cls = 'btn small') => el('button', { type: 'button', class: cls, title, onclick: action }, text);
|
|
1166
|
+
this.panel = el('aside', { id: 'photo-panel', class: 'photo-ui', 'aria-label': t('photo.title'), tabindex: '-1' },
|
|
1167
|
+
el('h2', {}, t('photo.title')),
|
|
1168
|
+
row(t('photo.look'), 'photo-look', c.look),
|
|
1169
|
+
row(t('photo.vignette'), 'photo-vignette', c.vignette),
|
|
1170
|
+
row(t('photo.grain'), 'photo-grain', c.grain),
|
|
1171
|
+
el('label', { class: 'photo-check' }, c.auto, t('photo.autofocus')),
|
|
1172
|
+
row(t('photo.focus'), 'photo-focus', c.focus),
|
|
1173
|
+
row(t('photo.aperture'), 'photo-aperture', c.aperture),
|
|
1174
|
+
row(t('photo.fov'), 'photo-fov', c.fov),
|
|
1175
|
+
row(t('photo.roll'), 'photo-roll', c.roll),
|
|
1176
|
+
row(t('photo.guide'), 'photo-guide', c.guide),
|
|
1177
|
+
row(t('photo.resolution'), 'photo-resolution', c.resolution),
|
|
1178
|
+
el('div', { class: 'photo-row photo-size', hidden: true }, c.width, el('span', { 'aria-hidden': 'true' }, t('photo.by')), c.height),
|
|
1179
|
+
el('div', { class: 'photo-actions' },
|
|
1180
|
+
button(t('photo.save'), t('photo.save.title'), () => this.save().catch((err) => console.warn('photo not saved', err)), 'btn small primary'),
|
|
1181
|
+
button(t('photo.hide'), t('photo.hide.title'), () => this.setHidden(true)),
|
|
1182
|
+
button(t('photo.leave'), t('photo.leave.title'), () => this.close())),
|
|
1183
|
+
this.statusLine,
|
|
1184
|
+
el('p', { class: 'photo-keys' }, tn('photo.keys')));
|
|
1185
|
+
const on = (control, event, read) => control.addEventListener(event, () => {
|
|
1186
|
+
try {
|
|
1187
|
+
this.update(read());
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
this.status(err.message);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
on(c.look, 'change', () => ({ look: c.look.value }));
|
|
1193
|
+
on(c.vignette, 'input', () => ({ vignette: Number(c.vignette.value) }));
|
|
1194
|
+
on(c.grain, 'input', () => ({ grain: Number(c.grain.value) }));
|
|
1195
|
+
on(c.auto, 'change', () => ({ focus: c.auto.checked ? 'auto' : this.focusGuess() }));
|
|
1196
|
+
on(c.focus, 'input', () => ({ focus: sliderToFocus(c.focus.value) }));
|
|
1197
|
+
on(c.aperture, 'input', () => ({ aperture: Number(c.aperture.value) }));
|
|
1198
|
+
on(c.fov, 'input', () => ({ fov: Number(c.fov.value) }));
|
|
1199
|
+
on(c.roll, 'input', () => ({ roll: Number(c.roll.value) }));
|
|
1200
|
+
on(c.guide, 'change', () => ({ guide: c.guide.value }));
|
|
1201
|
+
const custom = () => {
|
|
1202
|
+
const size = { width: Number(c.width.value), height: Number(c.height.value) };
|
|
1203
|
+
// The checks of photo-core.js, here with the message in the language of the page.
|
|
1204
|
+
for (const side of ['width', 'height']) {
|
|
1205
|
+
const v = size[side];
|
|
1206
|
+
if (!Number.isInteger(v) || v < MIN_SIDE || v > MAX_SIDE) {
|
|
1207
|
+
throw new RangeError(t(`photo.error.${side}`, { min: MIN_SIDE, max: MAX_SIDE, value: String(v) }));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return { resolution: size };
|
|
1211
|
+
};
|
|
1212
|
+
on(c.resolution, 'change', () => (c.resolution.value === 'custom' ? custom() : { resolution: c.resolution.value }));
|
|
1213
|
+
on(c.width, 'change', custom);
|
|
1214
|
+
on(c.height, 'change', custom);
|
|
1215
|
+
watch.append(this.guideLayer, this.panel);
|
|
1216
|
+
this.resizeObserver = new ResizeObserver(() => this.drawGuides());
|
|
1217
|
+
this.resizeObserver.observe(document.getElementById('stage'));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
destroyUi() {
|
|
1221
|
+
if (this.resizeObserver) {
|
|
1222
|
+
this.resizeObserver.disconnect();
|
|
1223
|
+
this.resizeObserver = null;
|
|
1224
|
+
}
|
|
1225
|
+
for (const node of [this.panel, this.guideLayer]) {
|
|
1226
|
+
if (node) {
|
|
1227
|
+
node.remove();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
this.panel = null;
|
|
1231
|
+
this.guideLayer = null;
|
|
1232
|
+
this.controls = null;
|
|
1233
|
+
this.statusLine = null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
syncPanel() {
|
|
1237
|
+
const c = this.controls;
|
|
1238
|
+
if (!c) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const s = this.settings;
|
|
1242
|
+
const show = (control, text) => {
|
|
1243
|
+
const out = document.getElementById(`${control.id}-value`);
|
|
1244
|
+
if (out) {
|
|
1245
|
+
out.textContent = text;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
const value = (control, v) => {
|
|
1249
|
+
if (document.activeElement !== control || control.type !== 'range') {
|
|
1250
|
+
control.value = String(v);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
value(c.look, s.look);
|
|
1254
|
+
value(c.vignette, s.vignette);
|
|
1255
|
+
show(c.vignette, t('photo.value.percent', { value: Math.round(s.vignette * 100) }));
|
|
1256
|
+
value(c.grain, s.grain);
|
|
1257
|
+
show(c.grain, t('photo.value.percent', { value: Math.round(s.grain * 100) }));
|
|
1258
|
+
c.auto.checked = s.focus === 'auto';
|
|
1259
|
+
value(c.focus, focusToSlider(s.focus === 'auto' ? this.focusGuess() : s.focus));
|
|
1260
|
+
show(c.focus, s.focus === 'auto' ? t('photo.value.center') : t('photo.value.meters', { value: decimal(s.focus) }));
|
|
1261
|
+
value(c.aperture, s.aperture);
|
|
1262
|
+
show(c.aperture, t('photo.value.percent', { value: Math.round(s.aperture * 100) }));
|
|
1263
|
+
value(c.fov, Math.round(s.fov * 2) / 2);
|
|
1264
|
+
show(c.fov, t('photo.value.degrees', { count: Math.round(s.fov) }));
|
|
1265
|
+
value(c.roll, Math.round(s.roll * 2) / 2);
|
|
1266
|
+
show(c.roll, t('photo.value.degrees', { count: Math.round(s.roll) }));
|
|
1267
|
+
value(c.guide, s.guide);
|
|
1268
|
+
const known = typeof s.resolution === 'string' && RESOLUTIONS.some((r) => r.id === s.resolution);
|
|
1269
|
+
value(c.resolution, known ? s.resolution : 'custom');
|
|
1270
|
+
c.width.parentElement.hidden = known;
|
|
1271
|
+
if (!known && s.resolution && typeof s.resolution === 'object') {
|
|
1272
|
+
value(c.width, s.resolution.width);
|
|
1273
|
+
value(c.height, s.resolution.height);
|
|
1274
|
+
}
|
|
1275
|
+
show(c.resolution, '');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// The guides and the frame of a photo of another shape, over the picture and never in it.
|
|
1279
|
+
drawGuides() {
|
|
1280
|
+
const layer = this.guideLayer;
|
|
1281
|
+
if (!layer) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const canvas = this.app.view.renderer.domElement;
|
|
1285
|
+
const w = canvas.clientWidth || 1;
|
|
1286
|
+
const h = canvas.clientHeight || 1;
|
|
1287
|
+
let output = null;
|
|
1288
|
+
try {
|
|
1289
|
+
const win = this.windowSize();
|
|
1290
|
+
const size = resolveResolution(this.settings.resolution, win.width, win.height);
|
|
1291
|
+
output = this.settings.resolution === 'window' ? null : size;
|
|
1292
|
+
} catch {
|
|
1293
|
+
output = null;
|
|
1294
|
+
}
|
|
1295
|
+
const g = guideGeometry(this.settings.guide, w, h, output);
|
|
1296
|
+
layer.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
1297
|
+
const nodes = [];
|
|
1298
|
+
if (g.frame) {
|
|
1299
|
+
const f = g.frame;
|
|
1300
|
+
nodes.push(svg('path', { class: 'shade', d: `M0 0H${w}V${h}H0Z M${f.x} ${f.y}V${f.y + f.height}H${f.x + f.width}V${f.y}Z` }));
|
|
1301
|
+
for (const cls of ['halo', 'ink']) {
|
|
1302
|
+
nodes.push(svg('rect', { class: cls, x: f.x, y: f.y, width: f.width, height: f.height }));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const cls of ['halo', 'ink']) {
|
|
1306
|
+
for (const [x1, y1, x2, y2] of g.lines) {
|
|
1307
|
+
nodes.push(svg('line', { class: cls, x1, y1, x2, y2 }));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
layer.replaceChildren(...nodes);
|
|
1311
|
+
layer.dataset.guide = g.kind;
|
|
1312
|
+
layer.dataset.lines = String(g.lines.length);
|
|
1313
|
+
}
|
|
1314
|
+
}
|