From de0c66f417f549796e0456bd88c4b3f100ed356e Mon Sep 17 00:00:00 2001 From: Skyhigh173 <100467674+Skyhigh173@users.noreply.github.com> Date: Wed, 1 May 2024 18:34:56 +0800 Subject: [PATCH 01/12] Create touch.js --- extensions/Skyhigh173/touch.js | 207 +++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 extensions/Skyhigh173/touch.js diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js new file mode 100644 index 0000000000..6b73636e96 --- /dev/null +++ b/extensions/Skyhigh173/touch.js @@ -0,0 +1,207 @@ +// Name: Multi Touch +// ID: skyhigh173touch +// Description: Multiple fingers at once! +// By: Skyhigh173 +// License: MIT + +(function (Scratch) { + 'use strict'; + + class MultiTouchExtension { + constructor() { + /** + * @type {HTMLDivElement} + */ + this.canvasDiv = null; + /** + * @type {HTMLCanvasElement} + */ + this.canvas = null; + /** + * @type {Array.} + */ + this._touches = []; + /** + * @type {Array.} + */ + this._fingers = []; + this._setup(); + } + + get bound() { + return this.canvas.getBoundingClientRect(); + } + + _clamp(min, x, max) { + return Math.max(Math.min(x, max), min); + } + _scale(x, sRmin, sRmax, tRmin, tRmax) { + return (tRmax - tRmin) / (sRmax - sRmin) * (x - sRmin) + tRmin; + } + + _propMap = { + // map client coord to scratch coord + _x: (clientX) => this._clamp(-240, this._scale(clientX, this.bound.left, this.bound.right, -240, 240), 240), + _y: (clientY) => this._clamp(-180, this._scale(clientY, this.bound.bottom, this.bound.top, -180, 180), 180), + + x: (t) => this._propMap._x(t.clientX), + y: (t) => this._propMap._y(t.clientY), + dx: (t) => (this._propMap._x(t.clientX) - this._propMap._x(t.prevX)), + dy: (t) => (this._propMap._y(t.clientY) - this._propMap._y(t.prevY)), + sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), + sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), + duration: (t) => (Date.now() - t.date) / 1000, + force: (t) => t.force, + }; + + getInfo() { + return { + id: 'skyhigh173touch', + name: 'Multi Touch', + color1: '#F76AB3', + blocks: [ + { + opcode: 'touchAvailable', + blockType: Scratch.BlockType.BOOLEAN, + text: 'is touch available?' + }, + { + opcode: 'maxMultiTouch', + blockType: Scratch.BlockType.REPORTER, + text: 'maximum finger count' + }, + '---', + { + opcode: 'numOfFingers', + blockType: Scratch.BlockType.REPORTER, + text: 'number of fingers', + }, + { + opcode: 'numOfFingersID', + blockType: Scratch.BlockType.REPORTER, + text: 'number of fingers ID', + }, + { + opcode: 'propOfFinger', + blockType: Scratch.BlockType.REPORTER, + text: '[PROP] of finger [ID]', + arguments: { + PROP: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'x', + menu: 'prop', + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'fingerExists', + blockType: Scratch.BlockType.BOOLEAN, + text: 'finger [ID] exists?', + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1 + } + } + } + ], + menus: { + prop: { + acceptReporters: true, + /** + * x: x position + * y: y position + * dx: change of x position compared to previous frame + * dy: change of y position compared to previous frame + * sx: avg speed x of finger in a second + * sy: avg speed y of finger in a second + * duration: time since finger press + * force (some devices only): force of finger press + */ + items: ['x', 'y', 'dx', 'dy', 'sx', 'sy', 'duration', 'force'], + }, + }, + }; + } + + _setup() { + this.canvas = Scratch.vm.runtime.renderer.canvas; + this.canvasDiv = this.canvas.parentElement; + + // update touchList + /** + * @param {TouchEvent} e + */ + const upd = e => { + this._touches = [...e.touches]; + // update position + this._touches.forEach(t => { + // if theres a new finger... + const idx = this._fingers.findIndex(f => f?.identifier === t.identifier); + if (idx == -1) { + this._fingers.push(t); + // extra infos + this._fingers.at(-1).date = Date.now(); + this._fingers.at(-1).prevX = t.clientX; + this._fingers.at(-1).prevY = t.clientY; + this._fingers.at(-1).prevDate = Date.now(); + this._fingers.at(-1).nowDate = Date.now(); + } else { + const finger = this._fingers[idx]; + const date = finger.date, oldX = finger.clientX, oldY = finger.clientY, oldDate = finger.nowDate; + this._fingers[idx] = t; + this._fingers[idx].date = date; + this._fingers[idx].prevX = oldX; + this._fingers[idx].prevY = oldY; + this._fingers[idx].prevDate = oldDate; + this._fingers[idx].nowDate = Date.now(); + } + }) + this._fingers.forEach((t, index) => { + // if the finger releases... + if (this._touches.findIndex(f => f.identifier === t?.identifier) == -1) { + this._fingers[index] = null; + } + }) + // clear trailing null values + while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } + console.log(this._fingers) + } + this.canvasDiv.addEventListener('touchstart', e => upd(e)); + this.canvasDiv.addEventListener('touchmove', e => upd(e)); + this.canvasDiv.addEventListener('touchend', e => upd(e)); + } + + touchAvailable() { + return window.navigator.maxTouchPoints > 0; + } + maxMultiTouch() { + return window.navigator.maxTouchPoints; + } + + numOfFingers() { + return this._touches.length; + } + + numOfFingersID() { + return this._fingers.length; + } + + propOfFinger({ PROP, ID }) { + PROP = this._propMap[PROP]; + ID = Scratch.Cast.toNumber(ID) - 1; + if (ID >= this._fingers.length || this._fingers[ID] === null) return ''; + return PROP(this._fingers[ID]); + } + + fingerExists({ ID }) { + ID = Scratch.Cast.toNumber(ID) - 1; + return ID < this._fingers.length && this._fingers[ID] !== null; + } + } + Scratch.extensions.register(new MultiTouchExtension()); +})(Scratch); From 8bcce4cbfea22dc3291fdb96e930b416bc363859 Mon Sep 17 00:00:00 2001 From: Skyhigh173 <100467674+Skyhigh173@users.noreply.github.com> Date: Wed, 1 May 2024 19:26:26 +0800 Subject: [PATCH 02/12] remove debug log --- extensions/Skyhigh173/touch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 6b73636e96..3244e33cb9 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -169,7 +169,6 @@ }) // clear trailing null values while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } - console.log(this._fingers) } this.canvasDiv.addEventListener('touchstart', e => upd(e)); this.canvasDiv.addEventListener('touchmove', e => upd(e)); From a3e3d793052296548fef8a27d83ceb8cf23b892f Mon Sep 17 00:00:00 2001 From: Skyhigh173 <100467674+Skyhigh173@users.noreply.github.com> Date: Wed, 1 May 2024 21:38:48 +0800 Subject: [PATCH 03/12] new block & fix bug --- extensions/Skyhigh173/touch.js | 60 +++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 3244e33cb9..278b19c520 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -107,7 +107,35 @@ defaultValue: 1 } } - } + }, + '---', + { + opcode: 'touchingFinger', + blockType: Scratch.BlockType.BOOLEAN, + text: 'touching any finger?', + disableMonitor: true, + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'touchingFingerCount', + blockType: Scratch.BlockType.REPORTER, + text: 'current touching finger count', + disableMonitor: true, + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'touchingFingerID', + blockType: Scratch.BlockType.REPORTER, + text: 'current touching finger [X] ID', + disableMonitor: true, + filter: [Scratch.TargetType.SPRITE], + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, ], menus: { prop: { @@ -170,9 +198,11 @@ // clear trailing null values while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } } - this.canvasDiv.addEventListener('touchstart', e => upd(e)); - this.canvasDiv.addEventListener('touchmove', e => upd(e)); - this.canvasDiv.addEventListener('touchend', e => upd(e)); + + // do not use this.canvasDiv because event will lost after 'see inside' or change page + window.addEventListener('touchstart', upd); + window.addEventListener('touchmove', upd); + window.addEventListener('touchend', upd); } touchAvailable() { @@ -201,6 +231,28 @@ ID = Scratch.Cast.toNumber(ID) - 1; return ID < this._fingers.length && this._fingers[ID] !== null; } + + touchingCondition = (f, util) => f !== null && util.target.isTouchingPoint(this._propMap.x(f) + this.bound.width / 2, this.bound.height / 2 - this._propMap.y(f)); + + touchingFinger({}, util) { + return this._fingers.find((f) => { + return this.touchingCondition(f, util); + }) !== undefined; + } + + touchingFingerCount({}, util) { + return this._fingers.reduce((total, f) => { + return total + (this.touchingCondition(f, util) ? 1 : 0); + }, 0); + } + + touchingFingerID({ ID }, util) { + ID = Scratch.Cast.toNumber(ID); + const result = this._fingers.findIndex(f => { + return this.touchingCondition(f, util) && (--ID <= 0); + }) + 1; + return result == 0 ? '' : result; + } } Scratch.extensions.register(new MultiTouchExtension()); })(Scratch); From 79db0fbd5e66799c071456c96cfdd435013bfb47 Mon Sep 17 00:00:00 2001 From: Skyhigh173 <100467674+Skyhigh173@users.noreply.github.com> Date: Wed, 1 May 2024 22:36:24 +0800 Subject: [PATCH 04/12] Update touch.js --- extensions/Skyhigh173/touch.js | 113 +++++++++++++++++---------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 278b19c520..3752f3c6a9 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -28,6 +28,56 @@ this._setup(); } + _setup() { + this.canvas = Scratch.vm.runtime.renderer.canvas; + this.canvasDiv = this.canvas.parentElement; + + /** + * update touch list + * @param {TouchEvent} e + */ + const upd = e => { + this._touches = [...e.touches]; + // update position + this._touches.forEach(t => { + // if theres a new finger... + const idx = this._fingers.findIndex(f => f?.identifier === t.identifier); + if (idx == -1) { + this._fingers.push(t); + // extra infos + this._fingers.at(-1).date = Date.now(); + this._fingers.at(-1).prevX = t.clientX; + this._fingers.at(-1).prevY = t.clientY; + this._fingers.at(-1).prevDate = Date.now(); + this._fingers.at(-1).nowDate = Date.now(); + } else { + const finger = this._fingers[idx]; + const date = finger.date, oldX = finger.clientX, oldY = finger.clientY, oldDate = finger.nowDate; + this._fingers[idx] = t; + this._fingers[idx].date = date; + this._fingers[idx].prevX = oldX; + this._fingers[idx].prevY = oldY; + this._fingers[idx].prevDate = oldDate; + this._fingers[idx].nowDate = Date.now(); + } + }) + this._fingers.forEach((t, index) => { + // if the finger releases... + if (this._touches.findIndex(f => f.identifier === t?.identifier) == -1) { + this._fingers[index] = null; + } + }) + // clear trailing null values + while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } + } + + // do not use this.canvasDiv because event will lost after 'see inside' or change page + window.addEventListener('touchstart', upd); + window.addEventListener('touchmove', upd); + window.addEventListener('touchend', upd); + } + + get bound() { return this.canvas.getBoundingClientRect(); } @@ -156,55 +206,6 @@ }; } - _setup() { - this.canvas = Scratch.vm.runtime.renderer.canvas; - this.canvasDiv = this.canvas.parentElement; - - // update touchList - /** - * @param {TouchEvent} e - */ - const upd = e => { - this._touches = [...e.touches]; - // update position - this._touches.forEach(t => { - // if theres a new finger... - const idx = this._fingers.findIndex(f => f?.identifier === t.identifier); - if (idx == -1) { - this._fingers.push(t); - // extra infos - this._fingers.at(-1).date = Date.now(); - this._fingers.at(-1).prevX = t.clientX; - this._fingers.at(-1).prevY = t.clientY; - this._fingers.at(-1).prevDate = Date.now(); - this._fingers.at(-1).nowDate = Date.now(); - } else { - const finger = this._fingers[idx]; - const date = finger.date, oldX = finger.clientX, oldY = finger.clientY, oldDate = finger.nowDate; - this._fingers[idx] = t; - this._fingers[idx].date = date; - this._fingers[idx].prevX = oldX; - this._fingers[idx].prevY = oldY; - this._fingers[idx].prevDate = oldDate; - this._fingers[idx].nowDate = Date.now(); - } - }) - this._fingers.forEach((t, index) => { - // if the finger releases... - if (this._touches.findIndex(f => f.identifier === t?.identifier) == -1) { - this._fingers[index] = null; - } - }) - // clear trailing null values - while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } - } - - // do not use this.canvasDiv because event will lost after 'see inside' or change page - window.addEventListener('touchstart', upd); - window.addEventListener('touchmove', upd); - window.addEventListener('touchend', upd); - } - touchAvailable() { return window.navigator.maxTouchPoints > 0; } @@ -232,24 +233,26 @@ return ID < this._fingers.length && this._fingers[ID] !== null; } - touchingCondition = (f, util) => f !== null && util.target.isTouchingPoint(this._propMap.x(f) + this.bound.width / 2, this.bound.height / 2 - this._propMap.y(f)); + _touchingCondition(f, util) { + return f !== null && util.target.isTouchingPoint(this._propMap.x(f) + this.bound.width / 2, this.bound.height / 2 - this._propMap.y(f)); + } - touchingFinger({}, util) { + touchingFinger(_, util) { return this._fingers.find((f) => { - return this.touchingCondition(f, util); + return this._touchingCondition(f, util); }) !== undefined; } - touchingFingerCount({}, util) { + touchingFingerCount(_, util) { return this._fingers.reduce((total, f) => { - return total + (this.touchingCondition(f, util) ? 1 : 0); + return total + (this._touchingCondition(f, util) ? 1 : 0); }, 0); } touchingFingerID({ ID }, util) { ID = Scratch.Cast.toNumber(ID); const result = this._fingers.findIndex(f => { - return this.touchingCondition(f, util) && (--ID <= 0); + return this._touchingCondition(f, util) && (--ID <= 0); }) + 1; return result == 0 ? '' : result; } From c9bd9b7a3dc3bf86bab13e8f7bb0edc69556a54f Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 16:51:56 -0500 Subject: [PATCH 05/12] refactor - 1 --- extensions/Skyhigh173/touch.js | 74 +++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 3752f3c6a9..a39bf958d3 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -7,10 +7,19 @@ (function (Scratch) { 'use strict'; + /** + * @typedef Finger + * @property {number} date + * @property {number} prevX + * @property {number} prevY + * @property {number} prevDate + * @property {number} nowDate + */ + class MultiTouchExtension { constructor() { /** - * @type {HTMLDivElement} + * @type {HTMLElement} */ this.canvasDiv = null; /** @@ -22,7 +31,7 @@ */ this._touches = []; /** - * @type {Array.} + * @type {Array.} */ this._fingers = []; this._setup(); @@ -36,48 +45,55 @@ * update touch list * @param {TouchEvent} e */ - const upd = e => { + const update = e => { this._touches = [...e.touches]; + // update position - this._touches.forEach(t => { + this._touches.forEach(touch => { + const finger = /** @type {Touch & Finger} */ (touch); + // if theres a new finger... - const idx = this._fingers.findIndex(f => f?.identifier === t.identifier); - if (idx == -1) { - this._fingers.push(t); - // extra infos - this._fingers.at(-1).date = Date.now(); - this._fingers.at(-1).prevX = t.clientX; - this._fingers.at(-1).prevY = t.clientY; - this._fingers.at(-1).prevDate = Date.now(); - this._fingers.at(-1).nowDate = Date.now(); + const idx = this._fingers.findIndex(finger => finger?.identifier === touch.identifier); + if (idx === -1) { + finger.date = Date.now(); + finger.prevX = touch.clientX; + finger.prevY = touch.clientY; + finger.prevDate = Date.now(); + finger.nowDate = Date.now(); + this._fingers.push(finger); } else { - const finger = this._fingers[idx]; - const date = finger.date, oldX = finger.clientX, oldY = finger.clientY, oldDate = finger.nowDate; - this._fingers[idx] = t; - this._fingers[idx].date = date; - this._fingers[idx].prevX = oldX; - this._fingers[idx].prevY = oldY; - this._fingers[idx].prevDate = oldDate; - this._fingers[idx].nowDate = Date.now(); + const date = finger.date; + const oldX = finger.clientX; + const oldY = finger.clientY; + const oldDate = finger.nowDate; + finger.date = date; + finger.prevX = oldX; + finger.prevY = oldY; + finger.prevDate = oldDate; + finger.nowDate = Date.now(); + this._fingers[idx] = finger; } - }) + }); + this._fingers.forEach((t, index) => { // if the finger releases... - if (this._touches.findIndex(f => f.identifier === t?.identifier) == -1) { + if (this._touches.findIndex(f => f.identifier === t?.identifier) === -1) { this._fingers[index] = null; } - }) + }); + // clear trailing null values - while (this._fingers.length > 0 && this._fingers.at(-1) === null) { this._fingers.pop(); } + while (this._fingers.length > 0 && this._fingers[this._fingers.length - 1] === null) { + this._fingers.pop(); + } } // do not use this.canvasDiv because event will lost after 'see inside' or change page - window.addEventListener('touchstart', upd); - window.addEventListener('touchmove', upd); - window.addEventListener('touchend', upd); + window.addEventListener('touchstart', update); + window.addEventListener('touchmove', update); + window.addEventListener('touchend', update); } - get bound() { return this.canvas.getBoundingClientRect(); } From 1a9a9162f0a6abecef3865941b7e513283df3a86 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:00:23 -0500 Subject: [PATCH 06/12] format and l10n --- extensions/Skyhigh173/touch.js | 220 +++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 65 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index a39bf958d3..04f6c96b5e 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -1,11 +1,11 @@ // Name: Multi Touch // ID: skyhigh173touch -// Description: Multiple fingers at once! +// Description: Sense multiple fingers at once. // By: Skyhigh173 // License: MIT (function (Scratch) { - 'use strict'; + "use strict"; /** * @typedef Finger @@ -40,20 +40,22 @@ _setup() { this.canvas = Scratch.vm.runtime.renderer.canvas; this.canvasDiv = this.canvas.parentElement; - + /** * update touch list - * @param {TouchEvent} e + * @param {TouchEvent} e */ - const update = e => { + const update = (e) => { this._touches = [...e.touches]; // update position - this._touches.forEach(touch => { + this._touches.forEach((touch) => { const finger = /** @type {Touch & Finger} */ (touch); // if theres a new finger... - const idx = this._fingers.findIndex(finger => finger?.identifier === touch.identifier); + const idx = this._fingers.findIndex( + (finger) => finger?.identifier === touch.identifier + ); if (idx === -1) { finger.date = Date.now(); finger.prevX = touch.clientX; @@ -77,21 +79,27 @@ this._fingers.forEach((t, index) => { // if the finger releases... - if (this._touches.findIndex(f => f.identifier === t?.identifier) === -1) { + if ( + this._touches.findIndex((f) => f.identifier === t?.identifier) === + -1 + ) { this._fingers[index] = null; } }); // clear trailing null values - while (this._fingers.length > 0 && this._fingers[this._fingers.length - 1] === null) { + while ( + this._fingers.length > 0 && + this._fingers[this._fingers.length - 1] === null + ) { this._fingers.pop(); } - } + }; // do not use this.canvasDiv because event will lost after 'see inside' or change page - window.addEventListener('touchstart', update); - window.addEventListener('touchmove', update); - window.addEventListener('touchend', update); + window.addEventListener("touchstart", update); + window.addEventListener("touchmove", update); + window.addEventListener("touchend", update); } get bound() { @@ -102,18 +110,28 @@ return Math.max(Math.min(x, max), min); } _scale(x, sRmin, sRmax, tRmin, tRmax) { - return (tRmax - tRmin) / (sRmax - sRmin) * (x - sRmin) + tRmin; + return ((tRmax - tRmin) / (sRmax - sRmin)) * (x - sRmin) + tRmin; } _propMap = { // map client coord to scratch coord - _x: (clientX) => this._clamp(-240, this._scale(clientX, this.bound.left, this.bound.right, -240, 240), 240), - _y: (clientY) => this._clamp(-180, this._scale(clientY, this.bound.bottom, this.bound.top, -180, 180), 180), + _x: (clientX) => + this._clamp( + -240, + this._scale(clientX, this.bound.left, this.bound.right, -240, 240), + 240 + ), + _y: (clientY) => + this._clamp( + -180, + this._scale(clientY, this.bound.bottom, this.bound.top, -180, 180), + 180 + ), x: (t) => this._propMap._x(t.clientX), y: (t) => this._propMap._y(t.clientY), - dx: (t) => (this._propMap._x(t.clientX) - this._propMap._x(t.prevX)), - dy: (t) => (this._propMap._y(t.clientY) - this._propMap._y(t.prevY)), + dx: (t) => this._propMap._x(t.clientX) - this._propMap._x(t.prevX), + dy: (t) => this._propMap._y(t.clientY) - this._propMap._y(t.prevY), sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), duration: (t) => (Date.now() - t.date) / 1000, @@ -122,85 +140,84 @@ getInfo() { return { - id: 'skyhigh173touch', - name: 'Multi Touch', - color1: '#F76AB3', + id: "skyhigh173touch", + name: Scratch.translate("Multi Touch"), + color1: "#F76AB3", blocks: [ { - opcode: 'touchAvailable', + opcode: "touchAvailable", blockType: Scratch.BlockType.BOOLEAN, - text: 'is touch available?' + text: Scratch.translate("is touch available?"), }, { - opcode: 'maxMultiTouch', + opcode: "maxMultiTouch", blockType: Scratch.BlockType.REPORTER, - text: 'maximum finger count' + text: Scratch.translate("maximum finger count"), }, - '---', + "---", { - opcode: 'numOfFingers', + opcode: "numOfFingers", blockType: Scratch.BlockType.REPORTER, - text: 'number of fingers', + text: Scratch.translate("number of fingers"), }, { - opcode: 'numOfFingersID', + opcode: "numOfFingersID", blockType: Scratch.BlockType.REPORTER, - text: 'number of fingers ID', + text: Scratch.translate("number of fingers ID"), }, { - opcode: 'propOfFinger', + opcode: "propOfFinger", blockType: Scratch.BlockType.REPORTER, - text: '[PROP] of finger [ID]', + text: Scratch.translate("[PROP] of finger [ID]"), arguments: { PROP: { type: Scratch.ArgumentType.STRING, - defaultValue: 'x', - menu: 'prop', + menu: "prop", }, ID: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 1 - } - } + defaultValue: "1", + }, + }, }, { - opcode: 'fingerExists', + opcode: "fingerExists", blockType: Scratch.BlockType.BOOLEAN, - text: 'finger [ID] exists?', + text: Scratch.translate("finger [ID] exists?"), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 1 - } - } + defaultValue: "1", + }, + }, }, - '---', + "---", { - opcode: 'touchingFinger', + opcode: "touchingFinger", blockType: Scratch.BlockType.BOOLEAN, - text: 'touching any finger?', + text: Scratch.translate("touching any finger?"), disableMonitor: true, - filter: [Scratch.TargetType.SPRITE] + filter: [Scratch.TargetType.SPRITE], }, { - opcode: 'touchingFingerCount', + opcode: "touchingFingerCount", blockType: Scratch.BlockType.REPORTER, - text: 'current touching finger count', + text: Scratch.translate("current touching finger count"), disableMonitor: true, - filter: [Scratch.TargetType.SPRITE] + filter: [Scratch.TargetType.SPRITE], }, { - opcode: 'touchingFingerID', + opcode: "touchingFingerID", blockType: Scratch.BlockType.REPORTER, - text: 'current touching finger [X] ID', + text: Scratch.translate("current touching finger [X] ID"), disableMonitor: true, filter: [Scratch.TargetType.SPRITE], arguments: { X: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 1 - } - } + defaultValue: "1", + }, + }, }, ], menus: { @@ -216,7 +233,70 @@ * duration: time since finger press * force (some devices only): force of finger press */ - items: ['x', 'y', 'dx', 'dy', 'sx', 'sy', 'duration', 'force'], + items: [ + { + text: Scratch.translate({ + default: "x position", + description: + "Menu option to get how the x coordinate of a finger. ", + }), + value: "x", + }, + { + text: Scratch.translate({ + default: "y position", + description: + "Menu option to get how the y coordinate of a finger. ", + }), + value: "y", + }, + { + text: Scratch.translate({ + default: "x delta", + description: + "Menu option to get how much the X coordinate of a finger has moved.", + }), + value: "dx", + }, + { + text: Scratch.translate({ + default: "y delta", + description: + "Menu option to get how much the X coordinate of a finger has moved.", + }), + value: "dy", + }, + { + text: Scratch.translate({ + default: "x speed", + description: + "Menu option to get fast the X coordinate of a finger is changing.", + }), + value: "sx", + }, + { + text: Scratch.translate({ + default: "y speed", + description: + "Menu option to get fast the Y coordinate of a finger is changing.", + }), + value: "sy", + }, + { + text: Scratch.translate({ + default: "duration", + description: + "Menu option to get how long a finger has been pressed.", + }), + }, + { + text: Scratch.translate({ + default: "force", + description: + "Menu option to get how hard a finger is being pressed.", + }), + }, + ], }, }, }; @@ -240,7 +320,7 @@ propOfFinger({ PROP, ID }) { PROP = this._propMap[PROP]; ID = Scratch.Cast.toNumber(ID) - 1; - if (ID >= this._fingers.length || this._fingers[ID] === null) return ''; + if (ID >= this._fingers.length || this._fingers[ID] === null) return ""; return PROP(this._fingers[ID]); } @@ -250,28 +330,38 @@ } _touchingCondition(f, util) { - return f !== null && util.target.isTouchingPoint(this._propMap.x(f) + this.bound.width / 2, this.bound.height / 2 - this._propMap.y(f)); + return ( + f !== null && + util.target.isTouchingPoint( + this._propMap.x(f) + this.bound.width / 2, + this.bound.height / 2 - this._propMap.y(f) + ) + ); } touchingFinger(_, util) { - return this._fingers.find((f) => { - return this._touchingCondition(f, util); - }) !== undefined; + return ( + this._fingers.find((f) => { + return this._touchingCondition(f, util); + }) !== undefined + ); } touchingFingerCount(_, util) { return this._fingers.reduce((total, f) => { - return total + (this._touchingCondition(f, util) ? 1 : 0); + return total + (this._touchingCondition(f, util) ? 1 : 0); }, 0); } touchingFingerID({ ID }, util) { ID = Scratch.Cast.toNumber(ID); - const result = this._fingers.findIndex(f => { - return this._touchingCondition(f, util) && (--ID <= 0); - }) + 1; - return result == 0 ? '' : result; + const result = + this._fingers.findIndex((f) => { + return this._touchingCondition(f, util) && --ID <= 0; + }) + 1; + return result == 0 ? "" : result; } } + Scratch.extensions.register(new MultiTouchExtension()); })(Scratch); From 0af3ef42ae836b517e51754cb742f82266061af7 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:09:19 -0500 Subject: [PATCH 07/12] refactor scratch x conversion and fix custom stage size --- extensions/Skyhigh173/touch.js | 47 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 04f6c96b5e..30f1c701d4 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -102,36 +102,41 @@ window.addEventListener("touchend", update); } - get bound() { + _getCanvasBounds() { return this.canvas.getBoundingClientRect(); } - _clamp(min, x, max) { return Math.max(Math.min(x, max), min); } - _scale(x, sRmin, sRmax, tRmin, tRmax) { + _map(x, sRmin, sRmax, tRmin, tRmax) { return ((tRmax - tRmin) / (sRmax - sRmin)) * (x - sRmin) + tRmin; } + _toScratchX(clientX) { + const bounds = this._getCanvasBounds(); + const min = -Scratch.vm.runtime.stageWidth / 2; + const max = Scratch.vm.runtime.stageWidth / 2; + return this._clamp( + min, + this._map(clientX, bounds.left, bounds.right, min, max), + max + ); + } + _toScratchY(clientY) { + const bounds = this._getCanvasBounds(); + const min = -Scratch.vm.runtime.stageHeight / 2; + const max = Scratch.vm.runtime.stageHeight / 2; + return this._clamp( + min, + this._map(clientY, bounds.bottom, bounds.top, min, max), + max + ); + } _propMap = { - // map client coord to scratch coord - _x: (clientX) => - this._clamp( - -240, - this._scale(clientX, this.bound.left, this.bound.right, -240, 240), - 240 - ), - _y: (clientY) => - this._clamp( - -180, - this._scale(clientY, this.bound.bottom, this.bound.top, -180, 180), - 180 - ), - - x: (t) => this._propMap._x(t.clientX), - y: (t) => this._propMap._y(t.clientY), - dx: (t) => this._propMap._x(t.clientX) - this._propMap._x(t.prevX), - dy: (t) => this._propMap._y(t.clientY) - this._propMap._y(t.prevY), + x: (t) => this._toScratchX(t.clientX), + y: (t) => this._toScratchY(t.clientY), + dx: (t) => this._toScratchX(t.clientX) - this._toScratchX(t.prevX), + dy: (t) => this._toScratchY(t.clientY) - this._toScratchY(t.prevY), sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), duration: (t) => (Date.now() - t.date) / 1000, From eddb8c38356882fbc1a9491de85f925eac7975b1 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:09:48 -0500 Subject: [PATCH 08/12] use monotonic time --- extensions/Skyhigh173/touch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 30f1c701d4..0ff532c726 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -57,11 +57,11 @@ (finger) => finger?.identifier === touch.identifier ); if (idx === -1) { - finger.date = Date.now(); + finger.date = performance.now(); finger.prevX = touch.clientX; finger.prevY = touch.clientY; - finger.prevDate = Date.now(); - finger.nowDate = Date.now(); + finger.prevDate = performance.now(); + finger.nowDate = performance.now(); this._fingers.push(finger); } else { const date = finger.date; @@ -72,7 +72,7 @@ finger.prevX = oldX; finger.prevY = oldY; finger.prevDate = oldDate; - finger.nowDate = Date.now(); + finger.nowDate = performance.now(); this._fingers[idx] = finger; } }); @@ -139,7 +139,7 @@ dy: (t) => this._toScratchY(t.clientY) - this._toScratchY(t.prevY), sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), - duration: (t) => (Date.now() - t.date) / 1000, + duration: (t) => (performance.now() - t.date) / 1000, force: (t) => t.force, }; From 9064cb8dd4e47f7f22353e84c8ecb9bacc137700 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:25:57 -0500 Subject: [PATCH 09/12] accumulate deltas between ticks --- extensions/Skyhigh173/touch.js | 110 +++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 0ff532c726..7b84518466 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -9,11 +9,9 @@ /** * @typedef Finger - * @property {number} date - * @property {number} prevX - * @property {number} prevY - * @property {number} prevDate - * @property {number} nowDate + * @property {number} startingDate when first detected + * @property {number} dx change since previous frame + * @property {number} dy change since previous frame */ class MultiTouchExtension { @@ -35,6 +33,8 @@ */ this._fingers = []; this._setup(); + + Scratch.vm.runtime.on("AFTER_EXECUTE", this._resetDeltas.bind(this)); } _setup() { @@ -50,30 +50,24 @@ // update position this._touches.forEach((touch) => { - const finger = /** @type {Touch & Finger} */ (touch); + // finger may not actually be a Finger yet here + const newFinger = /** @type {Touch & Finger} */ (touch); // if theres a new finger... const idx = this._fingers.findIndex( (finger) => finger?.identifier === touch.identifier ); if (idx === -1) { - finger.date = performance.now(); - finger.prevX = touch.clientX; - finger.prevY = touch.clientY; - finger.prevDate = performance.now(); - finger.nowDate = performance.now(); - this._fingers.push(finger); + newFinger.startingDate = performance.now(); + newFinger.dx = 0; + newFinger.dy = 0; + this._fingers.push(newFinger); } else { - const date = finger.date; - const oldX = finger.clientX; - const oldY = finger.clientY; - const oldDate = finger.nowDate; - finger.date = date; - finger.prevX = oldX; - finger.prevY = oldY; - finger.prevDate = oldDate; - finger.nowDate = performance.now(); - this._fingers[idx] = finger; + const oldFinger = this._fingers[idx]; + newFinger.startingDate = oldFinger.startingDate; + newFinger.dx = oldFinger.dx + newFinger.clientX - oldFinger.clientX; + newFinger.dy = oldFinger.dy + newFinger.clientY - oldFinger.clientY; + this._fingers[idx] = newFinger; } }); @@ -102,6 +96,13 @@ window.addEventListener("touchend", update); } + _resetDeltas() { + for (const finger of this._fingers) { + finger.dx = 0; + finger.dy = 0; + } + } + _getCanvasBounds() { return this.canvas.getBoundingClientRect(); } @@ -111,7 +112,15 @@ _map(x, sRmin, sRmax, tRmin, tRmax) { return ((tRmax - tRmin) / (sRmax - sRmin)) * (x - sRmin) + tRmin; } - _toScratchX(clientX) { + _scaleToScratchX(clientX) { + const bounds = this._getCanvasBounds(); + return clientX * Scratch.vm.runtime.stageWidth / bounds.width; + } + _scaleToScratchY(clientY) { + const bounds = this._getCanvasBounds(); + return clientY * Scratch.vm.runtime.stageHeight / bounds.height; + } + _boundToScratchX(clientX) { const bounds = this._getCanvasBounds(); const min = -Scratch.vm.runtime.stageWidth / 2; const max = Scratch.vm.runtime.stageWidth / 2; @@ -121,7 +130,7 @@ max ); } - _toScratchY(clientY) { + _boundToScratchY(clientY) { const bounds = this._getCanvasBounds(); const min = -Scratch.vm.runtime.stageHeight / 2; const max = Scratch.vm.runtime.stageHeight / 2; @@ -132,11 +141,12 @@ ); } + /** @type {Record number>} */ _propMap = { - x: (t) => this._toScratchX(t.clientX), - y: (t) => this._toScratchY(t.clientY), - dx: (t) => this._toScratchX(t.clientX) - this._toScratchX(t.prevX), - dy: (t) => this._toScratchY(t.clientY) - this._toScratchY(t.prevY), + x: (t) => this._boundToScratchX(t.clientX), + y: (t) => this._boundToScratchY(t.clientY), + dx: (t) => this._scaleToScratchX(t.dx), + dy: (t) => this._scaleToScratchY(-t.dy), sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), duration: (t) => (performance.now() - t.date) / 1000, @@ -310,6 +320,7 @@ touchAvailable() { return window.navigator.maxTouchPoints > 0; } + maxMultiTouch() { return window.navigator.maxTouchPoints; } @@ -323,38 +334,45 @@ } propOfFinger({ PROP, ID }) { - PROP = this._propMap[PROP]; + PROP = Scratch.Cast.toString(PROP); + if (!Object.prototype.hasOwnProperty.call(this._propMap, PROP)) { + return ""; + } + ID = Scratch.Cast.toNumber(ID) - 1; - if (ID >= this._fingers.length || this._fingers[ID] === null) return ""; - return PROP(this._fingers[ID]); + if (!this._fingers[ID]) { + return ""; + } + + return this._propMap[PROP](this._fingers[ID]); } fingerExists({ ID }) { ID = Scratch.Cast.toNumber(ID) - 1; - return ID < this._fingers.length && this._fingers[ID] !== null; + return !!this._fingers[ID]; } - _touchingCondition(f, util) { - return ( - f !== null && - util.target.isTouchingPoint( - this._propMap.x(f) + this.bound.width / 2, - this.bound.height / 2 - this._propMap.y(f) - ) + _touchingFinger(finger, target) { + if (!finger) { + return; + } + + const bounds = this._getCanvasBounds(); + return target.isTouchingPoint( + this._propMap.x(finger) + bounds.width / 2, + bounds.height / 2 - this._propMap.y(finger) ); } touchingFinger(_, util) { - return ( - this._fingers.find((f) => { - return this._touchingCondition(f, util); - }) !== undefined + return !!this._fingers.find((finger) => + this._touchingFinger(finger, util.target) ); } touchingFingerCount(_, util) { - return this._fingers.reduce((total, f) => { - return total + (this._touchingCondition(f, util) ? 1 : 0); + return this._fingers.reduce((total, finger) => { + return total + (this._touchingFinger(finger, util.target) ? 1 : 0); }, 0); } @@ -362,7 +380,7 @@ ID = Scratch.Cast.toNumber(ID); const result = this._fingers.findIndex((f) => { - return this._touchingCondition(f, util) && --ID <= 0; + return this._touchingFinger(f, util.target) && --ID <= 0; }) + 1; return result == 0 ? "" : result; } From c2c263d847125f64c19d7b7c991e8ab5fcf3cc81 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:30:12 -0500 Subject: [PATCH 10/12] unbreak some stuff, and scale force to [0, 100] --- extensions/Skyhigh173/touch.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 7b84518466..d4dde53396 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -147,10 +147,10 @@ y: (t) => this._boundToScratchY(t.clientY), dx: (t) => this._scaleToScratchX(t.dx), dy: (t) => this._scaleToScratchY(-t.dy), - sx: (t) => this._propMap.dx(t) / ((t.nowDate - t.prevDate) / 1000), - sy: (t) => this._propMap.dy(t) / ((t.nowDate - t.prevDate) / 1000), - duration: (t) => (performance.now() - t.date) / 1000, - force: (t) => t.force, + sx: (t) => this._scaleToScratchX(t.dx) / (Scratch.vm.runtime.currentStepTime / 1000), + sy: (t) => this._scaleToScratchY(-t.dy) / (Scratch.vm.runtime.currentStepTime / 1000), + duration: (t) => (performance.now() - t.startingDate) / 1000, + force: (t) => t.force * 100, }; getInfo() { From d53b5449a5445cb39d1c4f36ec6d0feee134a278 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:30:44 -0500 Subject: [PATCH 11/12] npm run format --- extensions/Skyhigh173/touch.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index d4dde53396..3603fe7655 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -114,11 +114,11 @@ } _scaleToScratchX(clientX) { const bounds = this._getCanvasBounds(); - return clientX * Scratch.vm.runtime.stageWidth / bounds.width; + return (clientX * Scratch.vm.runtime.stageWidth) / bounds.width; } _scaleToScratchY(clientY) { const bounds = this._getCanvasBounds(); - return clientY * Scratch.vm.runtime.stageHeight / bounds.height; + return (clientY * Scratch.vm.runtime.stageHeight) / bounds.height; } _boundToScratchX(clientX) { const bounds = this._getCanvasBounds(); @@ -147,8 +147,12 @@ y: (t) => this._boundToScratchY(t.clientY), dx: (t) => this._scaleToScratchX(t.dx), dy: (t) => this._scaleToScratchY(-t.dy), - sx: (t) => this._scaleToScratchX(t.dx) / (Scratch.vm.runtime.currentStepTime / 1000), - sy: (t) => this._scaleToScratchY(-t.dy) / (Scratch.vm.runtime.currentStepTime / 1000), + sx: (t) => + this._scaleToScratchX(t.dx) / + (Scratch.vm.runtime.currentStepTime / 1000), + sy: (t) => + this._scaleToScratchY(-t.dy) / + (Scratch.vm.runtime.currentStepTime / 1000), duration: (t) => (performance.now() - t.startingDate) / 1000, force: (t) => t.force * 100, }; From 345be6c3f42e93b351fde9dcddb9645256c21169 Mon Sep 17 00:00:00 2001 From: Muffin Date: Thu, 9 May 2024 17:30:58 -0500 Subject: [PATCH 12/12] false --- extensions/Skyhigh173/touch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Skyhigh173/touch.js b/extensions/Skyhigh173/touch.js index 3603fe7655..d0beb913fc 100644 --- a/extensions/Skyhigh173/touch.js +++ b/extensions/Skyhigh173/touch.js @@ -358,7 +358,7 @@ _touchingFinger(finger, target) { if (!finger) { - return; + return false; } const bounds = this._getCanvasBounds();