From 1184f7106bd909e9c99f01afad29f5a0f6d1beee Mon Sep 17 00:00:00 2001 From: Jez Swanson Date: Sun, 10 Dec 2023 22:08:35 -0800 Subject: [PATCH] Change how the accessibility fallback button works (#178) * Change how the accessibility fallback button works This appears to work better, successfully triggered it on desktop & Android. Next up I'll test it on iOS. * Remove unneeded font-size css * WIP code to detect accessibility clicks on iOS * Change the accessibility detecting to be better. * Clean up code. * Fix reference to renamed function --- static/scenes/railroad/index.html | 4 +- static/scenes/railroad/index.js | 2 +- static/scenes/railroad/index.scss | 13 +++-- static/scenes/railroad/js/game.js | 85 +++++++++++++++++++++++-------- 4 files changed, 76 insertions(+), 28 deletions(-) diff --git a/static/scenes/railroad/index.html b/static/scenes/railroad/index.html index fe5596f8d..857fc77cf 100644 --- a/static/scenes/railroad/index.html +++ b/static/scenes/railroad/index.html @@ -29,8 +29,8 @@
- - + +
diff --git a/static/scenes/railroad/index.js b/static/scenes/railroad/index.js index 341fee310..9f265976c 100644 --- a/static/scenes/railroad/index.js +++ b/static/scenes/railroad/index.js @@ -17,7 +17,7 @@ api.config({ }); function initialize() { - const contentElement = document.querySelector('.canvas-button'); + const contentElement = document.querySelector('#content'); const game = new Game(); api.addEventListener('pause', (ev) => game.pause()); diff --git a/static/scenes/railroad/index.scss b/static/scenes/railroad/index.scss index 151a79be3..bd8fd18e5 100644 --- a/static/scenes/railroad/index.scss +++ b/static/scenes/railroad/index.scss @@ -15,17 +15,20 @@ body { pointer-events: none; } -.canvas-button { +.throw-accessibility-button { + // Should only be triggered by keyboard or accessiblity tools. + pointer-events: none; + margin: 0; padding: 0; overflow: hidden; position: absolute; border: none; background: none; - left: 0; - right: 0; - top: 0; - bottom: 0; + left: 2%; + right: 2%; + top: 100px; + bottom: 2%; } .canvas-button:focus-visible { diff --git a/static/scenes/railroad/js/game.js b/static/scenes/railroad/js/game.js index d7e3bf98d..1ec9ef018 100644 --- a/static/scenes/railroad/js/game.js +++ b/static/scenes/railroad/js/game.js @@ -32,11 +32,9 @@ class Game { this.previousSeconds = Date.now() / 1000; - // Accessibility devices on Android fire off click events in the center of - // the Canvas element. We use these counters to guess if the user is using - // an accessibility tool. - this.numSuspectedAccessibilityClicks = 0; - this.numSuspectedNotAccessibilityClicks = 0; + // Previous clicks, used to guess if the user is using an accessibilty tool. + /** @type {Array} */ + this.previousClicks = [] } /** @@ -144,29 +142,21 @@ class Game { this.previousSeconds = nowSeconds; } - setUpListeners(containerButton) { + setUpListeners(container) { // Use `touchstart` for touch interfaces. - containerButton.addEventListener('touchstart', e => { + container.addEventListener('touchstart', e => { const touch = e.changedTouches[0]; this.handleClick(touch.clientX, touch.clientY); }); // Prevent the click event from firing on touch devices. - containerButton.addEventListener('touchend', e => { + container.addEventListener('touchend', e => { e.preventDefault(); }, {passive: false}) // Use `click` for mouse interfaces and for accessibility - containerButton.addEventListener('click', e => { - console.log(e); - if (isPossibleClickFromAccessibiltyTool(containerButton, e)) { - this.numSuspectedAccessibilityClicks++; - } - else { - this.numSuspectedNotAccessibilityClicks++; - } - - if (this.numSuspectedAccessibilityClicks > this.numSuspectedNotAccessibilityClicks) { + container.addEventListener('click', e => { + if (this.isSuspectedClickFromAccessibilityTool(container, e)) { this.level.throwToClosest(); } else { @@ -174,6 +164,12 @@ class Game { } }); + document.querySelector('.throw-accessibility-button').addEventListener('click', e => { + console.log('throw to closest from button'); + this.level.throwToClosest(); + e.stopPropagation(); + }) + window.addEventListener('resize', () => { this.renderer.setSize(window.innerWidth, window.innerHeight); if (this.level) { @@ -191,14 +187,63 @@ class Game { this.level.handleClick(clientX, clientY); } } + + /** + * @param {HTMLElement} element Element that handles click events + * @param {MouseEvent} e Click event + * @returns {boolean} Whether this click is likely from an accessibility tool (e.g. TalkBack). + */ + isSuspectedClickFromAccessibilityTool(element, e) { + // Keep track of the first few clicks. Only need the first few to detect + // accessibility tools. + if (this.previousClicks.length < 25) { + this.previousClicks.push(e); + } + + // Not enough clicks to compare them without having false positives + if (this.previousClicks.length < 3) { + // Use the click positions that a few accessibility tools use. This + // doesn't detect all tools so we have the fallback below. + return this.previousClicks + .every(click => isPossibleAccessibilityToolClickPosition(element, click)); + } + else { + // Return whether this click is in the same position as 90% of the previous clicks. + const numClicksPerPosition = new Map(); + for (const click of this.previousClicks) { + const clickStr = mouseEventToString(click); + if (!numClicksPerPosition.has(clickStr)) { + numClicksPerPosition.set(clickStr, 0); + } + const prevValue = numClicksPerPosition.get(clickStr); + numClicksPerPosition.set(clickStr, prevValue + 1) + } + + const percentOfClicksAtThisPosition = + numClicksPerPosition.get(mouseEventToString(e)) / this.previousClicks.length + return percentOfClicksAtThisPosition >= 0.9; + } + } +} + +/** + * @param {MouseEvent} e Mouse event + * @returns A string representation that works in a map. + */ +function mouseEventToString(e) { + return Math.round(e.clientX) + ":" + Math.round(e.clientY); } /** + * Checks if this click was in one of the specific places that some + * accessibility tools send their events. + * * @param {HTMLElement} element Element that handles click events * @param {MouseEvent} e Click event - * @returns {boolean} Whether this click could've been fired from an accessibility tool (e.g. TalkBack). + * @returns {boolean} Whether this click could've been fired from an + * accessibility tool (e.g. TalkBack). */ -function isPossibleClickFromAccessibiltyTool(element, e) { +function isPossibleAccessibilityToolClickPosition(element, e) { if (e.clientX === 0 && e.clientY === 0) { return true; }