diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index fb9d94c9b0..4e0c066d9a 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -7,16 +7,15 @@ (function (Scratch) { "use strict"; - //put these back here so I don't have to define scratch.cast again. - let notMobile = false; - - /* globals Accelerometer, Gyro */ - const SpeechRecognition = + // @ts-expect-error typeof webkitSpeechRecognition !== "undefined" - ? window.webkitSpeechRecognition - : typeof window.SpeechRecognition !== "undefined" - ? window.SpeechRecognition + ? // @ts-expect-error + window.webkitSpeechRecognition + : // @ts-expect-error + typeof window.SpeechRecognition !== "undefined" + ? // @ts-expect-error + window.SpeechRecognition : null; let recognizedSpeech = ""; @@ -64,135 +63,278 @@ }); }; - let initializedSensors = false; - const deviceVelocity = { - x: 0, - y: 0, - z: 0, + const physicalDeviceState = { + accelerationX: 0, + accelerationY: 0, + accelerationZ: 0, rotationX: 0, rotationY: 0, rotationZ: 0, }; - const deviceStatus = { - gyroscope: false, + const sensorStatus = { accelerometer: false, + gyroscope: false, }; - const initializeSensors = () => { - if (initializedSensors) { - return; - } - initializedSensors = true; + /** + * @returns {boolean} + */ + const sensorAccessRequiresPermission = () => + typeof DeviceMotionEvent === "function" && + // @ts-expect-error + typeof DeviceMotionEvent.requestPermission === "function"; - if (typeof Accelerometer !== "function") { - try { - const accelerometer = new Accelerometer({ - referenceFrame: "device", - }); - accelerometer.addEventListener("error", (e) => { - console.error("accelerometer error", e.error); - deviceStatus.accelerometer = false; - }); - accelerometer.addEventListener("reading", () => { - deviceVelocity.x = accelerometer.x; - deviceVelocity.y = accelerometer.y; - deviceVelocity.z = accelerometer.z; - deviceStatus.accelerometer = true; + /** + * Assumes you already checked sensorAccessRequiresPermission() === true. + * @returns {Promise<'granted'|'denied'|'unknown'>} Will never reject. + */ + const requestSensorPermission = () => { + // @ts-expect-error + return DeviceMotionEvent.requestPermission().catch((error) => { + // Usually this means we weren't in a user gesture. + console.error(error); + return "unknown"; + }); + }; + + /** + * Assumes you already checked sensorAccessRequiresPermission() === true. + * @returns {Promise} + */ + const askUserForSensorPermission = async () => { + // Safari automatically denies any request not made directly in a user gesture handler, + // so this request will almost certainly fail. We'll still try though, just in case. + let status = await requestSensorPermission(); + + if (status === "unknown") { + status = await new Promise((resolve) => { + const outer = document.createElement("div"); + outer.style.width = "100%"; + outer.style.height = "100%"; + outer.style.display = "flex"; + outer.style.alignItems = "center"; + outer.style.justifyContent = "center"; + outer.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + outer.style.backdropFilter = "blur(10px)"; + outer.style.pointerEvents = "auto"; + outer.tabIndex = 0; + + const inner = document.createElement("div"); + inner.textContent = Scratch.translate( + "Tap to allow access to accelerometer and gyroscope." + ); + inner.style.maxWidth = "360px"; + inner.style.color = "white"; + inner.style.textAlign = "center"; + outer.appendChild(inner); + + outer.addEventListener("click", () => { + resolve(requestSensorPermission()); + Scratch.renderer.removeOverlay(outer); }); - accelerometer.start(); - } catch (e) { - console.error("error setting up accelerometer", e); - } - } else { - console.warn("accelerometer API is not supported in this browser"); + + Scratch.renderer.addOverlay(outer, "scale"); + }); } - if (typeof Gyro !== "undefined") { - try { - const gyro = new Gyro({ - frequency: 30, - }); - gyro.addEventListener("error", (e) => { - console.error("gyro error", e.error); - deviceStatus.gyroscope = false; - }); - gyro.addEventListener("reading", () => { - deviceVelocity.rotationX = gyro.x; - deviceVelocity.rotationY = gyro.y; - deviceVelocity.rotationZ = gyro.z; - deviceStatus.gyroscope = true; - }); - } catch (e) { - console.error("error setting up gyro", e); - } - } else { - console.warn("gyro API is not supported in this browser"); + if (status === "denied") { + // Requesting permission again will be ignored no matter what. + // The flow for resetting this is awful, so let's at least tell the user how to do that. + alert( + Scratch.translate( + "To allow accelerometer and gyroscope access, open iOS settings > Apps > Safari > Advanced > Website Data > press Edit > Clear data for {domain}, then refresh this page.", + { + domain: location.hostname, + } + ) + ); } + + const granted = status === "granted"; + sensorStatus.accelerometer = granted; + sensorStatus.gyroscope = granted; }; + /** @type {null|Promise} */ + let initializingSensorsPromise = null; + /** @type {boolean} */ + let askedForSensorPermission = false; + + /** + * @template T + * @param {() => T} callback + * @returns {T|Promise} + */ + const whenSensorsInitialized = (callback) => { + if (!sensorAccessRequiresPermission() || askedForSensorPermission) { + return callback(); + } + + if (!initializingSensorsPromise) { + initializingSensorsPromise = askUserForSensorPermission().then(() => { + // Whether we got permission or not, asking again won't change the result. + askedForSensorPermission = true; + }); + } + + return initializingSensorsPromise.then(callback); + }; + + window.addEventListener("devicemotion", (event) => { + // On desktops, this event is fired with nulls. + if ( + event.accelerationIncludingGravity.x !== null && + event.accelerationIncludingGravity.y !== null && + event.accelerationIncludingGravity.z !== null + ) { + askedForSensorPermission = true; + sensorStatus.accelerometer = true; + physicalDeviceState.accelerationX = event.accelerationIncludingGravity.x; + physicalDeviceState.accelerationY = event.accelerationIncludingGravity.y; + physicalDeviceState.accelerationZ = event.accelerationIncludingGravity.z; + } + }); + + window.addEventListener("deviceorientation", (event) => { + // On desktops, this event is fired with nulls. + if (event.alpha !== null && event.beta !== null && event.gamma !== null) { + askedForSensorPermission = true; + sensorStatus.gyroscope = true; + physicalDeviceState.rotationX = event.beta; + physicalDeviceState.rotationY = event.gamma; + physicalDeviceState.rotationZ = event.alpha; + } + }); + const vm = Scratch.vm; const runtime = vm.runtime; const canvas = runtime.renderer.canvas; - let fingersDown = 0; - const lastFingerPositions = []; - const fingerPositions = []; + const maxTouchPoints = navigator.maxTouchPoints; + const supportsTouches = maxTouchPoints > 0; + + /** + * Maps system Touch identifiers to the identifers we expose to projects. + * This is necessary because Safari uses incremental IDs that only ever go up, + * so they get very big and won't start from 0. + * @type {Map} + */ + const nativeTouchIdToScratchId = new Map(); + + /** + * @typedef ScratchFinger + * @property {number} x + * @property {number} y + * @property {number} lastX + * @property {number} lastY + */ + + /** + * Maps Scratch touch ID to internal object. + * @type {Map} + */ + const scratchFingers = new Map(); + + /** + * @returns {number} A positive integer. + */ + const getUnusedScratchId = () => { + // This is slower than it could be but this doesn't run enough to matter. + // IDs start from 1, like Scratch lists. + let i = 1; + while (scratchFingers.has(i)) { + i++; + } + return i; + }; /** @param {TouchEvent} event */ - function handleTouchStart(event) { + const handleTouchStart = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; - const changedTouchesKeys = Object.keys(changedTouches); + const canvasPos = canvas.getBoundingClientRect(); - fingersDown = event.touches.length; - - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - fingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - }); - } + for (const touch of event.changedTouches) { + const nextAvailableScratchId = getUnusedScratchId(); + nativeTouchIdToScratchId.set(touch.identifier, nextAvailableScratchId); + + const x = touch.clientX - canvasPos.left; + const y = touch.clientY - canvasPos.top; + scratchFingers.set(nextAvailableScratchId, { + x: x, + y: y, + lastX: x, + lastY: y, + }); + } + }; /** @param {TouchEvent} event */ - function handleTouchMove(event) { + const handleTouchMove = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; + const canvasPos = canvas.getBoundingClientRect(); - const changedTouchesKeys = Object.keys(changedTouches); - fingersDown = event.touches.length; - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = [ - fingerPositions[changedTouches[touch].identifier][0], - fingerPositions[changedTouches[touch].identifier][1], - ]; - fingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - }); - } + for (const touch of event.changedTouches) { + const scratchId = nativeTouchIdToScratchId.get(touch.identifier); + const finger = scratchFingers.get(scratchId); + finger.lastX = finger.x; + finger.lastY = finger.y; + finger.x = touch.clientX - canvasPos.left; + finger.y = touch.clientY - canvasPos.top; + } + }; /** @param {TouchEvent} event */ - function handleTouchEnd(event) { + const handleTouchEnd = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; - const changedTouchesKeys = Object.keys(changedTouches); - fingersDown = event.touches.length; - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = null; - fingerPositions[changedTouches[touch].identifier] = null; - }); - } - canvas.addEventListener("touchstart", handleTouchStart, false); - canvas.addEventListener("touchmove", handleTouchMove, false); - canvas.addEventListener("touchcancel", handleTouchEnd, false); - canvas.addEventListener("touchend", handleTouchEnd, false); + for (const touch of event.changedTouches) { + const scratchId = nativeTouchIdToScratchId.get(touch.identifier); + scratchFingers.delete(scratchId); + nativeTouchIdToScratchId.delete(touch.identifier); + } + }; + + /** + * @param {VM.Target} target + * @returns {number} -1 if not touching, else the Scratch ID + */ + const findAnyTouchingFinger = (target) => { + for (const [scratchId, finger] of scratchFingers.entries()) { + const touching = target.isTouchingPoint(finger.x, finger.y); + if (touching) { + return scratchId; + } + } + return -1; + }; + + /** + * @param {VM.Target} target + * @param {number} scratchId + * @returns {boolean} + */ + const isTouchingSpecificFinger = (target, scratchId) => { + const finger = scratchFingers.get(scratchId); + return !!finger && target.isTouchingPoint(finger.x, finger.y); + }; + + canvas.addEventListener("touchstart", handleTouchStart, { + passive: false, + }); + canvas.addEventListener("touchmove", handleTouchMove, { + passive: false, + }); + canvas.addEventListener("touchcancel", handleTouchEnd, { + passive: false, + }); + canvas.addEventListener("touchend", handleTouchEnd, { + passive: false, + }); + + const fingersMenu = []; + for (let i = 0; i < Math.max(maxTouchPoints, 10); i++) { + fingersMenu.push((i + 1).toString()); + } /** * @param {string} listData @@ -239,44 +381,6 @@ const layerIco = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI0NS45NzY4OCIgaGVpZ2h0PSI0Ni40NzQ2NiIgdmlld0JveD0iMCwwLDQ1Ljk3Njg4LDQ2LjQ3NDY2Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjE1LjAzOTI5LC0xNTguOTgzODMpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxnIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTIzOS4xODkyOSwxNjUuNDc3MjNjMC4xNSwtMC4xIDAuNCwtMC4wNSAwLjQ1LDAuMTVsMS4zLDUuMzVjMCwwIDMuMiwyLjM1IDQuMTUsNGMxLjYsMi43NSAxLjY1LDUgMS42NSw1YzAsMCAzLjU1LDEuMDUgNC4xNSwzLjljMC42LDIuODUgLTEuNiw4LjI1IC0xMSwxMC4xYy05LjQsMS44NSAtMTYuOTUsLTAuNyAtMjAuNSwtNi40Yy0zLjU1LC01LjcgMi4wNSwtMTIuNSAxLjc1LC0xMi4xbC0xLjA1LC04Ljk1Yy0wLjA1LC0wLjIgMC4yLC0wLjM1IDAuNCwtMC4yNWw2LjA1LDMuOTVjMCwwIDIuMjUsLTAuODUgNC42LC0wLjk1YzEuNCwtMC4xIDIuNiwwIDMuNzUsMC4yeiIgZmlsbD0iIzNiYTJjZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjQ2LjU4OTI5LDE4MC4xNzcyM2MwLDAgMy40NSwwLjkgNC4wNSwzLjc1YzAuNiwyLjg1IC0xLjgsOCAtMTEuMSw5LjhjLTEyLjEsMi41IC0xNy44NSwtNC43IC0xNC41LC0xMGMzLjM1LC01LjM1IDkuMSwtMC44IDEzLjMsLTEuMWMzLjYsLTAuMjUgNCwtMy40IDguMjUsLTIuNDV6IiBmaWxsPSIjYTdlMmZiIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjU1Ljc4OTI5LDE4MC43MjcyM2MtMi4zNSwxLjkgLTUuOTUsMS45NSAtNS45NSwxLjk1IiBmaWxsPSJub25lIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjU1LjEzOTI5LDE4Ni4zMjcyM2MtMy4xNSwwLjI1IC01LjEsLTAuNyAtNS4xLC0wLjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzFiNTU2ZSIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMzguMTM5MjksMTgxLjMyNzIzYzEuMDUsMCAyLjE1LDAuMSAyLjIsMC40NWMwLjA1LDAuNyAtMC43LDIuMSAtMS41LDIuMTVjLTAuOSwwLjEgLTMsLTEuMTUgLTMsLTEuOTVjLTAuMDUsLTAuNiAxLjMsLTAuNjUgMi4zLC0wLjY1eiIgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIxNS42MzkyOSwxODAuNTc3MjNjMCwwIDQuMywxLjQgNi4wNSwyLjk1IiBmaWxsPSJub25lIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjIxLjgzOTI5LDE4NS4yNzcyM2MtMi4xNSwwLjg1IC01Ljg1LDAuMyAtNS44NSwwLjMiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzFiNTU2ZSIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxnIGZpbGw9IiMxYjU1NmUiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciI+PHBhdGggZD0iTTI0My44MzkyOSwxNzguMzI3MjNjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAgMC45LDAuNDUgMC45LDEiLz48L2c+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjMxLjMzOTI5LDE3OS43NzcyM2MwLDAuNTUgLTAuNCwxIC0wLjksMWMtMC41LDAgLTAuOSwtMC40NSAtMC45LC0xYzAsLTAuNTUgMC40LC0xIDAuOSwtMWMwLjUsMC4wNSAwLjksMC40NSAwLjksMSIvPjwvZz48L2c+PHBhdGggZD0iTTIxOC45ODM4MywyMDEuMDE2MTl2LTQyLjAzMjM1aDQyLjAzMjM1djQyLjAzMjM1eiIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48ZyBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yNDMuNDc1LDE3NS43NjI5NWMwLjE1LC0wLjEgMC40LC0wLjA1IDAuNDUsMC4xNWwxLjMsNS4zNWMwLDAgMy4yLDIuMzUgNC4xNSw0YzEuNiwyLjc1IDEuNjUsNSAxLjY1LDVjMCwwIDMuNTUsMS4wNSA0LjE1LDMuOWMwLjYsMi44NSAtMS42LDguMjUgLTExLDEwLjFjLTkuNCwxLjg1IC0xNi45NSwtMC43IC0yMC41LC02LjRjLTMuNTUsLTUuNyAyLjA1LC0xMi41IDEuNzUsLTEyLjFsLTEuMDUsLTguOTVjLTAuMDUsLTAuMiAwLjIsLTAuMzUgMC40LC0wLjI1bDYuMDUsMy45NWMwLDAgMi4yNSwtMC44NSA0LjYsLTAuOTVjMS40LC0wLjEgMi42LDAgMy43NSwwLjJ6IiBmaWxsPSIjM2JhMmNlIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjxwYXRoIGQ9Ik0yNTAuODc1LDE5MC40NjI5NWMwLDAgMy40NSwwLjkgNC4wNSwzLjc1YzAuNiwyLjg1IC0xLjgsOCAtMTEuMSw5LjhjLTEyLjEsMi41IC0xNy44NSwtNC43IC0xNC41LC0xMGMzLjM1LC01LjM1IDkuMSwtMC44IDEzLjMsLTEuMWMzLjYsLTAuMjUgNCwtMy40IDguMjUsLTIuNDV6IiBmaWxsPSIjYTdlMmZiIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjYwLjA3NSwxOTEuMDEyOTVjLTIuMzUsMS45IC01Ljk1LDEuOTUgLTUuOTUsMS45NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI1OS40MjUsMTk2LjYxMjk1Yy0zLjE1LDAuMjUgLTUuMSwtMC43IC01LjEsLTAuNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI0Mi40MjUsMTkxLjYxMjk1YzEuMDUsMCAyLjE1LDAuMSAyLjIsMC40NWMwLjA1LDAuNyAtMC43LDIuMSAtMS41LDIuMTVjLTAuOSwwLjEgLTMsLTEuMTUgLTMsLTEuOTVjLTAuMDUsLTAuNiAxLjMsLTAuNjUgMi4zLC0wLjY1eiIgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIxOS45MjUsMTkwLjg2Mjk1YzAsMCA0LjMsMS40IDYuMDUsMi45NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIyNi4xMjUsMTk1LjU2Mjk1Yy0yLjE1LDAuODUgLTUuODUsMC4zIC01Ljg1LDAuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjQ4LjEyNSwxODguNjEyOTVjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAgMC45LDAuNDUgMC45LDEiLz48L2c+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjM1LjYyNSwxOTAuMDYyOTVjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAuMDUgMC45LDAuNDUgMC45LDEiLz48L2c+PC9nPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjI0Ljk2MDcwOTI4NTcxNDMzOjIxLjAxNjE2NS0tPg=="; - const userAgent = navigator.userAgent; - let supportsTouches = true; - if ( - userAgent.includes("Safari") && - /^((?!chrome|android).)*safari/i.test(userAgent) - ) { - //* Its a problem with all safari browsers from what I see now which is odd since apple says its supported? - supportsTouches = false; - } else if ( - userAgent.includes("Windows") || - userAgent.includes("Mac OS") || - userAgent.includes("Linux") || - userAgent.includes("CrOS") - ) { - //* <-- Most chrome OS devices support touch events with up to 10 fingers but include a check to make it better anyways. - notMobile = true; - supportsTouches = navigator.maxTouchPoints > 0; - } - - const maxTouchPoints = navigator.maxTouchPoints; - - function makeArrayOfTouches() { - let TPArray = []; - if (maxTouchPoints == 0 || notMobile) { - //*For non touch compatible devices - for (let TP = 0; TP < 10; TP++) { - TPArray.push(Scratch.Cast.toString(TP + 1)); - } - } else { - for (let TP = 0; TP < maxTouchPoints; TP++) { - TPArray.push(Scratch.Cast.toString(TP + 1)); - } - } - return TPArray; - } - - const touchPointsArray = makeArrayOfTouches(); //* <-- Do this for devices that really can't support that many touches. - class SensingPlus { getInfo() { return { @@ -287,14 +391,6 @@ id: "obviousalexsensing", name: Scratch.translate("Sensing+"), blocks: [ - { - blockType: "label", - text: Scratch.translate("Touch blocks are broken in Safari."), - }, - { - blockType: "label", - text: Scratch.translate("We will try to fix them soon."), - }, { opcode: "supportsTouches", blockType: Scratch.BlockType.BOOLEAN, @@ -631,12 +727,12 @@ opcode: "getDeviceSpeed", blockIconURI: deviceVelIco, blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("[type] speed on the [axis] axis"), + text: Scratch.translate("[type] on the [axis] axis"), disableMonitor: true, arguments: { type: { type: Scratch.ArgumentType.STRING, - menu: "velocitymenu", + menu: "velocitymenu", // velocitymenu is poorly named }, axis: { type: Scratch.ArgumentType.STRING, @@ -649,7 +745,7 @@ menus: { fingerIDMenu: { acceptReporters: true, - items: touchPointsArray, + items: fingersMenu, }, deviceMenu: { acceptReporters: true, @@ -672,15 +768,16 @@ acceptReporters: true, items: ["x", "y", "z"], }, + // velocitymenu is poorly named. velocitymenu: { acceptReporters: true, items: [ { - text: Scratch.translate("positional"), + text: Scratch.translate("positional acceleration"), value: "positional", }, { - text: Scratch.translate("rotational"), + text: Scratch.translate("rotation rate"), value: "rotational", }, ], @@ -742,61 +839,6 @@ }; } - _touchUtil = { - blankExpression: () => {}, - isTouchingAnyFinger(util, success, fail) { - success = success || this.blankExpression; - if (success == null) { - success = this.blankExpression; - } - fail = fail || this.blankExpression; - if (fail == null) { - fail = this.blankExpression; - } - - for (let index = 0; index < fingerPositions.length; index++) { - const fingerPos = fingerPositions[index]; - if (fingerPos != null) { - const touching = util.target.isTouchingPoint( - fingerPos[0], - fingerPos[1] - ); - if (touching) { - success(index); - return true; - } - } - } - fail(-1); - return false; - }, - - isTouchingSpecificFinger(id, util, success, fail) { - success = success || this.blankExpression; - if (success == null) { - success = this.blankExpression; - } - fail = fail || this.blankExpression; - if (fail == null) { - fail = this.blankExpression; - } - - const fingerPos = fingerPositions[Scratch.Cast.toNumber(id) - 1]; - if (fingerPos != null) { - const touching = util.target.isTouchingPoint( - fingerPos[0], - fingerPos[1] - ); - if (touching) { - success(id); - return true; - } - } - fail(id); - return false; - }, - }; - supportsTouches() { return supportsTouches; } @@ -806,16 +848,16 @@ } getFingerSpeed({ ID }) { - const fingerPos = fingerPositions[ID - 1]; - const fingerLastPos = lastFingerPositions[ID - 1]; - if (!fingerPos || !fingerLastPos) { + const finger = scratchFingers.get(Scratch.Cast.toNumber(ID)); + if (!finger) { return 0; } const speed = Math.sqrt( - Math.pow(fingerPos[0] - fingerLastPos[0], 2) + - Math.pow(fingerPos[1] - fingerLastPos[1], 2) + Math.pow(finger.x - finger.lastX, 2) + + Math.pow(finger.y - finger.lastY, 2) ); - lastFingerPositions[ID - 1] = [fingerPos[0], fingerPos[1]]; + finger.lastX = finger.x; + finger.lastY = finger.y; return speed; } @@ -878,33 +920,36 @@ } hasDevice({ device }) { - if (deviceStatus[device]) { - return deviceStatus[device]; - } - return false; + return whenSensorsInitialized(() => { + if (Object.prototype.hasOwnProperty.call(sensorStatus, device)) { + return sensorStatus[device]; + } + return false; + }); } getDeviceSpeed({ type, axis }) { - initializeSensors(); - if (type === "positional") { - if (axis === "x") { - return deviceVelocity.x; - } else if (axis === "y") { - return deviceVelocity.y; - } else if (axis === "z") { - return deviceVelocity.z; - } - } else if (type === "rotational") { - if (axis === "x") { - return deviceVelocity.rotationX; - } else if (axis === "y") { - return deviceVelocity.rotationY; - } else if (axis === "z") { - return deviceVelocity.rotationZ; + return whenSensorsInitialized(() => { + if (type === "positional") { + if (axis === "x") { + return physicalDeviceState.accelerationX; + } else if (axis === "y") { + return physicalDeviceState.accelerationY; + } else if (axis === "z") { + return physicalDeviceState.accelerationZ; + } + } else if (type === "rotational") { + if (axis === "x") { + return physicalDeviceState.rotationX; + } else if (axis === "y") { + return physicalDeviceState.rotationY; + } else if (axis === "z") { + return physicalDeviceState.rotationZ; + } } - } - // should never happen - return 0; + // should never happen + return 0; + }); } getClipBoard() { @@ -926,7 +971,7 @@ } getFingersTouching() { - return fingersDown; + return scratchFingers.size; } getSprites() { @@ -979,49 +1024,40 @@ } touchingFinger(args, util) { - return this._touchUtil.isTouchingAnyFinger(util); + return findAnyTouchingFinger(util.target) !== -1; } touchingSpecificFinger({ ID }, util) { - return this._touchUtil.isTouchingSpecificFinger(ID, util); + return isTouchingSpecificFinger(util.target, Scratch.Cast.toNumber(ID)); } getTouchingFingerID(args, util) { - let TouchingFingerID = 0; - this._touchUtil.isTouchingAnyFinger(util, (FID) => { - TouchingFingerID = FID + 1; - }); - return TouchingFingerID; + const touching = findAnyTouchingFinger(util.target); + if (touching === -1) { + return 0; + } + return touching; } fingerPosition({ ID, PositionType }) { - const index = Scratch.Cast.toNumber(ID) - 1; - const fingerPos = fingerPositions[index]; - if (fingerPos) { - const positionIndex = PositionType === "x" ? 0 : 1; - const finger = [fingerPos[0], fingerPos[1]]; - let scratchCoords = finger; - const runtime = Scratch.vm.runtime; - + const finger = scratchFingers.get(Scratch.Cast.toNumber(ID)); + if (finger) { const canvasRect = canvas.getBoundingClientRect(); - if (PositionType === "x") { + if (Scratch.Cast.toString(PositionType) === "x") { const clientWidth = canvasRect.right - canvasRect.left; const toScratch = runtime.stageWidth / clientWidth; - scratchCoords[0] *= toScratch; - scratchCoords[0] -= runtime.stageWidth / 2; + return finger.x * toScratch - runtime.stageWidth / 2; } else { const clientheight = canvasRect.bottom - canvasRect.top; const toScratch = runtime.stageHeight / clientheight; - scratchCoords[1] *= toScratch; - scratchCoords[1] = runtime.stageHeight / 2 - scratchCoords[1]; + return runtime.stageHeight / 2 - finger.y * toScratch; } - return scratchCoords[positionIndex]; } return 0; } isFingerDown({ ID }) { - return !!fingerPositions[Scratch.Cast.toNumber(ID) - 1]; + return scratchFingers.has(Scratch.Cast.toNumber(ID)); } listInSprite({ index, List }) {