Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New extension: Multi Touch #1432

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
394 changes: 394 additions & 0 deletions extensions/Skyhigh173/touch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,394 @@
// Name: Multi Touch
// ID: skyhigh173touch
// Description: Sense multiple fingers at once.
// By: Skyhigh173
// License: MIT

(function (Scratch) {
"use strict";

/**
* @typedef Finger
* @property {number} startingDate when first detected
* @property {number} dx change since previous frame
* @property {number} dy change since previous frame
*/

class MultiTouchExtension {
constructor() {
/**
* @type {HTMLElement}
*/
this.canvasDiv = null;
/**
* @type {HTMLCanvasElement}
*/
this.canvas = null;
/**
* @type {Array.<Touch>}
*/
this._touches = [];
/**
* @type {Array.<null|(Finger & Touch)>}
*/
this._fingers = [];
this._setup();

Scratch.vm.runtime.on("AFTER_EXECUTE", this._resetDeltas.bind(this));
}

_setup() {
this.canvas = Scratch.vm.runtime.renderer.canvas;
this.canvasDiv = this.canvas.parentElement;

/**
* update touch list
* @param {TouchEvent} e
*/
const update = (e) => {
this._touches = [...e.touches];

// update position
this._touches.forEach((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) {
newFinger.startingDate = performance.now();
newFinger.dx = 0;
newFinger.dy = 0;
this._fingers.push(newFinger);
} else {
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;
}
});

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[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);
}

_resetDeltas() {
for (const finger of this._fingers) {
finger.dx = 0;
finger.dy = 0;
}
}

_getCanvasBounds() {
return this.canvas.getBoundingClientRect();
}
_clamp(min, x, max) {
return Math.max(Math.min(x, max), min);
}
_map(x, sRmin, sRmax, tRmin, tRmax) {
return ((tRmax - tRmin) / (sRmax - sRmin)) * (x - sRmin) + tRmin;
}
_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;
return this._clamp(
min,
this._map(clientX, bounds.left, bounds.right, min, max),
max
);
}
_boundToScratchY(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
);
}

/** @type {Record<string, (t: Touch & Finger) => number>} */
_propMap = {
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._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() {
return {
id: "skyhigh173touch",
name: Scratch.translate("Multi Touch"),
color1: "#F76AB3",
blocks: [
{
opcode: "touchAvailable",
blockType: Scratch.BlockType.BOOLEAN,
text: Scratch.translate("is touch available?"),
},
{
opcode: "maxMultiTouch",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("maximum finger count"),
},
"---",
{
opcode: "numOfFingers",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("number of fingers"),
},
{
opcode: "numOfFingersID",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("number of fingers ID"),
},
{
opcode: "propOfFinger",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("[PROP] of finger [ID]"),
arguments: {
PROP: {
type: Scratch.ArgumentType.STRING,
menu: "prop",
},
ID: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: "1",
},
},
},
{
opcode: "fingerExists",
blockType: Scratch.BlockType.BOOLEAN,
text: Scratch.translate("finger [ID] exists?"),
arguments: {
ID: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: "1",
},
},
},
"---",
{
opcode: "touchingFinger",
blockType: Scratch.BlockType.BOOLEAN,
text: Scratch.translate("touching any finger?"),
disableMonitor: true,
filter: [Scratch.TargetType.SPRITE],
},
{
opcode: "touchingFingerCount",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("current touching finger count"),
disableMonitor: true,
filter: [Scratch.TargetType.SPRITE],
},
{
opcode: "touchingFingerID",
blockType: Scratch.BlockType.REPORTER,
text: Scratch.translate("current touching finger [X] ID"),
disableMonitor: true,
filter: [Scratch.TargetType.SPRITE],
arguments: {
X: {
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: [
{
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.",
}),
},
],
},
},
};
}

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 = Scratch.Cast.toString(PROP);
if (!Object.prototype.hasOwnProperty.call(this._propMap, PROP)) {
return "";
}

ID = Scratch.Cast.toNumber(ID) - 1;
if (!this._fingers[ID]) {
return "";
}

return this._propMap[PROP](this._fingers[ID]);
}

fingerExists({ ID }) {
ID = Scratch.Cast.toNumber(ID) - 1;
return !!this._fingers[ID];
}

_touchingFinger(finger, target) {
if (!finger) {
return false;
}

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((finger) =>
this._touchingFinger(finger, util.target)
);
}

touchingFingerCount(_, util) {
return this._fingers.reduce((total, finger) => {
return total + (this._touchingFinger(finger, util.target) ? 1 : 0);
}, 0);
}

touchingFingerID({ ID }, util) {
ID = Scratch.Cast.toNumber(ID);
const result =
this._fingers.findIndex((f) => {
return this._touchingFinger(f, util.target) && --ID <= 0;
}) + 1;
return result == 0 ? "" : result;
}
}

Scratch.extensions.register(new MultiTouchExtension());
})(Scratch);