@the-inclusionist/engine 9.0.0 → 11.0.0
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.
- package/README.md +120 -78
- package/app/css/style.css +761 -377
- package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
- package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
- package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
- package/app/public/vendor/fonts.css +36 -84
- package/dist-pkg/boot/create-game.d.ts +499 -219
- package/dist-pkg/boot/create-game.js +3944 -562
- package/dist-pkg/core/a11y-sr.d.ts +25 -3
- package/dist-pkg/core/a11y-sr.js +38 -19
- package/dist-pkg/core/accessible-label.d.ts +17 -0
- package/dist-pkg/core/accessible-label.js +37 -0
- package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
- package/dist-pkg/core/accommodation-subjects.js +32 -0
- package/dist-pkg/core/accommodations.d.ts +125 -0
- package/dist-pkg/core/accommodations.js +195 -0
- package/dist-pkg/core/actions.d.ts +108 -55
- package/dist-pkg/core/actions.js +122 -71
- package/dist-pkg/core/calm-mode.d.ts +31 -0
- package/dist-pkg/core/calm-mode.js +47 -0
- package/dist-pkg/core/camera-cycle.d.ts +15 -0
- package/dist-pkg/core/camera-cycle.js +7 -0
- package/dist-pkg/core/caption-duration.d.ts +15 -0
- package/dist-pkg/core/caption-duration.js +19 -0
- package/dist-pkg/core/cartridge-problems.d.ts +34 -0
- package/dist-pkg/core/cartridge-problems.js +53 -0
- package/dist-pkg/core/constants.d.ts +0 -54
- package/dist-pkg/core/constants.js +9 -76
- package/dist-pkg/core/contract.d.ts +142 -164
- package/dist-pkg/core/contract.js +222 -208
- package/dist-pkg/core/dom-query.d.ts +4 -4
- package/dist-pkg/core/dom-query.js +12 -20
- package/dist-pkg/core/entity.d.ts +62 -77
- package/dist-pkg/core/entity.js +18 -32
- package/dist-pkg/core/escape-html.d.ts +9 -10
- package/dist-pkg/core/escape-html.js +16 -20
- package/dist-pkg/core/flash-threshold.d.ts +17 -0
- package/dist-pkg/core/flash-threshold.js +113 -0
- package/dist-pkg/core/game-speed.d.ts +11 -0
- package/dist-pkg/core/game-speed.js +16 -0
- package/dist-pkg/core/genres.d.ts +20 -0
- package/dist-pkg/core/genres.js +44 -0
- package/dist-pkg/core/i18n.d.ts +71 -60
- package/dist-pkg/core/i18n.js +186 -200
- package/dist-pkg/core/loop.d.ts +17 -6
- package/dist-pkg/core/loop.js +26 -26
- package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
- package/dist-pkg/core/pause-icon-catalogue.js +54 -0
- package/dist-pkg/core/ring.d.ts +17 -0
- package/dist-pkg/core/ring.js +29 -0
- package/dist-pkg/core/rng.d.ts +7 -12
- package/dist-pkg/core/rng.js +23 -53
- package/dist-pkg/core/route.d.ts +26 -26
- package/dist-pkg/core/route.js +126 -121
- package/dist-pkg/core/scenes.d.ts +37 -40
- package/dist-pkg/core/scenes.js +41 -43
- package/dist-pkg/core/screens.d.ts +4 -4
- package/dist-pkg/core/screens.js +11 -16
- package/dist-pkg/core/session-clock.d.ts +23 -0
- package/dist-pkg/core/session-clock.js +37 -0
- package/dist-pkg/core/setting-defaults.d.ts +61 -0
- package/dist-pkg/core/setting-defaults.js +66 -0
- package/dist-pkg/core/speech-rate.d.ts +29 -0
- package/dist-pkg/core/speech-rate.js +47 -0
- package/dist-pkg/core/state.d.ts +118 -118
- package/dist-pkg/core/state.js +322 -297
- package/dist-pkg/core/visual-cycles.d.ts +30 -0
- package/dist-pkg/core/visual-cycles.js +52 -0
- package/dist-pkg/core/visual-state.d.ts +13 -0
- package/dist-pkg/core/visual-state.js +6 -0
- package/dist-pkg/educational/activities-registry.d.ts +1 -1
- package/dist-pkg/educational/activities-registry.js +9 -8
- package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
- package/dist-pkg/educational/adaptive-engine.js +17 -17
- package/dist-pkg/educational/segment-bar.d.ts +12 -12
- package/dist-pkg/educational/segment-bar.js +9 -9
- package/dist-pkg/i18n/en.js +324 -223
- package/dist-pkg/i18n/es.js +323 -223
- package/dist-pkg/i18n/pt.js +348 -226
- package/dist-pkg/input/default-bindings.d.ts +37 -38
- package/dist-pkg/input/default-bindings.js +127 -144
- package/dist-pkg/input/devices.d.ts +7 -7
- package/dist-pkg/input/devices.js +17 -16
- package/dist-pkg/input/edges.d.ts +23 -32
- package/dist-pkg/input/edges.js +23 -28
- package/dist-pkg/input/empathy-filter.d.ts +22 -0
- package/dist-pkg/input/empathy-filter.js +34 -0
- package/dist-pkg/input/face-map.d.ts +96 -0
- package/dist-pkg/input/face-map.js +255 -0
- package/dist-pkg/input/face-signals.d.ts +40 -0
- package/dist-pkg/input/face-signals.js +48 -0
- package/dist-pkg/input/gamepad.d.ts +195 -145
- package/dist-pkg/input/gamepad.js +383 -556
- package/dist-pkg/input/gaze-cycle.d.ts +64 -0
- package/dist-pkg/input/gaze-cycle.js +133 -0
- package/dist-pkg/input/gaze-relative.d.ts +68 -0
- package/dist-pkg/input/gaze-relative.js +142 -0
- package/dist-pkg/input/hand-map.d.ts +28 -0
- package/dist-pkg/input/hand-map.js +142 -0
- package/dist-pkg/input/input-cooldown.d.ts +17 -0
- package/dist-pkg/input/input-cooldown.js +39 -0
- package/dist-pkg/input/key-default.d.ts +30 -0
- package/dist-pkg/input/key-default.js +60 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +15 -21
- package/dist-pkg/input/keyboard-runtime.js +23 -27
- package/dist-pkg/input/keyboard.d.ts +68 -63
- package/dist-pkg/input/keyboard.js +83 -105
- package/dist-pkg/input/keydown.d.ts +123 -137
- package/dist-pkg/input/keydown.js +284 -289
- package/dist-pkg/input/latch-edge.d.ts +33 -21
- package/dist-pkg/input/latch-edge.js +34 -35
- package/dist-pkg/input/latch-scope.d.ts +51 -50
- package/dist-pkg/input/latch-scope.js +61 -82
- package/dist-pkg/input/latch-store.d.ts +26 -30
- package/dist-pkg/input/latch-store.js +42 -50
- package/dist-pkg/input/latch-sync.d.ts +29 -29
- package/dist-pkg/input/latch-sync.js +25 -25
- package/dist-pkg/input/latch.d.ts +14 -17
- package/dist-pkg/input/latch.js +29 -37
- package/dist-pkg/input/pad-defaults.d.ts +13 -16
- package/dist-pkg/input/pad-defaults.js +21 -28
- package/dist-pkg/input/pad-reading.d.ts +76 -0
- package/dist-pkg/input/pad-reading.js +169 -0
- package/dist-pkg/input/pad-wizard.d.ts +99 -0
- package/dist-pkg/input/pad-wizard.js +256 -0
- package/dist-pkg/input/pointer-space.d.ts +19 -20
- package/dist-pkg/input/pointer-space.js +27 -32
- package/dist-pkg/input/pointer.d.ts +39 -40
- package/dist-pkg/input/pointer.js +42 -45
- package/dist-pkg/input/state.d.ts +101 -89
- package/dist-pkg/input/state.js +54 -134
- package/dist-pkg/input/switch-scan.d.ts +59 -0
- package/dist-pkg/input/switch-scan.js +67 -0
- package/dist-pkg/input/synthetic-source.d.ts +42 -0
- package/dist-pkg/input/synthetic-source.js +57 -0
- package/dist-pkg/input/touch-bindings.d.ts +127 -116
- package/dist-pkg/input/touch-bindings.js +158 -173
- package/dist-pkg/input/touch.d.ts +147 -72
- package/dist-pkg/input/touch.js +362 -126
- package/dist-pkg/input/transport-in-use.d.ts +107 -0
- package/dist-pkg/input/transport-in-use.js +130 -0
- package/dist-pkg/input/transports.d.ts +90 -117
- package/dist-pkg/input/transports.js +104 -106
- package/dist-pkg/input/virtual-controller.d.ts +82 -0
- package/dist-pkg/input/virtual-controller.js +69 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
- package/dist-pkg/input/vocabulary-migration.js +80 -79
- package/dist-pkg/input/voice-map.d.ts +59 -0
- package/dist-pkg/input/voice-map.js +123 -0
- package/dist-pkg/platform/audio-ambient.d.ts +13 -2
- package/dist-pkg/platform/audio-ambient.js +14 -14
- package/dist-pkg/platform/audio-earcons.d.ts +20 -19
- package/dist-pkg/platform/audio-earcons.js +51 -42
- package/dist-pkg/platform/audio-jingles.js +9 -10
- package/dist-pkg/platform/audio-mixer.d.ts +14 -2
- package/dist-pkg/platform/audio-mixer.js +31 -39
- package/dist-pkg/platform/audio-sonar.d.ts +49 -152
- package/dist-pkg/platform/audio-sonar.js +124 -302
- package/dist-pkg/platform/audio.d.ts +62 -19
- package/dist-pkg/platform/audio.js +193 -168
- package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
- package/dist-pkg/platform/choose-by-voice.js +157 -0
- package/dist-pkg/platform/flash-sampler.d.ts +19 -0
- package/dist-pkg/platform/flash-sampler.js +79 -0
- package/dist-pkg/platform/font-library.d.ts +77 -0
- package/dist-pkg/platform/font-library.js +98 -0
- package/dist-pkg/platform/font-library.json +721 -0
- package/dist-pkg/platform/heavy-catalogue.d.ts +77 -0
- package/dist-pkg/platform/heavy-catalogue.js +256 -0
- package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
- package/dist-pkg/platform/heavy-mirror.js +73 -0
- package/dist-pkg/platform/heavy.d.ts +136 -0
- package/dist-pkg/platform/heavy.js +324 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
- package/dist-pkg/platform/interruptible-speech.js +77 -54
- package/dist-pkg/platform/kokoro-port.d.ts +27 -0
- package/dist-pkg/platform/kokoro-port.js +50 -0
- package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
- package/dist-pkg/platform/kokoro-runtime.js +52 -0
- package/dist-pkg/platform/kokoro.d.ts +70 -0
- package/dist-pkg/platform/kokoro.js +133 -0
- package/dist-pkg/platform/listener-scope.d.ts +12 -0
- package/dist-pkg/platform/listener-scope.js +90 -0
- package/dist-pkg/platform/locale-host.d.ts +14 -0
- package/dist-pkg/platform/locale-host.js +45 -0
- package/dist-pkg/platform/microphone.d.ts +51 -0
- package/dist-pkg/platform/microphone.js +118 -0
- package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
- package/dist-pkg/platform/onnx-runtime.js +42 -0
- package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
- package/dist-pkg/platform/reading-in-worker.js +125 -0
- package/dist-pkg/platform/reading-model.d.ts +94 -0
- package/dist-pkg/platform/reading-model.js +272 -0
- package/dist-pkg/platform/reading-runtime.d.ts +24 -0
- package/dist-pkg/platform/reading-runtime.js +164 -0
- package/dist-pkg/platform/reading-worker.d.ts +41 -0
- package/dist-pkg/platform/reading-worker.js +58 -0
- package/dist-pkg/platform/reading.d.ts +80 -0
- package/dist-pkg/platform/reading.js +136 -0
- package/dist-pkg/platform/speech-recognition.d.ts +60 -0
- package/dist-pkg/platform/speech-recognition.js +104 -0
- package/dist-pkg/platform/speech.d.ts +23 -1
- package/dist-pkg/platform/speech.js +29 -18
- package/dist-pkg/platform/storage-keys.d.ts +96 -0
- package/dist-pkg/platform/storage-keys.js +106 -0
- package/dist-pkg/platform/storage.d.ts +46 -111
- package/dist-pkg/platform/storage.js +104 -169
- package/dist-pkg/platform/tts.d.ts +85 -39
- package/dist-pkg/platform/tts.js +447 -127
- package/dist-pkg/platform/vision-loop.d.ts +23 -0
- package/dist-pkg/platform/vision-loop.js +56 -0
- package/dist-pkg/platform/vision.d.ts +132 -0
- package/dist-pkg/platform/vision.js +117 -0
- package/dist-pkg/platform/voice-listener.d.ts +40 -0
- package/dist-pkg/platform/voice-listener.js +88 -0
- package/dist-pkg/platform/voice-plan.d.ts +15 -98
- package/dist-pkg/platform/voice-plan.js +56 -143
- package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
- package/dist-pkg/platform/vosk-runtime.js +151 -0
- package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
- package/dist-pkg/platform/vosk-vocabulary.js +178 -0
- package/dist-pkg/render/canvas.d.ts +13 -7
- package/dist-pkg/render/canvas.js +13 -13
- package/dist-pkg/render/crt.d.ts +48 -12
- package/dist-pkg/render/crt.js +78 -80
- package/dist-pkg/render/cvd-matrices.d.ts +22 -18
- package/dist-pkg/render/cvd-matrices.js +37 -38
- package/dist-pkg/render/hc-role-data.d.ts +7 -7
- package/dist-pkg/render/hc-role-data.js +14 -14
- package/dist-pkg/render/high-contrast.d.ts +67 -56
- package/dist-pkg/render/high-contrast.js +226 -217
- package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
- package/dist-pkg/render/low-vision-drawing.js +46 -0
- package/dist-pkg/render/lq-filter.d.ts +32 -20
- package/dist-pkg/render/lq-filter.js +59 -65
- package/dist-pkg/render/port.d.ts +86 -88
- package/dist-pkg/render/port.js +17 -23
- package/dist-pkg/render/screen-pipeline.d.ts +42 -42
- package/dist-pkg/render/screen-pipeline.js +80 -87
- package/dist-pkg/render/sprite-fx.d.ts +2 -1
- package/dist-pkg/render/sprite-fx.js +16 -14
- package/dist-pkg/render/viewports.d.ts +20 -15
- package/dist-pkg/render/viewports.js +83 -114
- package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
- package/dist-pkg/render/viz-axes-labels.js +88 -0
- package/dist-pkg/render/viz-axes.d.ts +89 -86
- package/dist-pkg/render/viz-axes.js +128 -116
- package/dist-pkg/render/viz-modes.d.ts +28 -29
- package/dist-pkg/render/viz-modes.js +40 -45
- package/dist-pkg/render/viz-refusal.d.ts +32 -0
- package/dist-pkg/render/viz-refusal.js +57 -0
- package/dist-pkg/render/viz-setters.d.ts +104 -66
- package/dist-pkg/render/viz-setters.js +182 -191
- package/dist-pkg/ui/aac-sets.d.ts +45 -0
- package/dist-pkg/ui/aac-sets.js +41 -0
- package/dist-pkg/ui/audio-choices.d.ts +68 -0
- package/dist-pkg/ui/audio-choices.js +98 -0
- package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
- package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
- package/dist-pkg/ui/camera-control.d.ts +53 -0
- package/dist-pkg/ui/camera-control.js +48 -0
- package/dist-pkg/ui/changed-mark.d.ts +9 -8
- package/dist-pkg/ui/changed-mark.js +22 -53
- package/dist-pkg/ui/control-choices.d.ts +37 -0
- package/dist-pkg/ui/control-choices.js +84 -0
- package/dist-pkg/ui/debug-panel.d.ts +59 -43
- package/dist-pkg/ui/debug-panel.js +68 -69
- package/dist-pkg/ui/declared-words.d.ts +21 -0
- package/dist-pkg/ui/declared-words.js +66 -0
- package/dist-pkg/ui/dom.d.ts +10 -14
- package/dist-pkg/ui/dom.js +10 -47
- package/dist-pkg/ui/drawing-problems.d.ts +25 -0
- package/dist-pkg/ui/drawing-problems.js +94 -0
- package/dist-pkg/ui/eye-control.d.ts +36 -0
- package/dist-pkg/ui/eye-control.js +178 -0
- package/dist-pkg/ui/face-control.d.ts +25 -0
- package/dist-pkg/ui/face-control.js +183 -0
- package/dist-pkg/ui/focus-trap.d.ts +39 -49
- package/dist-pkg/ui/focus-trap.js +56 -65
- package/dist-pkg/ui/fonts.d.ts +157 -68
- package/dist-pkg/ui/fonts.js +305 -115
- package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
- package/dist-pkg/ui/footer-scroll-driver.js +201 -0
- package/dist-pkg/ui/footer-scroll.d.ts +38 -0
- package/dist-pkg/ui/footer-scroll.js +55 -0
- package/dist-pkg/ui/game-options.d.ts +47 -0
- package/dist-pkg/ui/game-options.js +158 -0
- package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
- package/dist-pkg/ui/gaze-overlay.js +165 -0
- package/dist-pkg/ui/hand-control.d.ts +25 -0
- package/dist-pkg/ui/hand-control.js +143 -0
- package/dist-pkg/ui/help-panel.d.ts +87 -0
- package/dist-pkg/ui/help-panel.js +242 -0
- package/dist-pkg/ui/hud-bands.d.ts +62 -0
- package/dist-pkg/ui/hud-bands.js +146 -0
- package/dist-pkg/ui/hud-row.d.ts +34 -0
- package/dist-pkg/ui/hud-row.js +80 -0
- package/dist-pkg/ui/hud.d.ts +73 -73
- package/dist-pkg/ui/hud.js +94 -98
- package/dist-pkg/ui/item-announcement.d.ts +13 -12
- package/dist-pkg/ui/item-announcement.js +25 -26
- package/dist-pkg/ui/layout.d.ts +103 -28
- package/dist-pkg/ui/layout.js +117 -99
- package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
- package/dist-pkg/ui/libras-avatar-clip.js +86 -0
- package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
- package/dist-pkg/ui/libras-avatar-load.js +134 -0
- package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
- package/dist-pkg/ui/libras-avatar-plan.js +211 -0
- package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
- package/dist-pkg/ui/libras-avatar-player.js +226 -0
- package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
- package/dist-pkg/ui/libras-avatar-stage.js +208 -0
- package/dist-pkg/ui/libras-glosses.d.ts +28 -0
- package/dist-pkg/ui/libras-glosses.js +161 -0
- package/dist-pkg/ui/locale-flags.d.ts +16 -0
- package/dist-pkg/ui/locale-flags.js +30 -0
- package/dist-pkg/ui/loop-crash.d.ts +17 -17
- package/dist-pkg/ui/loop-crash.js +33 -62
- package/dist-pkg/ui/menu-intent.d.ts +61 -0
- package/dist-pkg/ui/menu-intent.js +88 -0
- package/dist-pkg/ui/menu-items.d.ts +20 -0
- package/dist-pkg/ui/menu-items.js +51 -0
- package/dist-pkg/ui/menu-nav.d.ts +98 -92
- package/dist-pkg/ui/menu-nav.js +398 -286
- package/dist-pkg/ui/mobility-choices.d.ts +26 -0
- package/dist-pkg/ui/mobility-choices.js +41 -0
- package/dist-pkg/ui/motion-choices.d.ts +31 -0
- package/dist-pkg/ui/motion-choices.js +68 -0
- package/dist-pkg/ui/motion-scene.d.ts +24 -19
- package/dist-pkg/ui/motion-scene.js +29 -61
- package/dist-pkg/ui/mount-panel.d.ts +81 -0
- package/dist-pkg/ui/mount-panel.js +61 -0
- package/dist-pkg/ui/panel-shell.d.ts +65 -31
- package/dist-pkg/ui/panel-shell.js +92 -66
- package/dist-pkg/ui/panel-widgets.d.ts +150 -0
- package/dist-pkg/ui/panel-widgets.js +294 -0
- package/dist-pkg/ui/pause-buttons.d.ts +35 -0
- package/dist-pkg/ui/pause-buttons.js +66 -0
- package/dist-pkg/ui/pause-icons.d.ts +261 -347
- package/dist-pkg/ui/pause-icons.js +671 -601
- package/dist-pkg/ui/pause-markup.d.ts +86 -0
- package/dist-pkg/ui/pause-markup.js +125 -0
- package/dist-pkg/ui/reach-notice.d.ts +22 -23
- package/dist-pkg/ui/reach-notice.js +55 -58
- package/dist-pkg/ui/scan-overlay.d.ts +23 -0
- package/dist-pkg/ui/scan-overlay.js +50 -0
- package/dist-pkg/ui/screen-text.d.ts +25 -0
- package/dist-pkg/ui/screen-text.js +152 -0
- package/dist-pkg/ui/session-clock.d.ts +32 -0
- package/dist-pkg/ui/session-clock.js +67 -0
- package/dist-pkg/ui/settings-aac.d.ts +70 -0
- package/dist-pkg/ui/settings-aac.js +208 -0
- package/dist-pkg/ui/settings-audio.d.ts +97 -114
- package/dist-pkg/ui/settings-audio.js +493 -425
- package/dist-pkg/ui/settings-controls.d.ts +53 -96
- package/dist-pkg/ui/settings-controls.js +143 -215
- package/dist-pkg/ui/settings-empathy.d.ts +39 -30
- package/dist-pkg/ui/settings-empathy.js +53 -60
- package/dist-pkg/ui/settings-mobility.d.ts +192 -0
- package/dist-pkg/ui/settings-mobility.js +332 -0
- package/dist-pkg/ui/settings-motion.d.ts +91 -70
- package/dist-pkg/ui/settings-motion.js +287 -198
- package/dist-pkg/ui/settings-panel.d.ts +58 -45
- package/dist-pkg/ui/settings-panel.js +189 -94
- package/dist-pkg/ui/settings-typo.d.ts +29 -78
- package/dist-pkg/ui/settings-typo.js +158 -129
- package/dist-pkg/ui/settings-visual.d.ts +29 -87
- package/dist-pkg/ui/settings-visual.js +240 -193
- package/dist-pkg/ui/shell.d.ts +112 -135
- package/dist-pkg/ui/shell.js +148 -217
- package/dist-pkg/ui/simulation-list.d.ts +27 -0
- package/dist-pkg/ui/simulation-list.js +76 -0
- package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
- package/dist-pkg/ui/simulation-over-the-world.js +131 -0
- package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
- package/dist-pkg/ui/simulation-refusal.js +8 -56
- package/dist-pkg/ui/switchable-control.d.ts +11 -0
- package/dist-pkg/ui/switchable-control.js +19 -0
- package/dist-pkg/ui/title.d.ts +3 -12
- package/dist-pkg/ui/title.js +49 -18
- package/dist-pkg/ui/top-band.d.ts +26 -0
- package/dist-pkg/ui/top-band.js +96 -0
- package/dist-pkg/ui/typo-choices.d.ts +71 -0
- package/dist-pkg/ui/typo-choices.js +85 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
- package/dist-pkg/ui/visual-axes-panel.js +12 -94
- package/dist-pkg/ui/visual-choices.d.ts +77 -0
- package/dist-pkg/ui/visual-choices.js +107 -0
- package/dist-pkg/ui/vlibras.d.ts +77 -10
- package/dist-pkg/ui/vlibras.js +130 -81
- package/dist-pkg/ui/voice-control.d.ts +68 -0
- package/dist-pkg/ui/voice-control.js +276 -0
- package/dist-pkg/ui/voice-settings.d.ts +95 -0
- package/dist-pkg/ui/voice-settings.js +352 -0
- package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
- package/dist-pkg/ui/where-the-child-is.js +70 -0
- package/docs/CREDITS.md +300 -44
- package/docs/LICENSES.md +189 -145
- package/package.json +51 -27
- package/scripts/check-cartridge.mjs +78 -0
- package/scripts/game-build.d.mts +25 -0
- package/scripts/game-build.mjs +144 -0
- package/scripts/heavy-into-the-delivery.mjs +388 -0
- package/scripts/libras-avatar.json +680 -0
- package/scripts/libras-avatar.mjs +246 -0
- package/scripts/libras-glosses/gloss.py +95 -0
- package/scripts/libras-glosses/pyproject.toml +32 -0
- package/scripts/libras-glosses/uv.lock +781 -0
- package/scripts/libras-glosses.mjs +351 -0
- package/scripts/licences/Apache-2.0.txt +176 -0
- package/scripts/licences/GPL-3.0.txt +674 -0
- package/scripts/licences/MIT.txt +17 -0
- package/scripts/licences/OFL-1.1.txt +86 -0
- package/scripts/licences/UFL-1.0.txt +96 -0
- package/scripts/licences/fonts.mjs +389 -0
- package/scripts/licences/third-party.mjs +301 -0
- package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
- package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/dist-pkg/core/anel.d.ts +0 -17
- package/dist-pkg/core/anel.js +0 -31
- package/dist-pkg/core/collision.d.ts +0 -21
- package/dist-pkg/core/collision.js +0 -62
- package/dist-pkg/core/layers.d.ts +0 -63
- package/dist-pkg/core/layers.js +0 -95
- package/dist-pkg/core/letter-grid.d.ts +0 -55
- package/dist-pkg/core/letter-grid.js +0 -116
- package/dist-pkg/core/password.d.ts +0 -34
- package/dist-pkg/core/password.js +0 -205
- package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
- package/dist-pkg/core/rotulo-acessivel.js +0 -41
- package/dist-pkg/core/run-state.d.ts +0 -115
- package/dist-pkg/core/run-state.js +0 -40
- package/dist-pkg/core/tiles.d.ts +0 -12
- package/dist-pkg/core/tiles.js +0 -61
- package/dist-pkg/core/world.d.ts +0 -4
- package/dist-pkg/core/world.js +0 -58
- package/dist-pkg/input/origem-sintetica.d.ts +0 -44
- package/dist-pkg/input/origem-sintetica.js +0 -60
- package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
- package/dist-pkg/input/transporte-em-uso.js +0 -130
- package/dist-pkg/platform/audio-nav.d.ts +0 -51
- package/dist-pkg/platform/audio-nav.js +0 -99
- package/dist-pkg/platform/guide-intensity.d.ts +0 -40
- package/dist-pkg/platform/guide-intensity.js +0 -73
- package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
- package/dist-pkg/platform/pesados-catalogo.js +0 -177
- package/dist-pkg/platform/pesados.d.ts +0 -31
- package/dist-pkg/platform/pesados.js +0 -75
- package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
- package/dist-pkg/platform/vozes-prontas.js +0 -61
- package/dist-pkg/render/camera.d.ts +0 -58
- package/dist-pkg/render/camera.js +0 -105
- package/dist-pkg/render/cenario-data.d.ts +0 -127
- package/dist-pkg/render/cenario-data.js +0 -145
- package/dist-pkg/render/city-tex.d.ts +0 -63
- package/dist-pkg/render/city-tex.js +0 -228
- package/dist-pkg/render/city-tiles.d.ts +0 -11
- package/dist-pkg/render/city-tiles.js +0 -140
- package/dist-pkg/render/draw.d.ts +0 -149
- package/dist-pkg/render/draw.js +0 -231
- package/dist-pkg/render/fx.d.ts +0 -63
- package/dist-pkg/render/fx.js +0 -123
- package/dist-pkg/render/minimap.d.ts +0 -11
- package/dist-pkg/render/minimap.js +0 -94
- package/dist-pkg/render/parallax.d.ts +0 -85
- package/dist-pkg/render/parallax.js +0 -158
- package/dist-pkg/render/player-anim.d.ts +0 -71
- package/dist-pkg/render/player-anim.js +0 -152
- package/dist-pkg/render/recycling-tex.d.ts +0 -48
- package/dist-pkg/render/recycling-tex.js +0 -164
- package/dist-pkg/render/scene-city.d.ts +0 -77
- package/dist-pkg/render/scene-city.js +0 -181
- package/dist-pkg/render/scene-parallax.d.ts +0 -72
- package/dist-pkg/render/scene-parallax.js +0 -426
- package/dist-pkg/render/scene-sky.d.ts +0 -126
- package/dist-pkg/render/scene-sky.js +0 -295
- package/dist-pkg/render/set-cenario.d.ts +0 -54
- package/dist-pkg/render/set-cenario.js +0 -103
- package/dist-pkg/render/textures.d.ts +0 -75
- package/dist-pkg/render/textures.js +0 -240
- package/dist-pkg/render/title-scene.d.ts +0 -46
- package/dist-pkg/render/title-scene.js +0 -75
- package/dist-pkg/render/weather.d.ts +0 -96
- package/dist-pkg/render/weather.js +0 -171
- package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
- package/dist-pkg/render/wheelchair-sprites.js +0 -69
- package/dist-pkg/render/world-tex.d.ts +0 -18
- package/dist-pkg/render/world-tex.js +0 -87
- package/dist-pkg/ui/activities-menu.d.ts +0 -258
- package/dist-pkg/ui/activities-menu.js +0 -648
- package/dist-pkg/ui/caa-sets.d.ts +0 -39
- package/dist-pkg/ui/caa-sets.js +0 -65
- package/dist-pkg/ui/latch-refusal.d.ts +0 -32
- package/dist-pkg/ui/latch-refusal.js +0 -60
- package/dist-pkg/ui/map-hub.d.ts +0 -56
- package/dist-pkg/ui/map-hub.js +0 -138
- package/dist-pkg/ui/settings-caa.d.ts +0 -45
- package/dist-pkg/ui/settings-caa.js +0 -137
- package/dist-pkg/ui/settings-motor.d.ts +0 -175
- package/dist-pkg/ui/settings-motor.js +0 -342
- package/dist-pkg/ui/webcam.d.ts +0 -7
- package/dist-pkg/ui/webcam.js +0 -93
package/dist-pkg/ui/dom.js
CHANGED
|
@@ -1,38 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// ui/dom.ts — atalhos de seleção do DOM, usados em toda a UI (menus/HUD/pausa/quiz/opções). PURO no import:
|
|
3
|
-
// só define as funções (não toca no DOM → importável em node). $ = querySelector; $$ = querySelectorAll
|
|
4
|
-
// como Array. Genéricos: $<HTMLInputElement>('#x') já tipa o retorno. (Fase 2.27 / Tier 1)
|
|
5
|
-
//
|
|
6
|
-
// DEIXOU DE SER FOLHA no item 14, e de propósito: passou a importar `core/i18n` para que `toggleLabel` (o
|
|
7
|
-
// texto do botão de alternância) more ao lado do `toggleBtn` (a classe e o `aria-pressed`). A propriedade que
|
|
8
|
-
// importa — importável em node, sem I/O no import — continua valendo; o que se perdeu foi a contagem de zero
|
|
9
|
-
// dependências, e o que se ganhou está escrito no comentário de `toggleLabel`.
|
|
10
|
-
import { t } from '../core/i18n.js';
|
|
11
|
-
// O PADRAO E `HTMLElement`, NAO `Element`, e a diferenca aparece em dezessete pontos de chamada: `.hidden`,
|
|
12
|
-
// `.focus()` e `.value` nao existem em `Element`, e sao exatamente o que uma aplicacao faz com o que
|
|
13
|
-
// seleciona. `Element` e o padrao do lib.dom porque `querySelector` tambem serve para SVG e MathML —
|
|
14
|
-
// nenhum seletor deste projeto pega um desses (conferido). Quem precisar de um tipo mais estreito passa
|
|
15
|
-
// o parametro: `$<HTMLSelectElement>('#pad-design')`.
|
|
16
|
-
/**
|
|
17
|
-
* ⚠️ RESOLVIDO POR `globalThis` E NÃO PELO GLOBAL CRU, e a diferença é entre devolver `null` e LANÇAR.
|
|
18
|
-
*
|
|
19
|
-
* `document.querySelector(...)` com `document` inexistente dá `ReferenceError` — não `undefined` —, e a
|
|
20
|
-
* assinatura destas duas funções promete `T | null`. Uma consulta que lança onde promete `null` é um defeito
|
|
21
|
-
* pela própria assinatura, e ele viajava longe: medido em 2026-09-08, o `core/a11y-sr.srAlert` chama o `$`
|
|
22
|
-
* daqui, e o `createGame` chama o `srAlert` ao mostrar o aviso de alcance — logo bootar a engine contra um
|
|
23
|
-
* documento INJECTADO (um iframe, um editor ao lado do jogo, um teste) rebentava o boot inteiro num anúncio.
|
|
24
|
-
*
|
|
25
|
-
* 📌 É o ACHADO 15 do `boot/create-game` outra vez, e sobreviveu pela mesma razão: enquanto toda raiz era um
|
|
26
|
-
* `main.ts` num navegador, o global ERA o documento certo. `globalThis.document` é a mesma coisa onde ele
|
|
27
|
-
* existe, e é `undefined` — em vez de explosão — onde não existe.
|
|
28
|
-
*
|
|
29
|
-
* ⚠️ E ELAS CONTINUAM A OLHAR PARA O GLOBAL, de propósito: quem precisa de consultar OUTRO documento injecta
|
|
30
|
-
* o seu (`create-game` tem um `$` próprio ligado ao `doc` do hospedeiro, e o `ui/pause-icons` tem o
|
|
31
|
-
* `docDaMontagem`). O que este conserto muda não é ONDE se procura — é o que acontece quando não há onde.
|
|
32
|
-
*/
|
|
33
|
-
const docGlobal = () => globalThis.document;
|
|
34
|
-
export const $ = (s) => docGlobal()?.querySelector(s) ?? null;
|
|
35
|
-
export const $$ = (s) => [...(docGlobal()?.querySelectorAll(s) ?? [])];
|
|
36
2
|
/**
|
|
37
3
|
* Reflects an on/off state onto a toggle button: the visual class AND `aria-pressed`.
|
|
38
4
|
* Lives here because the two must never drift apart — a button that looks pressed but does not
|
|
@@ -45,21 +11,18 @@ export function toggleBtn(b, on) {
|
|
|
45
11
|
b.setAttribute('aria-pressed', String(on));
|
|
46
12
|
}
|
|
47
13
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* Mora aqui pelo mesmo motivo do `toggleBtn` logo acima — os dois são as duas metades do mesmo gesto, e a
|
|
51
|
-
* nota daquela função diz que elas "não podem se separar". Elas tinham se separado em TREZE cópias, em nove
|
|
52
|
-
* arquivos, e dois deles declaravam um helper que se chamava compartilhado e servia um chamador só
|
|
53
|
-
* (`settings-empathy.toggleLabel` e `settings-motor.onOffLabel`, ambos apagados agora).
|
|
14
|
+
* The TEXT of a toggle button: 'On' / 'Off' in English.
|
|
54
15
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
16
|
+
* It lives here for the same reason as `toggleBtn` just above — the two are halves of one gesture, and must not
|
|
17
|
+
* drift apart. A text copied into each panel is a text that translating the game has to find everywhere, and the
|
|
18
|
+
* one copy it misses leaves a button in the wrong language — with no error and no red test.
|
|
57
19
|
*/
|
|
58
|
-
export function toggleLabel(on) {
|
|
20
|
+
export function toggleLabel(t, on) {
|
|
59
21
|
return t(on ? 'ui.toggle.on' : 'ui.toggle.off');
|
|
60
22
|
}
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
23
|
+
/** The `aria-label` of the same toggle: '{alvo}: on' (`{alvo}` is the dictionary's interpolation key). Separate from
|
|
24
|
+
* the visible label because a screen reader needs the NAME of the target with it, and the screen does not (the
|
|
25
|
+
* name is already on the row beside the button). */
|
|
26
|
+
export function toggleAria(t, target, on) {
|
|
27
|
+
return t(on ? 'ui.toggle.ariaOn' : 'ui.toggle.ariaOff', { alvo: target });
|
|
65
28
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { type Scale } from './layout.js';
|
|
3
|
+
export interface DrawingProblemsCtx {
|
|
4
|
+
/** `#game-region`, read at EVERY call — see the note at the top of this file. */
|
|
5
|
+
readonly region: () => HTMLElement | null;
|
|
6
|
+
/** The accessibility bar, or `null` where the game declined it. Read at every call, for the same reason. */
|
|
7
|
+
readonly bar: () => Element | null;
|
|
8
|
+
/** The scale the engine forced, or `null` before the first layout has run. */
|
|
9
|
+
readonly scale: () => Scale | null;
|
|
10
|
+
/** `window.getComputedStyle`, absent in a host that has none — then a node is counted rather than guessed about. */
|
|
11
|
+
readonly computedStyle?: (el: Element) => CSSStyleDeclaration;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* ADR-0163 rule 4, second half: the text and the targets the CARTRIDGE draws under the floor, named — read when `problems` is.
|
|
15
|
+
* `null` when there is nothing to say, which is what a clean page answers.
|
|
16
|
+
*/
|
|
17
|
+
export declare function drawnBelowTheFloor(ctx: DrawingProblemsCtx): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The nodes that PAINT over the accessibility bar's rectangle (ADR-0148 §3).
|
|
20
|
+
*
|
|
21
|
+
* 📏 ONLY WHAT PAINTS, and it was measured in `dist/quiz.html`: the quiz was accused at every boot because `#quiz-app` — a
|
|
22
|
+
* transparent box the size of the region, whose top padding IS the bar's room — intersects the bar's rectangle and draws
|
|
23
|
+
* nothing there. A node counts when it has text of its own, is a control or a medium, or paints a background.
|
|
24
|
+
*/
|
|
25
|
+
export declare function barIntruderProblems(ctx: DrawingProblemsCtx): string[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// ui/drawing-problems — WHAT A CARTRIDGE DREW THAT THE CHILD PAYS FOR, named (ADR-0163 rule 4, ADR-0148 §3; issue #203).
|
|
3
|
+
//
|
|
4
|
+
// Two questions the engine asks about its own region, and both answer with the sentence a `problems` line needs: text and
|
|
5
|
+
// targets drawn UNDER the floor, and the nodes drawn OVER the accessibility bar. They are one subject — the engine measuring
|
|
6
|
+
// what somebody else drew — and they lived inside `boot/create-game`, which is why this module exists: 📏 that file reached
|
|
7
|
+
// 2185 lines and 330 decision nodes (ADR-0221), and the debt is paid by SUBJECT, never by slicing.
|
|
8
|
+
//
|
|
9
|
+
// ⚠️ THE READERS ARE FUNCTIONS AND NOT VALUES, and that is the whole care in this file. `#game-region`, the bar and the applied
|
|
10
|
+
// scale are all things the root REPLACES: a `mount()` swaps the cartridge and the bar is built after the root is. Taking them
|
|
11
|
+
// as values here would freeze the first answer and report, forever, about a region that is no longer on the page — silently,
|
|
12
|
+
// because a stale element still answers `getBoundingClientRect()`.
|
|
13
|
+
import { barIntruders, belowFloor, minimumTarget } from './layout.js';
|
|
14
|
+
/** The engine's OWN nodes inside the region — their sizes are held by their own gates — and what is not drawn for the eye. */
|
|
15
|
+
const ENGINE_NODES = '#title-icons, .screen-pause, .overlay, #touch-controls, .rodape-da-tela, .pausa-rapida, #incl-parou, .sr-only, .hud-faixa, .hud-row';
|
|
16
|
+
/** What a finger is meant to hit; anything else has no target floor to fail. */
|
|
17
|
+
const TOUCH_TARGET = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])';
|
|
18
|
+
/**
|
|
19
|
+
* A node named the way a developer finds it: `tag#id.firstClass`.
|
|
20
|
+
*
|
|
21
|
+
* 📌 It is NOT exported, and the `exports-without-consumer` gate is what said so: exporting it would publish a name only this
|
|
22
|
+
* file uses, and an export with no consumer is surface somebody has to maintain with nobody needing it (ADR-0170 §3).
|
|
23
|
+
*/
|
|
24
|
+
function nodeName(el) {
|
|
25
|
+
return el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '')
|
|
26
|
+
+ (el.className ? `.${String(el.className).trim().split(/\s+/)[0]}` : '');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ADR-0163 rule 4, second half: the text and the targets the CARTRIDGE draws under the floor, named — read when `problems` is.
|
|
30
|
+
* `null` when there is nothing to say, which is what a clean page answers.
|
|
31
|
+
*/
|
|
32
|
+
export function drawnBelowTheFloor(ctx) {
|
|
33
|
+
const region = ctx.region();
|
|
34
|
+
const scale = ctx.scale();
|
|
35
|
+
if (!scale || !region || typeof region.querySelectorAll !== 'function' || !ctx.computedStyle)
|
|
36
|
+
return null;
|
|
37
|
+
const nodes = [...region.querySelectorAll('*')].map((el) => {
|
|
38
|
+
const b = el.getBoundingClientRect();
|
|
39
|
+
const hasText = [...el.childNodes].some((c) => c.nodeType === 3 && (c.textContent ?? '').trim() !== '');
|
|
40
|
+
return {
|
|
41
|
+
name: nodeName(el),
|
|
42
|
+
daEngine: el.closest(ENGINE_NODES) !== null,
|
|
43
|
+
fontPx: hasText && b.width > 0 && b.height > 0 ? parseFloat(ctx.computedStyle(el).fontSize) : null,
|
|
44
|
+
target: el.matches(TOUCH_TARGET) ? { w: b.width, h: b.height } : null,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const { text: smallText, targets: smallTargets } = belowFloor(nodes, scale.k);
|
|
48
|
+
if (!smallText.length && !smallTargets.length)
|
|
49
|
+
return null;
|
|
50
|
+
const k = minimumTarget(scale.k) / 22;
|
|
51
|
+
const parts = [
|
|
52
|
+
smallText.length ? `text under ${8 * k} px (${smallText.slice(0, 4).join(', ')})` : '',
|
|
53
|
+
smallTargets.length ? `targets under ${22 * k} px (${smallTargets.slice(0, 4).join(', ')})` : '',
|
|
54
|
+
].filter(Boolean);
|
|
55
|
+
return `the cartridge draws ${parts.join(' and ')} in #game-region: a child with low vision or unsteady hands cannot `
|
|
56
|
+
+ 'read or hit them — text and targets start at 16 and 44 px at 640×360 and grow with the scale (ADR-0163): size them '
|
|
57
|
+
+ 'from `--ui-fs` and `--alvo-min`';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The nodes that PAINT over the accessibility bar's rectangle (ADR-0148 §3).
|
|
61
|
+
*
|
|
62
|
+
* 📏 ONLY WHAT PAINTS, and it was measured in `dist/quiz.html`: the quiz was accused at every boot because `#quiz-app` — a
|
|
63
|
+
* transparent box the size of the region, whose top padding IS the bar's room — intersects the bar's rectangle and draws
|
|
64
|
+
* nothing there. A node counts when it has text of its own, is a control or a medium, or paints a background.
|
|
65
|
+
*/
|
|
66
|
+
export function barIntruderProblems(ctx) {
|
|
67
|
+
const region = ctx.region();
|
|
68
|
+
const bar = ctx.bar();
|
|
69
|
+
if (!bar || !region || typeof bar.getBoundingClientRect !== 'function')
|
|
70
|
+
return [];
|
|
71
|
+
const boxOf = (el) => {
|
|
72
|
+
const b = el.getBoundingClientRect();
|
|
73
|
+
return { x: b.x, y: b.y, w: b.width, h: b.height };
|
|
74
|
+
};
|
|
75
|
+
const paints = (el) => {
|
|
76
|
+
if ([...el.childNodes].some((c) => c.nodeType === 3 && (c.textContent ?? '').trim() !== ''))
|
|
77
|
+
return true;
|
|
78
|
+
if (/^(img|canvas|svg|video|input|button|select|textarea)$/i.test(el.tagName))
|
|
79
|
+
return true;
|
|
80
|
+
if (!ctx.computedStyle)
|
|
81
|
+
return true; // no way to tell: count it, as before
|
|
82
|
+
const cs = ctx.computedStyle(el);
|
|
83
|
+
return (cs.backgroundColor !== '' && cs.backgroundColor !== 'transparent' && cs.backgroundColor !== 'rgba(0, 0, 0, 0)')
|
|
84
|
+
|| (cs.backgroundImage !== '' && cs.backgroundImage !== 'none');
|
|
85
|
+
};
|
|
86
|
+
const nodes = [...region.querySelectorAll('*')].filter((el) => paints(el)).map((el) => ({
|
|
87
|
+
name: nodeName(el),
|
|
88
|
+
box: boxOf(el),
|
|
89
|
+
// the engine's HUD bands and HUD row are placed by the ENGINE: if one reaches the bar, that is the engine's defect and
|
|
90
|
+
// not the game's
|
|
91
|
+
isBar: el === bar || bar.contains(el) || el.closest('.hud-faixa, .hud-row') !== null,
|
|
92
|
+
}));
|
|
93
|
+
return barIntruders(boxOf(bar), nodes);
|
|
94
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { Translate } from '../core/i18n.js';
|
|
3
|
+
import type { VirtualController } from '../input/virtual-controller.js';
|
|
4
|
+
import { type FaceTrackerLoad, type VisionDeps } from '../platform/vision.js';
|
|
5
|
+
import { type VisionLoopDeps } from '../platform/vision-loop.js';
|
|
6
|
+
import type { SwitchableControl } from './switchable-control.js';
|
|
7
|
+
/** A camera picture the tracker can read. */
|
|
8
|
+
export interface CameraFeed {
|
|
9
|
+
readonly frame: unknown;
|
|
10
|
+
ready(): boolean;
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface EyeControlDeps {
|
|
14
|
+
/** Translates in the page's language — the root's translator (ADR-0232 D3). REQUIRED: text built from nowhere is a raw key. */
|
|
15
|
+
t: Translate;
|
|
16
|
+
readonly doc: Document;
|
|
17
|
+
/** Where the regions are drawn: `#game-region`. */
|
|
18
|
+
readonly region: HTMLElement;
|
|
19
|
+
/** The page's address, to reach `heavy/`. */
|
|
20
|
+
readonly base: string;
|
|
21
|
+
/** Whether a file is in the checked cache, handed to the tracker's loader (ADR-0232 D4: the root lends the cache). */
|
|
22
|
+
readonly hasFile: VisionDeps['hasFile'];
|
|
23
|
+
readonly loop: VisionLoopDeps;
|
|
24
|
+
/** Where the presses go: seat 0's virtual controller. */
|
|
25
|
+
readonly controller: VirtualController;
|
|
26
|
+
readonly say: (text: string) => void;
|
|
27
|
+
readonly alert: (text: string) => void;
|
|
28
|
+
/** A line for `problems`. */
|
|
29
|
+
readonly report: (line: string) => void;
|
|
30
|
+
/** Put the 📷 back to off: what cannot start must not show as on. */
|
|
31
|
+
readonly turnOff: () => void;
|
|
32
|
+
readonly loadTracker?: (deps: VisionDeps) => Promise<FaceTrackerLoad>;
|
|
33
|
+
readonly openFeed?: () => Promise<CameraFeed>;
|
|
34
|
+
}
|
|
35
|
+
export declare function videoFeed(doc: Document, media: MediaDevices): () => Promise<CameraFeed>;
|
|
36
|
+
export declare function createEyeControl(d: EyeControlDeps): SwitchableControl;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// ui/eye-control — PLAYING WITH THE EYES, PUT TOGETHER (ADR-0213; issues #194, #196).
|
|
3
|
+
//
|
|
4
|
+
// The pieces are each tested apart; this joins them for the root: the camera and the face tracker (platform/vision), a frame loop that
|
|
5
|
+
// cannot stop in silence (platform/vision-loop), the right eye's gaze read from its own rest (input/gaze-relative), the cycle that turns
|
|
6
|
+
// zones into actions (input/gaze-cycle), the presses on the virtual controller with the source `olhos` (input/virtual-controller, ADR-0111:
|
|
7
|
+
// never a disguised keyboard), and the regions drawn over the game
|
|
8
|
+
// (ui/gaze-overlay). The 📷 at its eyes position drives it (ADR-0215); any other position lets the camera go.
|
|
9
|
+
// · Nothing commands before the rest is measured: until then, and on a frame with no face or a head turning fast, the cycle is frozen.
|
|
10
|
+
// · A reading that re-centres itself cancels the gesture in hand.
|
|
11
|
+
// · Only the RIGHT eye is read (ADR-0213): the Dev's brain suppresses the left one, and a child's may too.
|
|
12
|
+
// · What cannot work is said to the child and written in `problems`, once: the files not on the device, a runtime that does not load,
|
|
13
|
+
// the camera refused, frames that stop or crawl (`ui/camera-control.startCameraReader` for the first three). A 📷 that is on and does nothing is the defect this module exists not to ship.
|
|
14
|
+
import { headPoseFromMatrix, scoresFromCategories, eyeGazeFromScores, bothEyesClosed } from '../input/face-signals.js';
|
|
15
|
+
import { createGazeReader, GAZE_DEFAULTS } from '../input/gaze-relative.js';
|
|
16
|
+
import { createGazeCycle } from '../input/gaze-cycle.js';
|
|
17
|
+
import { loadFaceTracker, openCamera, closeCamera } from '../platform/vision.js';
|
|
18
|
+
import { createVisionLoop } from '../platform/vision-loop.js';
|
|
19
|
+
import { drawGazeOverlay } from './gaze-overlay.js';
|
|
20
|
+
import { startCameraReader } from './camera-control.js';
|
|
21
|
+
// 📌 THE SHAPE IS THE FAMILY'S (ADR-0221 step 7f): what this module returns IS a SwitchableControl — following the 📷: on in
|
|
22
|
+
// the eyes position it opens the camera and the drawing, off it lets go of everything.
|
|
23
|
+
export function videoFeed(doc, media) {
|
|
24
|
+
return async () => {
|
|
25
|
+
const stream = await openCamera(media);
|
|
26
|
+
const video = doc.createElement('video');
|
|
27
|
+
video.muted = true;
|
|
28
|
+
video.playsInline = true;
|
|
29
|
+
video.srcObject = stream;
|
|
30
|
+
await video.play();
|
|
31
|
+
return { frame: video, ready: () => video.readyState >= 2, close: () => { video.pause(); video.srcObject = null; closeCamera(stream); } };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** What one detection says: the blendshapes (none without a face), the head's pose, and the RIGHT eye's gaze. */
|
|
35
|
+
function readDetection(det) {
|
|
36
|
+
const cats = det?.faceBlendshapes?.[0]?.categories;
|
|
37
|
+
const scores = cats ? scoresFromCategories(cats) : null;
|
|
38
|
+
return { scores, pose: headPoseFromMatrix(det?.facialTransformationMatrixes?.[0]?.data), eye: scores ? eyeGazeFromScores(scores).right : null };
|
|
39
|
+
}
|
|
40
|
+
/** What the cycle is told: frozen while nothing can be trusted — no rest yet, no face, a head turning fast — and cancelled when
|
|
41
|
+
* the reader re-centres under the gaze, because what was held was never a decision. */
|
|
42
|
+
function cycleFrameOf(reading, scores) {
|
|
43
|
+
return {
|
|
44
|
+
zone: reading.zone,
|
|
45
|
+
eyesClosed: !!scores && bothEyesClosed(scores),
|
|
46
|
+
frozen: !reading.ready || !scores || reading.reason === 'head-moving',
|
|
47
|
+
cancel: reading.reason === 'recentring',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The reading, written on the drawing's own element: when a child's eyes are not answered, this says at which step it stops. */
|
|
51
|
+
function writeDiagnosis(canvas, reading, out, face, delegate) {
|
|
52
|
+
const f2 = (n) => (n === undefined ? '' : n.toFixed(2));
|
|
53
|
+
Object.assign(canvas.dataset, {
|
|
54
|
+
face: String(face), ready: String(reading.ready), reason: reading.reason ?? '', zone: reading.zone ?? '',
|
|
55
|
+
tremorH: f2(reading.tremor?.h), tremorV: f2(reading.tremor?.v), dh: f2(reading.displacement?.h), dv: f2(reading.displacement?.v),
|
|
56
|
+
stillMs: String(Math.round(reading.stillMs ?? 0)), armed: String(out.armed), preparing: String(out.preparing), preview: out.preview?.item ?? '',
|
|
57
|
+
delegate,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** The regions over the game at the region's size, the middle counting down while the rest is measured, and the face's lines. */
|
|
61
|
+
function drawFrame(t, canvas, region, reading, out, det, lines) {
|
|
62
|
+
const w = canvas.width = region.clientWidth, h = canvas.height = region.clientHeight;
|
|
63
|
+
drawGazeOverlay(t, canvas.getContext('2d'), w, h, {
|
|
64
|
+
zone: reading.zone, armed: out.armed, preparing: out.preparing, preview: out.preview, restReady: reading.ready,
|
|
65
|
+
restLeftMs: reading.ready ? undefined : Math.max(0, GAZE_DEFAULTS.restMs - (reading.stillMs ?? 0)),
|
|
66
|
+
}, { face: det?.faceLandmarks?.[0] ? { landmarks: det.faceLandmarks[0], lines } : null });
|
|
67
|
+
}
|
|
68
|
+
export function createEyeControl(d) {
|
|
69
|
+
const { t } = d;
|
|
70
|
+
const loadTracker = d.loadTracker ?? loadFaceTracker;
|
|
71
|
+
const openFeed = d.openFeed ?? videoFeed(d.doc, d.doc.defaultView.navigator.mediaDevices);
|
|
72
|
+
const said = new Set();
|
|
73
|
+
const once = (kind, line, spoken) => {
|
|
74
|
+
if (!said.has(kind)) {
|
|
75
|
+
said.add(kind);
|
|
76
|
+
d.report(line);
|
|
77
|
+
}
|
|
78
|
+
d.alert(spoken);
|
|
79
|
+
};
|
|
80
|
+
let on = false, running = false, starting = null;
|
|
81
|
+
const current = () => on; // read through a call: `apply` awaits, and `on` can change meanwhile
|
|
82
|
+
let tracker = null, feed = null, canvas = null;
|
|
83
|
+
let pressed = null, wasReady = false;
|
|
84
|
+
let reader = createGazeReader(), cycle = createGazeCycle();
|
|
85
|
+
let loop = null;
|
|
86
|
+
const release = () => { if (pressed)
|
|
87
|
+
d.controller.release(pressed, 'olhos'); pressed = null; };
|
|
88
|
+
/** Only a change reaches the controller: the old action is let go, the new one pressed, from the eyes. */
|
|
89
|
+
const followPress = (next) => {
|
|
90
|
+
if (next === pressed)
|
|
91
|
+
return;
|
|
92
|
+
if (pressed)
|
|
93
|
+
d.controller.release(pressed, 'olhos');
|
|
94
|
+
if (next)
|
|
95
|
+
d.controller.press(next, 'olhos');
|
|
96
|
+
pressed = next;
|
|
97
|
+
};
|
|
98
|
+
const frame = (ms) => {
|
|
99
|
+
if (!tracker || !feed || !canvas || !feed.ready())
|
|
100
|
+
return;
|
|
101
|
+
const det = tracker.detect(feed.frame, ms);
|
|
102
|
+
const { scores, pose, eye } = readDetection(det);
|
|
103
|
+
const reading = reader(ms, { h: eye?.h, v: eye?.v, pose });
|
|
104
|
+
if (reading.ready && !wasReady)
|
|
105
|
+
d.say(t('sr.eyes.ready'));
|
|
106
|
+
wasReady = reading.ready;
|
|
107
|
+
const out = cycle(ms, cycleFrameOf(reading, scores));
|
|
108
|
+
followPress(out.pressed);
|
|
109
|
+
writeDiagnosis(canvas, reading, out, !!scores, tracker.delegate());
|
|
110
|
+
drawFrame(t, canvas, d.region, reading, out, det, tracker.eyeLines);
|
|
111
|
+
};
|
|
112
|
+
const health = (h, fps) => {
|
|
113
|
+
if (h === 'stalled')
|
|
114
|
+
once('stalled', 'eye control: a second without a camera frame — meanwhile the child\'s gaze commands nothing; keep the tab in front and the camera free', t('sr.eyes.stopped'));
|
|
115
|
+
if (h === 'slow')
|
|
116
|
+
once('slow', `eye control: ${fps} frames a second, under 8 — the reading misses movements and the child may not be answered; close other heavy tabs or use a lighter device`, t('sr.eyes.slow'));
|
|
117
|
+
};
|
|
118
|
+
const stop = () => {
|
|
119
|
+
loop?.stop();
|
|
120
|
+
loop = null;
|
|
121
|
+
release();
|
|
122
|
+
feed?.close();
|
|
123
|
+
feed = null;
|
|
124
|
+
tracker?.close();
|
|
125
|
+
tracker = null;
|
|
126
|
+
canvas?.remove();
|
|
127
|
+
canvas = null;
|
|
128
|
+
running = false;
|
|
129
|
+
wasReady = false;
|
|
130
|
+
};
|
|
131
|
+
/** Nothing started: said, written once, and the 📷 back to off — unless it already moved on, and then nobody is waiting (as the 👄). */
|
|
132
|
+
const failed = (kind, line, spoken) => {
|
|
133
|
+
if (!current())
|
|
134
|
+
return;
|
|
135
|
+
once(kind, line, spoken);
|
|
136
|
+
d.turnOff();
|
|
137
|
+
};
|
|
138
|
+
const start = async () => {
|
|
139
|
+
const opened = await startCameraReader({
|
|
140
|
+
subject: 'eye control', loses: 'play with the eyes',
|
|
141
|
+
spoken: { files: () => t('sr.eyes.needsInternet'), runtime: () => t('sr.eyes.failed'), camera: () => t('sr.eyes.noCamera') },
|
|
142
|
+
}, { load: () => loadTracker({ base: d.base, hasFile: d.hasFile }), openFeed, failed });
|
|
143
|
+
if (!opened)
|
|
144
|
+
return;
|
|
145
|
+
tracker = opened.tracker;
|
|
146
|
+
feed = opened.feed;
|
|
147
|
+
canvas = d.doc.createElement('canvas');
|
|
148
|
+
canvas.className = 'gaze-overlay';
|
|
149
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
150
|
+
Object.assign(canvas.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '5' });
|
|
151
|
+
d.region.appendChild(canvas);
|
|
152
|
+
reader = createGazeReader();
|
|
153
|
+
cycle = createGazeCycle();
|
|
154
|
+
pressed = null;
|
|
155
|
+
loop = createVisionLoop(d.loop, { onFrame: frame, onError: (e) => once('frame', `eye control: a frame failed (${String(e)}) — the loop goes on, but a repeated failure means the child is not being read`, t('sr.eyes.stopped')), onHealth: health });
|
|
156
|
+
running = true;
|
|
157
|
+
d.say(t('sr.eyes.lookMiddle'));
|
|
158
|
+
loop.start();
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
async apply(next) {
|
|
162
|
+
on = next;
|
|
163
|
+
if (starting)
|
|
164
|
+
await starting;
|
|
165
|
+
if (!on) {
|
|
166
|
+
if (running)
|
|
167
|
+
stop();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (running)
|
|
171
|
+
return;
|
|
172
|
+
starting = start().finally(() => { starting = null; });
|
|
173
|
+
await starting;
|
|
174
|
+
if (!current() && running)
|
|
175
|
+
stop(); // turned off while the camera was opening
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { Translate } from '../core/i18n.js';
|
|
3
|
+
import type { VirtualController } from '../input/virtual-controller.js';
|
|
4
|
+
import { type FaceTrackerLoad, type VisionDeps } from '../platform/vision.js';
|
|
5
|
+
import { type VisionLoopDeps } from '../platform/vision-loop.js';
|
|
6
|
+
import type { CameraFeed } from './eye-control.js';
|
|
7
|
+
import type { SwitchableControl } from './switchable-control.js';
|
|
8
|
+
export interface FaceControlDeps {
|
|
9
|
+
/** Translates in the page's language — the root's translator (ADR-0232 D3). REQUIRED: text built from nowhere is a raw key. */
|
|
10
|
+
t: Translate;
|
|
11
|
+
readonly doc: Document;
|
|
12
|
+
readonly region: HTMLElement;
|
|
13
|
+
readonly base: string;
|
|
14
|
+
/** Whether a file is in the checked cache, handed to the tracker's loader (ADR-0232 D4: the root lends the cache). */
|
|
15
|
+
readonly hasFile: VisionDeps['hasFile'];
|
|
16
|
+
readonly loop: VisionLoopDeps;
|
|
17
|
+
readonly controller: VirtualController;
|
|
18
|
+
readonly say: (text: string) => void;
|
|
19
|
+
readonly alert: (text: string) => void;
|
|
20
|
+
readonly report: (line: string) => void;
|
|
21
|
+
readonly turnOff: () => void;
|
|
22
|
+
readonly openFeed: () => Promise<CameraFeed>;
|
|
23
|
+
readonly loadTracker?: (deps: VisionDeps) => Promise<FaceTrackerLoad>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createFaceControl(d: FaceControlDeps): SwitchableControl;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// ui/face-control — PLAYING WITH THE FACE, PUT TOGETHER (ADR-0210 and its erratum, ADR-0212 §3; issue #191).
|
|
3
|
+
//
|
|
4
|
+
// The camera and the face tracker (platform/vision), the frame loop (platform/vision-loop), the Dev's face map (input/face-map), the
|
|
5
|
+
// presses on the virtual controller with the source `rosto` (input/virtual-controller, ADR-0111), and what is drawn over the game. The
|
|
6
|
+
// 📷 at its face position drives it (ADR-0215): it reads the face and draws the eyes, brows and lips — never the camera's picture
|
|
7
|
+
// (ADR-0212); any other position lets the camera go.
|
|
8
|
+
// · The rest measures itself (the Dev, 2026-09-16): the face still, looking at the middle, for 3 s in a row; until then the middle asks for
|
|
9
|
+
// it and nothing commands.
|
|
10
|
+
// · A frame with no face lets go of every held action: a face that left the camera is not holding anything.
|
|
11
|
+
// · What cannot start is said, written once in `problems`, and puts the 📷 back to off — the same promise as the eyes.
|
|
12
|
+
import { faceScoresFromCategories, headTurn, createFaceMapReader, createFaceRest } from '../input/face-map.js';
|
|
13
|
+
import { loadFaceTracker } from '../platform/vision.js';
|
|
14
|
+
import { createVisionLoop } from '../platform/vision-loop.js';
|
|
15
|
+
import { gazeFontPx } from './gaze-overlay.js';
|
|
16
|
+
import { startCameraReader } from './camera-control.js';
|
|
17
|
+
// 📌 THE SHAPE IS THE FAMILY'S (ADR-0221 step 7f): this module declares no `FaceControl` of its own — what it returns IS a
|
|
18
|
+
// `SwitchableControl`, and the compiler sees it.
|
|
19
|
+
const REST_MS = 3000;
|
|
20
|
+
/** The middle's request while the rest is measured, and the face lines, mirrored so they move the way the child moves. */
|
|
21
|
+
function draw(ctx, w, h, restLeftMs, lines, landmarks, say) {
|
|
22
|
+
ctx.clearRect(0, 0, w, h);
|
|
23
|
+
const f = gazeFontPx(w, h);
|
|
24
|
+
if (restLeftMs !== null) {
|
|
25
|
+
ctx.textAlign = 'center';
|
|
26
|
+
ctx.textBaseline = 'middle';
|
|
27
|
+
ctx.fillStyle = '#ffd23f';
|
|
28
|
+
ctx.shadowColor = '#000';
|
|
29
|
+
ctx.shadowBlur = 4;
|
|
30
|
+
ctx.font = `bold ${f}px system-ui`;
|
|
31
|
+
ctx.fillText(say('gaze.lookHere'), w / 2, h / 2 - f * 0.7);
|
|
32
|
+
ctx.font = `${f * 0.85}px system-ui`;
|
|
33
|
+
ctx.fillText(`${(restLeftMs / 1000).toFixed(1)} s`, w / 2, h / 2 + f * 0.8);
|
|
34
|
+
ctx.shadowBlur = 0;
|
|
35
|
+
}
|
|
36
|
+
if (lines && landmarks) {
|
|
37
|
+
ctx.strokeStyle = '#ffffff';
|
|
38
|
+
ctx.lineWidth = Math.max(2, f / 8);
|
|
39
|
+
ctx.shadowColor = '#000';
|
|
40
|
+
ctx.shadowBlur = 3;
|
|
41
|
+
for (const { start, end } of [...lines.eyes, ...lines.brows, ...lines.lips]) {
|
|
42
|
+
const a = landmarks[start], b = landmarks[end];
|
|
43
|
+
if (!a || !b)
|
|
44
|
+
continue;
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo((1 - a.x) * w, a.y * h);
|
|
47
|
+
ctx.lineTo((1 - b.x) * w, b.y * h);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
}
|
|
50
|
+
ctx.shadowBlur = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function createFaceControl(d) {
|
|
54
|
+
const { t } = d;
|
|
55
|
+
const loadTracker = d.loadTracker ?? loadFaceTracker;
|
|
56
|
+
const said = new Set();
|
|
57
|
+
const once = (kind, line, spoken) => { if (!said.has(kind)) {
|
|
58
|
+
said.add(kind);
|
|
59
|
+
d.report(line);
|
|
60
|
+
} d.alert(spoken); };
|
|
61
|
+
let on = false, running = false, starting = null;
|
|
62
|
+
const current = () => on;
|
|
63
|
+
let tracker = null, feed = null, canvas = null;
|
|
64
|
+
let loop = null;
|
|
65
|
+
let rest = null, measure = createFaceRest({ restMs: REST_MS }), read = null;
|
|
66
|
+
const held = new Set();
|
|
67
|
+
const releaseAll = () => { for (const a of held)
|
|
68
|
+
d.controller.release(a, 'rosto'); held.clear(); };
|
|
69
|
+
const frame = (ms) => {
|
|
70
|
+
if (!tracker || !feed || !canvas || !feed.ready())
|
|
71
|
+
return;
|
|
72
|
+
const det = tracker.detect(feed.frame, ms);
|
|
73
|
+
const cats = det?.faceBlendshapes?.[0]?.categories;
|
|
74
|
+
const landmarks = det?.faceLandmarks?.[0];
|
|
75
|
+
const w = canvas.width = d.region.clientWidth, h = canvas.height = d.region.clientHeight;
|
|
76
|
+
const g = canvas.getContext('2d');
|
|
77
|
+
if (!cats) {
|
|
78
|
+
releaseAll();
|
|
79
|
+
draw(g, w, h, rest ? null : REST_MS, null, undefined, t);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const scores = faceScoresFromCategories(cats);
|
|
83
|
+
const restLeft = measureRest(ms, scores, landmarks);
|
|
84
|
+
if (read)
|
|
85
|
+
holdExactly(new Set(read(ms, scores, landmarks).held)); // nothing commands before the rest: no reader yet
|
|
86
|
+
draw(g, w, h, restLeft, tracker.faceLines, landmarks, t);
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Until the rest is measured, measure it on this frame — the head counts too — and answer how long the middle is still asked
|
|
90
|
+
* for. Once it is measured: the reader is made, «ready» is said ONCE, and the answer is `null` from then on.
|
|
91
|
+
*/
|
|
92
|
+
const measureRest = (ms, scores, landmarks) => {
|
|
93
|
+
if (rest)
|
|
94
|
+
return null;
|
|
95
|
+
const m = measure(ms, scores, headTurn(landmarks).x);
|
|
96
|
+
if (!m.rest)
|
|
97
|
+
return Math.max(0, REST_MS - m.stillMs);
|
|
98
|
+
rest = m.rest;
|
|
99
|
+
read = createFaceMapReader({ rest });
|
|
100
|
+
d.say(t('sr.face.ready'));
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
/** Hold exactly what the reading holds now: let go of what left it, press what joined it — once. */
|
|
104
|
+
const holdExactly = (now) => {
|
|
105
|
+
for (const a of held)
|
|
106
|
+
if (!now.has(a)) {
|
|
107
|
+
d.controller.release(a, 'rosto');
|
|
108
|
+
held.delete(a);
|
|
109
|
+
}
|
|
110
|
+
for (const a of now)
|
|
111
|
+
if (!held.has(a)) {
|
|
112
|
+
d.controller.press(a, 'rosto');
|
|
113
|
+
held.add(a);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const health = (hh, fps) => {
|
|
117
|
+
if (hh === 'stalled')
|
|
118
|
+
once('stalled', 'face control: a second without a camera frame — meanwhile the child\'s face commands nothing; keep the tab in front and the camera free', t('sr.face.stopped'));
|
|
119
|
+
if (hh === 'slow')
|
|
120
|
+
once('slow', `face control: ${fps} frames a second, under 8 — expressions are missed and the child may not be answered; close other heavy tabs or use a lighter device`, t('sr.face.slow'));
|
|
121
|
+
};
|
|
122
|
+
const stop = () => {
|
|
123
|
+
loop?.stop();
|
|
124
|
+
loop = null;
|
|
125
|
+
releaseAll();
|
|
126
|
+
feed?.close();
|
|
127
|
+
feed = null;
|
|
128
|
+
tracker?.close();
|
|
129
|
+
tracker = null;
|
|
130
|
+
canvas?.remove();
|
|
131
|
+
canvas = null;
|
|
132
|
+
running = false;
|
|
133
|
+
rest = null;
|
|
134
|
+
read = null;
|
|
135
|
+
};
|
|
136
|
+
/** Nothing started: said, written once, and the 📷 back to off — unless it already moved on, and then nobody is waiting (as the 👄). */
|
|
137
|
+
const failed = (kind, line, spoken) => {
|
|
138
|
+
if (!current())
|
|
139
|
+
return;
|
|
140
|
+
once(kind, line, spoken);
|
|
141
|
+
d.turnOff();
|
|
142
|
+
};
|
|
143
|
+
const start = async () => {
|
|
144
|
+
const opened = await startCameraReader({
|
|
145
|
+
subject: 'face control', loses: 'play with the face',
|
|
146
|
+
spoken: { files: () => t('sr.face.needsInternet'), runtime: () => t('sr.face.failed'), camera: () => t('sr.face.noCamera') },
|
|
147
|
+
}, { load: () => loadTracker({ base: d.base, hasFile: d.hasFile }), openFeed: d.openFeed, failed });
|
|
148
|
+
if (!opened)
|
|
149
|
+
return;
|
|
150
|
+
tracker = opened.tracker;
|
|
151
|
+
feed = opened.feed;
|
|
152
|
+
canvas = d.doc.createElement('canvas');
|
|
153
|
+
canvas.className = 'face-overlay';
|
|
154
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
155
|
+
Object.assign(canvas.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '5' });
|
|
156
|
+
d.region.appendChild(canvas);
|
|
157
|
+
rest = null;
|
|
158
|
+
read = null;
|
|
159
|
+
measure = createFaceRest({ restMs: REST_MS });
|
|
160
|
+
loop = createVisionLoop(d.loop, { onFrame: frame, onError: (e) => once('frame', `face control: a frame failed (${String(e)}) — the loop goes on, but a repeated failure means the child is not being read`, t('sr.face.stopped')), onHealth: health });
|
|
161
|
+
running = true;
|
|
162
|
+
d.say(t('sr.face.lookMiddle'));
|
|
163
|
+
loop.start();
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
async apply(next) {
|
|
167
|
+
on = next;
|
|
168
|
+
if (starting)
|
|
169
|
+
await starting;
|
|
170
|
+
if (!on) {
|
|
171
|
+
if (running)
|
|
172
|
+
stop();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (running)
|
|
176
|
+
return;
|
|
177
|
+
starting = start().finally(() => { starting = null; });
|
|
178
|
+
await starting;
|
|
179
|
+
if (!current() && running)
|
|
180
|
+
stop();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|