diff --git a/src/app/TouchHandler.ts b/src/app/TouchHandler.ts deleted file mode 100644 index b9e69c6f..00000000 --- a/src/app/TouchHandler.ts +++ /dev/null @@ -1,350 +0,0 @@ -import MotionEvent from './MotionEvent'; -import ScreenInfo from './ScreenInfo'; -import { TouchControlMessage } from './controlMessage/TouchControlMessage'; -import Size from './Size'; -import Point from './Point'; -import Position from './Position'; -import TouchPointPNG from '../public/images/multitouch/touch_point.png'; -import CenterPointPNG from '../public/images/multitouch/center_point.png'; - -interface Touch { - action: number; - position: Position; - buttons: number; -} - -interface TouchOnClient { - client: { - width: number; - height: number; - }; - touch: Touch; -} - -interface CommonTouchAndMouse { - clientX: number; - clientY: number; - type: string; - target: EventTarget | null; - button: number; -} - -export default class TouchHandler { - private static readonly STROKE_STYLE: string = '#00BEA4'; - private static BUTTONS_MAP: Record = { - 0: 17, // ?? BUTTON_PRIMARY - 1: MotionEvent.BUTTON_TERTIARY, - 2: 26, // ?? BUTTON_SECONDARY - }; - private static EVENT_ACTION_MAP: Record = { - touchstart: MotionEvent.ACTION_DOWN, - touchend: MotionEvent.ACTION_UP, - touchmove: MotionEvent.ACTION_MOVE, - touchcancel: MotionEvent.ACTION_UP, - mousedown: MotionEvent.ACTION_DOWN, - mousemove: MotionEvent.ACTION_MOVE, - mouseup: MotionEvent.ACTION_UP, - }; - private static multiTouchActive = false; - private static multiTouchCenter?: Point; - private static multiTouchShift = false; - private static dirtyPlace: Point[] = []; - private static idToPointerMap: Map = new Map(); - private static pointerToIdMap: Map = new Map(); - private static touchPointRadius = 10; - private static centerPointRadius = 5; - private static touchPointImage?: HTMLImageElement; - private static centerPointImage?: HTMLImageElement; - private static pointImagesLoaded = false; - private static initialized = false; - - public static init(): void { - if (this.initialized) { - return; - } - this.loadImages(); - this.initialized = true; - } - - private static loadImages(): void { - const total = 2; - let current = 0; - - const onload = (e: Event) => { - if (++current === total) { - this.pointImagesLoaded = true; - } - if (e.target === this.touchPointImage) { - this.touchPointRadius = this.touchPointImage.width / 2; - } else if (e.target === this.centerPointImage) { - this.centerPointRadius = this.centerPointImage.width / 2; - } - }; - const touch = (this.touchPointImage = new Image()); - touch.src = TouchPointPNG; - touch.onload = onload; - const center = (this.centerPointImage = new Image()); - center.src = CenterPointPNG; - center.onload = onload; - } - - private static getPointerId(type: string, identifier: number): number { - if (this.idToPointerMap.has(identifier)) { - const pointerId = this.idToPointerMap.get(identifier) as number; - if (type === 'touchend' || type === 'touchcancel') { - this.idToPointerMap.delete(identifier); - this.pointerToIdMap.delete(pointerId); - } - return pointerId; - } - let pointerId = 0; - while (this.pointerToIdMap.has(pointerId)) { - pointerId++; - } - this.idToPointerMap.set(identifier, pointerId); - this.pointerToIdMap.set(pointerId, identifier); - return pointerId; - } - - private static calculateCoordinates(e: CommonTouchAndMouse, screenInfo: ScreenInfo): TouchOnClient | null { - const action = this.EVENT_ACTION_MAP[e.type]; - if (typeof action === 'undefined' || !screenInfo) { - return null; - } - const htmlTag = document.getElementsByTagName('html')[0] as HTMLElement; - const { width, height } = screenInfo.videoSize; - const target: HTMLElement = e.target as HTMLElement; - const { scrollTop, scrollLeft } = htmlTag; - let { clientWidth, clientHeight } = target; - let touchX = e.clientX - target.offsetLeft + scrollLeft; - let touchY = e.clientY - target.offsetTop + scrollTop; - const eps = 1e5; - const ratio = width / height; - const shouldBe = Math.round(eps * ratio); - const haveNow = Math.round((eps * clientWidth) / clientHeight); - if (shouldBe > haveNow) { - const realHeight = Math.ceil(clientWidth / ratio); - const top = (clientHeight - realHeight) / 2; - if (touchY < top || touchY > top + realHeight) { - return null; - } - touchY -= top; - clientHeight = realHeight; - } else if (shouldBe < haveNow) { - const realWidth = Math.ceil(clientHeight * ratio); - const left = (clientWidth - realWidth) / 2; - if (touchX < left || touchX > left + realWidth) { - return null; - } - touchX -= left; - clientWidth = realWidth; - } - const x = (touchX * width) / clientWidth; - const y = (touchY * height) / clientHeight; - const size = new Size(width, height); - const point = new Point(x, y); - const position = new Position(point, size); - const buttons = this.BUTTONS_MAP[e.button]; - return { - client: { - width: clientWidth, - height: clientHeight, - }, - touch: { - action, - position, - buttons, - }, - }; - } - - private static getTouch(e: MouseEvent, screenInfo: ScreenInfo): Touch[] | null { - const touchOnClient = this.calculateCoordinates(e, screenInfo); - if (!touchOnClient) { - return null; - } - const { client, touch } = touchOnClient; - const result: Touch[] = [touch]; - if (!e.ctrlKey) { - this.multiTouchActive = false; - this.multiTouchCenter = undefined; - this.multiTouchShift = false; - this.clearCanvas(e.target as HTMLCanvasElement); - return result; - } - const { position, action, buttons } = touch; - const { point, screenSize } = position; - const { width, height } = screenSize; - const { x, y } = point; - if (!this.multiTouchActive) { - if (e.shiftKey) { - this.multiTouchCenter = point; - this.multiTouchShift = true; - } else { - this.multiTouchCenter = new Point(client.width / 2, client.height / 2); - } - } - this.multiTouchActive = true; - let opposite: Point | undefined; - if (this.multiTouchShift && this.multiTouchCenter) { - const oppoX = 2 * this.multiTouchCenter.x - x; - const oppoY = 2 * this.multiTouchCenter.y - y; - if (oppoX <= width && oppoX >= 0 && oppoY <= height && oppoY >= 0) { - opposite = new Point(oppoX, oppoY); - } - } else { - opposite = new Point(client.width - x, client.height - y); - } - if (opposite) { - result.push({ - action, - buttons, - position: new Position(opposite, screenSize), - }); - } - return result; - } - - private static drawCircle(ctx: CanvasRenderingContext2D, point: Point, radius: number): void { - ctx.beginPath(); - ctx.arc(point.x, point.y, radius, 0, Math.PI * 2, true); - ctx.stroke(); - } - - public static drawLine(ctx: CanvasRenderingContext2D, point1: Point, point2: Point): void { - ctx.save(); - ctx.strokeStyle = this.STROKE_STYLE; - ctx.beginPath(); - ctx.moveTo(point1.x, point1.y); - ctx.lineTo(point2.x, point2.y); - ctx.stroke(); - ctx.restore(); - } - - private static drawPoint( - ctx: CanvasRenderingContext2D, - point: Point, - radius: number, - image?: HTMLImageElement, - ): void { - let { lineWidth } = ctx; - if (this.pointImagesLoaded && image) { - radius = image.width / 2; - lineWidth = 0; - ctx.drawImage(image, point.x - radius, point.y - radius); - } else { - this.drawCircle(ctx, point, radius); - } - - const topLeft = new Point(point.x - radius - lineWidth, point.y - radius - lineWidth); - const bottomRight = new Point(point.x + radius + lineWidth, point.y + radius + lineWidth); - this.updateDirty(topLeft, bottomRight); - } - - public static drawPointer(ctx: CanvasRenderingContext2D, point: Point): void { - this.drawPoint(ctx, point, this.touchPointRadius, this.touchPointImage); - } - - public static drawCenter(ctx: CanvasRenderingContext2D, point: Point): void { - this.drawPoint(ctx, point, this.centerPointRadius, this.centerPointImage); - } - - private static updateDirty(topLeft: Point, bottomRight: Point): void { - if (!this.dirtyPlace.length) { - this.dirtyPlace.push(topLeft, bottomRight); - return; - } - const currentTopLeft = this.dirtyPlace[0]; - const currentBottomRight = this.dirtyPlace[1]; - const newTopLeft = new Point(Math.min(currentTopLeft.x, topLeft.x), Math.min(currentTopLeft.y, topLeft.y)); - const newBottomRight = new Point( - Math.max(currentBottomRight.x, bottomRight.x), - Math.max(currentBottomRight.y, bottomRight.y), - ); - this.dirtyPlace.length = 0; - this.dirtyPlace.push(newTopLeft, newBottomRight); - } - - public static clearCanvas(target: HTMLCanvasElement): void { - const { clientWidth, clientHeight } = target; - const ctx = target.getContext('2d'); - if (ctx && this.dirtyPlace.length) { - const topLeft = this.dirtyPlace[0]; - const bottomRight = this.dirtyPlace[1]; - this.dirtyPlace.length = 0; - const x = Math.max(topLeft.x, 0); - const y = Math.max(topLeft.y, 0); - const w = Math.min(clientWidth, bottomRight.x - x); - const h = Math.min(clientHeight, bottomRight.y - y); - ctx.clearRect(x, y, w, h); - } - } - - public static formatTouchEvent( - e: TouchEvent, - screenInfo: ScreenInfo, - tag: HTMLElement, - ): TouchControlMessage[] | null { - const events: TouchControlMessage[] = []; - const touches = e.changedTouches; - if (touches && touches.length) { - for (let i = 0, l = touches.length; i < l; i++) { - const touch = touches[i]; - const pointerId = TouchHandler.getPointerId(e.type, touch.identifier); - if (touch.target !== tag) { - continue; - } - const item: CommonTouchAndMouse = { - clientX: touch.clientX, - clientY: touch.clientY, - type: e.type, - button: 0, - target: e.target, - }; - const event = this.calculateCoordinates(item, screenInfo); - if (event) { - const { action, buttons, position } = event.touch; - const pressure = touch.force * 255; - events.push(new TouchControlMessage(action, pointerId, position, pressure, buttons)); - } else { - console.error(`Failed to format touch`, touch); - } - } - } else { - console.error('No "touches"', e); - } - if (events.length) { - return events; - } - return null; - } - - public static buildTouchEvent(e: MouseEvent, screenInfo: ScreenInfo): TouchControlMessage[] | null { - const touches = this.getTouch(e, screenInfo); - if (!touches) { - return null; - } - const target = e.target as HTMLCanvasElement; - if (this.multiTouchActive) { - const ctx = target.getContext('2d'); - if (ctx) { - this.clearCanvas(target); - ctx.strokeStyle = TouchHandler.STROKE_STYLE; - touches.forEach((touch) => { - const { point } = touch.position; - this.drawPointer(ctx, point); - if (this.multiTouchCenter) { - this.drawLine(ctx, this.multiTouchCenter, point); - } - }); - if (this.multiTouchCenter) { - this.drawCenter(ctx, this.multiTouchCenter); - } - } - } - return touches.map((touch: Touch, pointerId: number) => { - const { action, buttons, position } = touch; - return new TouchControlMessage(action, pointerId, position, 255, buttons); - }); - } -} diff --git a/src/app/client/StreamClientQVHack.ts b/src/app/client/StreamClientQVHack.ts index 7d8ce274..fde7e768 100644 --- a/src/app/client/StreamClientQVHack.ts +++ b/src/app/client/StreamClientQVHack.ts @@ -7,18 +7,17 @@ import { WsQVHackClient } from './WsQVHackClient'; import Size from '../Size'; import ScreenInfo from '../ScreenInfo'; import { StreamReceiver } from './StreamReceiver'; -import TouchHandler from '../TouchHandler'; import Position from '../Position'; import { MsePlayerForQVHack } from '../player/MsePlayerForQVHack'; import { BasePlayer } from '../player/BasePlayer'; +import { SimpleTouchHandler, TouchHandlerListener } from '../touchHandler/SimpleTouchHandler'; const ACTION = 'stream-qvh'; const PORT = 8080; const WAIT_CLASS = 'wait'; -export class StreamClientQVHack extends BaseClient { +export class StreamClientQVHack extends BaseClient implements TouchHandlerListener { public static ACTION: QVHackStreamParams['action'] = ACTION; - private hasTouchListeners = false; private deviceName = ''; private managerClient = new WsQVHackClient(); private wdaConnection = new WdaConnection(); @@ -26,6 +25,7 @@ export class StreamClientQVHack extends BaseClient { private wdaUrl?: string; private readonly streamReceiver: StreamReceiver; private videoWrapper?: HTMLElement; + private touchHandler?: SimpleTouchHandler; constructor(params: QVHackStreamParams) { super(); @@ -146,80 +146,17 @@ export class StreamClientQVHack extends BaseClient { } private setTouchListeners(player: BasePlayer): void { - if (!this.hasTouchListeners) { - TouchHandler.init(); - let down = 0; - // const supportsPassive = Util.supportsPassive(); - let startPosition: Position | undefined; - let endPosition: Position | undefined; - const onMouseEvent = (e: MouseEvent) => { - let handled = false; - const tag = player.getTouchableElement(); - - if (e.target === tag) { - const screenInfo: ScreenInfo = player.getScreenInfo() as ScreenInfo; - if (!screenInfo) { - return; - } - handled = true; - const events = TouchHandler.buildTouchEvent(e, screenInfo); - if (down === 1 && events?.length === 1) { - if (e.type === 'mousedown') { - startPosition = events[0].position; - } else { - endPosition = events[0].position; - } - const target = e.target as HTMLCanvasElement; - const ctx = target.getContext('2d'); - if (ctx) { - if (startPosition) { - TouchHandler.drawPointer(ctx, startPosition.point); - } - if (endPosition) { - TouchHandler.drawPointer(ctx, endPosition.point); - if (startPosition) { - TouchHandler.drawLine(ctx, startPosition.point, endPosition.point); - } - } - } - if (e.type === 'mouseup') { - if (startPosition && endPosition) { - TouchHandler.clearCanvas(target); - if (startPosition.point.distance(endPosition.point) < 10) { - this.wdaConnection.wdaPerformClick(endPosition); - } else { - this.wdaConnection.wdaPerformScroll(startPosition, endPosition); - } - } - } - } - if (handled) { - if (e.cancelable) { - e.preventDefault(); - } - e.stopPropagation(); - } - } - if (e.type === 'mouseup') { - startPosition = undefined; - endPosition = undefined; - } - }; - document.body.addEventListener('click', (e: MouseEvent): void => { - onMouseEvent(e); - }); - document.body.addEventListener('mousedown', (e: MouseEvent): void => { - down++; - onMouseEvent(e); - }); - document.body.addEventListener('mouseup', (e: MouseEvent): void => { - onMouseEvent(e); - down--; - }); - document.body.addEventListener('mousemove', (e: MouseEvent): void => { - onMouseEvent(e); - }); - this.hasTouchListeners = true; + if (this.touchHandler) { + return; } + this.touchHandler = new SimpleTouchHandler(player, this); + } + + public performClick(position: Position): void { + this.wdaConnection.wdaPerformClick(position); + } + + public performScroll(from: Position, to: Position): void { + this.wdaConnection.wdaPerformScroll(from, to); } } diff --git a/src/app/client/StreamClientScrcpy.ts b/src/app/client/StreamClientScrcpy.ts index 686141d8..5b27b9ea 100644 --- a/src/app/client/StreamClientScrcpy.ts +++ b/src/app/client/StreamClientScrcpy.ts @@ -7,10 +7,7 @@ import Size from '../Size'; import { ControlMessage } from '../controlMessage/ControlMessage'; import { StreamReceiver } from './StreamReceiver'; import { CommandControlMessage } from '../controlMessage/CommandControlMessage'; -import TouchHandler from '../TouchHandler'; import Util from '../Util'; -import ScreenInfo from '../ScreenInfo'; -import { TouchControlMessage } from '../controlMessage/TouchControlMessage'; import FilePushHandler from '../FilePushHandler'; import DragAndPushLogger from '../DragAndPushLogger'; import { KeyEventListener, KeyInputHandler } from '../KeyInputHandler'; @@ -21,21 +18,23 @@ import { ConfigureScrcpy, ConfigureScrcpyOptions } from './ConfigureScrcpy'; import { DeviceTrackerDroid } from './DeviceTrackerDroid'; import { DeviceTrackerCommand } from '../../common/DeviceTrackerCommand'; import { html } from '../ui/HtmlTag'; +import { FeaturedTouchHandler, TouchHandlerListener } from '../touchHandler/FeaturedTouchHandler'; const ATTRIBUTE_UDID = 'data-udid'; const ATTRIBUTE_COMMAND = 'data-command'; +const TAG = '[StreamClientScrcpy]'; -export class StreamClientScrcpy extends BaseClient implements KeyEventListener { +export class StreamClientScrcpy extends BaseClient implements KeyEventListener, TouchHandlerListener { public static ACTION: ScrcpyStreamParams['action'] = 'stream'; private static players: Map = new Map(); private static configureDialog?: ConfigureScrcpy; - private hasTouchListeners = false; private controlButtons?: HTMLElement; private deviceName = ''; private clientId = -1; private clientsCount = -1; private requestedVideoSettings?: VideoSettings; + private touchHandler?: FeaturedTouchHandler; public static registerPlayer(playerClass: PlayerClass): void { if (playerClass.isSupported()) { @@ -108,7 +107,7 @@ export class StreamClientScrcpy extends BaseClient implements KeyEventLis deviceView.className = 'device-view'; const stop = (ev?: string | Event) => { if (ev && ev instanceof Event && ev.type === 'error') { - console.error(ev); + console.error(TAG, ev); } let parent; parent = deviceView.parentElement; @@ -207,13 +206,13 @@ export class StreamClientScrcpy extends BaseClient implements KeyEventLis } } if (!min.equals(videoSettings) || !playing) { - this.sendEvent(CommandControlMessage.createSetVideoSettingsCommand(min)); + this.sendMessage(CommandControlMessage.createSetVideoSettingsCommand(min)); } }); - console.log(player.getName(), udid); + console.log(TAG, player.getName(), udid); } - public sendEvent(e: ControlMessage): void { + public sendMessage(e: ControlMessage): void { this.streamReceiver.sendEvent(e); } @@ -230,12 +229,12 @@ export class StreamClientScrcpy extends BaseClient implements KeyEventLis } public onKeyEvent(event: KeyCodeControlMessage): void { - this.sendEvent(event); + this.sendMessage(event); } public sendNewVideoSetting(videoSettings: VideoSettings): void { this.requestedVideoSettings = videoSettings; - this.sendEvent(CommandControlMessage.createSetVideoSettingsCommand(videoSettings)); + this.sendMessage(CommandControlMessage.createSetVideoSettingsCommand(videoSettings)); } public getClientId(): number { @@ -257,79 +256,10 @@ export class StreamClientScrcpy extends BaseClient implements KeyEventLis } private setTouchListeners(player: BasePlayer): void { - if (!this.hasTouchListeners) { - TouchHandler.init(); - let down = 0; - const supportsPassive = Util.supportsPassive(); - const onMouseEvent = (e: MouseEvent | TouchEvent) => { - const tag = player.getTouchableElement(); - if (e.target === tag) { - const screenInfo: ScreenInfo = player.getScreenInfo() as ScreenInfo; - if (!screenInfo) { - return; - } - let events: TouchControlMessage[] | null = null; - let condition = true; - if (e instanceof MouseEvent) { - condition = down > 0; - events = TouchHandler.buildTouchEvent(e, screenInfo); - } else if (e instanceof TouchEvent) { - events = TouchHandler.formatTouchEvent(e, screenInfo, tag); - } - if (events && events.length && condition) { - events.forEach((event) => { - this.sendEvent(event); - }); - } - if (e.cancelable) { - e.preventDefault(); - } - e.stopPropagation(); - } - }; - - const options = supportsPassive ? { passive: false } : false; - document.body.addEventListener( - 'touchstart', - (e: TouchEvent): void => { - onMouseEvent(e); - }, - options, - ); - document.body.addEventListener( - 'touchend', - (e: TouchEvent): void => { - onMouseEvent(e); - }, - options, - ); - document.body.addEventListener( - 'touchmove', - (e: TouchEvent): void => { - onMouseEvent(e); - }, - options, - ); - document.body.addEventListener( - 'touchcancel', - (e: TouchEvent): void => { - onMouseEvent(e); - }, - options, - ); - document.body.addEventListener('mousedown', (e: MouseEvent): void => { - down++; - onMouseEvent(e); - }); - document.body.addEventListener('mouseup', (e: MouseEvent): void => { - onMouseEvent(e); - down--; - }); - document.body.addEventListener('mousemove', (e: MouseEvent): void => { - onMouseEvent(e); - }); - this.hasTouchListeners = true; + if (this.touchHandler) { + return; } + this.touchHandler = new FeaturedTouchHandler(player, this); } public static createEntryForDeviceList( diff --git a/src/app/controlMessage/ControlMessage.ts b/src/app/controlMessage/ControlMessage.ts index 1470fb42..fbd35211 100644 --- a/src/app/controlMessage/ControlMessage.ts +++ b/src/app/controlMessage/ControlMessage.ts @@ -5,7 +5,7 @@ export interface ControlMessageInterface { export class ControlMessage { public static TYPE_KEYCODE = 0; public static TYPE_TEXT = 1; - public static TYPE_MOUSE = 2; + public static TYPE_TOUCH = 2; public static TYPE_SCROLL = 3; public static TYPE_BACK_OR_SCREEN_ON = 4; public static TYPE_EXPAND_NOTIFICATION_PANEL = 5; diff --git a/src/app/controlMessage/MotionControlMessage.ts b/src/app/controlMessage/MotionControlMessage.ts deleted file mode 100644 index cde6649e..00000000 --- a/src/app/controlMessage/MotionControlMessage.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ControlMessage, ControlMessageInterface } from './ControlMessage'; -import Position, { PositionInterface } from '../Position'; - -export interface MotionControlMessageInterface extends ControlMessageInterface { - action: number; - buttons: number; - position: PositionInterface; -} - -export class MotionControlMessage extends ControlMessage { - public static PAYLOAD_LENGTH = 17; - - constructor(readonly action: number, readonly buttons: number, readonly position: Position) { - super(ControlMessage.TYPE_MOUSE); - } - - /** - * @override - */ - public toBuffer(): Buffer { - const buffer: Buffer = new Buffer(MotionControlMessage.PAYLOAD_LENGTH + 1); - buffer.writeUInt8(this.type, 0); - buffer.writeUInt8(this.action, 1); - buffer.writeUInt32BE(this.buttons, 2); - buffer.writeUInt32BE(this.position.point.x, 6); - buffer.writeUInt32BE(this.position.point.y, 10); - buffer.writeUInt16BE(this.position.screenSize.width, 14); - buffer.writeUInt16BE(this.position.screenSize.height, 16); - return buffer; - } - - public toString(): string { - return `MotionControlMessage{action=${this.action}, buttons=${this.buttons}, position=${this.position}}`; - } - - public toJSON(): MotionControlMessageInterface { - return { - type: this.type, - action: this.action, - buttons: this.buttons, - position: this.position.toJSON(), - }; - } -} diff --git a/src/app/controlMessage/TouchControlMessage.ts b/src/app/controlMessage/TouchControlMessage.ts index 1cb83d10..bf57060d 100644 --- a/src/app/controlMessage/TouchControlMessage.ts +++ b/src/app/controlMessage/TouchControlMessage.ts @@ -12,6 +12,22 @@ export interface TouchControlMessageInterface extends ControlMessageInterface { export class TouchControlMessage extends ControlMessage { public static PAYLOAD_LENGTH = 28; + /** + * - For a touch screen or touch pad, reports the approximate pressure + * applied to the surface by a finger or other tool. The value is + * normalized to a range from 0 (no pressure at all) to 1 (normal pressure), + * although values higher than 1 may be generated depending on the + * calibration of the input device. + * - For a trackball, the value is set to 1 if the trackball button is pressed + * or 0 otherwise. + * - For a mouse, the value is set to 1 if the primary mouse button is pressed + * or 0 otherwise. + * + * - scrcpy server expects signed short (2 bytes) for a pressure value + * - in browser TouchEvent has `force` property (values in 0..1 range), we + * use it as "pressure" for scrcpy + */ + public static readonly MAX_PRESSURE_VALUE = 0xffff; constructor( readonly action: number, @@ -20,7 +36,7 @@ export class TouchControlMessage extends ControlMessage { readonly pressure: number, readonly buttons: number, ) { - super(ControlMessage.TYPE_MOUSE); + super(ControlMessage.TYPE_TOUCH); } /** @@ -37,7 +53,7 @@ export class TouchControlMessage extends ControlMessage { offset = buffer.writeUInt32BE(this.position.point.y, offset); offset = buffer.writeUInt16BE(this.position.screenSize.width, offset); offset = buffer.writeUInt16BE(this.position.screenSize.height, offset); - offset = buffer.writeUInt16BE(this.pressure, offset); + offset = buffer.writeUInt16BE(this.pressure * TouchControlMessage.MAX_PRESSURE_VALUE, offset); buffer.writeUInt32BE(this.buttons, offset); return buffer; } diff --git a/src/app/player/BasePlayer.ts b/src/app/player/BasePlayer.ts index 3712172e..a60f5a35 100644 --- a/src/app/player/BasePlayer.ts +++ b/src/app/player/BasePlayer.ts @@ -214,7 +214,7 @@ export abstract class BasePlayer extends TypedEmitter { public abstract getPreferredVideoSetting(): VideoSettings; protected abstract calculateMomentumStats(): void; - public getTouchableElement(): HTMLElement { + public getTouchableElement(): HTMLCanvasElement { return this.touchableCanvas; } diff --git a/src/app/toolbox/DroidMoreBox.ts b/src/app/toolbox/DroidMoreBox.ts index f53b04c0..b894e3b9 100644 --- a/src/app/toolbox/DroidMoreBox.ts +++ b/src/app/toolbox/DroidMoreBox.ts @@ -35,7 +35,7 @@ export class DroidMoreBox { DroidMoreBox.wrap('p', [input, sendButton], moreBox); sendButton.onclick = () => { if (input.value) { - client.sendEvent(new TextControlMessage(input.value)); + client.sendMessage(new TextControlMessage(input.value)); } }; @@ -161,7 +161,7 @@ export class DroidMoreBox { event = new CommandControlMessage(action); } if (event) { - client.sendEvent(event); + client.sendMessage(event); } }; } diff --git a/src/app/toolbox/DroidToolBox.ts b/src/app/toolbox/DroidToolBox.ts index 370be3cb..0bcb1456 100644 --- a/src/app/toolbox/DroidToolBox.ts +++ b/src/app/toolbox/DroidToolBox.ts @@ -64,7 +64,7 @@ export class DroidToolBox extends ToolBox { const { code } = element.optional; const action = type === 'mousedown' ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; const event = new KeyCodeControlMessage(action, code, 0, 0); - client.sendEvent(event); + client.sendMessage(event); }; const elements: ToolBoxElement[] = list.map((item) => { const button = new ToolBoxButton(item.title, item.icon, { diff --git a/src/app/touchHandler/FeaturedTouchHandler.ts b/src/app/touchHandler/FeaturedTouchHandler.ts new file mode 100644 index 00000000..d2d0856d --- /dev/null +++ b/src/app/touchHandler/FeaturedTouchHandler.ts @@ -0,0 +1,103 @@ +import { KeyEventNames, TouchEventNames, TouchHandler } from './TouchHandler'; +import { BasePlayer } from '../player/BasePlayer'; +import { TouchControlMessage } from '../controlMessage/TouchControlMessage'; +import MotionEvent from '../MotionEvent'; + +const TAG = '[FeaturedTouchHandler]'; + +export interface TouchHandlerListener { + sendMessage: (messages: TouchControlMessage) => void; +} + +export class FeaturedTouchHandler extends TouchHandler { + private readonly storedFromMouseEvent = new Map(); + private readonly storedFromTouchEvent = new Map(); + private static readonly touchEventsNames: TouchEventNames[] = [ + 'touchstart', + 'touchend', + 'touchmove', + 'touchcancel', + 'mousedown', + 'mouseup', + 'mousemove', + ]; + private static readonly keyEventsNames: KeyEventNames[] = ['keydown', 'keyup']; + + constructor(player: BasePlayer, public readonly listener: TouchHandlerListener) { + super(player, FeaturedTouchHandler.touchEventsNames, FeaturedTouchHandler.keyEventsNames); + this.tag.addEventListener('mouseleave', this.onMouseLeave); + this.tag.addEventListener('mouseenter', this.onMouseEnter); + } + + protected onTouchEvent(e: MouseEvent | TouchEvent): void { + const screenInfo = this.player.getScreenInfo(); + if (!screenInfo) { + return; + } + let messages: TouchControlMessage[]; + let storage: Map; + if (e instanceof MouseEvent) { + if (e.target !== this.tag) { + return; + } + storage = this.storedFromMouseEvent; + messages = this.buildTouchEvent(e, screenInfo, storage); + if (this.over) { + this.lastPosition = e; + } + } else if (e instanceof TouchEvent) { + // TODO: Research drag from out of the target inside it + if (e.target !== this.tag) { + return; + } + storage = this.storedFromTouchEvent; + messages = this.formatTouchEvent(e, screenInfo, storage); + } else { + console.error(TAG, 'Unsupported event', e); + return; + } + if (e.cancelable) { + e.preventDefault(); + } + e.stopPropagation(); + messages.forEach((message) => { + this.listener.sendMessage(message); + }); + } + + protected onKey(e: KeyboardEvent): void { + if (!this.lastPosition) { + return; + } + const screenInfo = this.player.getScreenInfo(); + if (!screenInfo) { + return; + } + const { ctrlKey, shiftKey } = e; + const { target, button, buttons, clientY, clientX } = this.lastPosition; + const type = TouchHandler.SIMULATE_MULTI_TOUCH; + const event = { ctrlKey, shiftKey, type, target, button, buttons, clientX, clientY }; + this.buildTouchEvent(event, screenInfo, new Map()); + } + + private onMouseEnter = (): void => { + this.over = true; + }; + private onMouseLeave = (): void => { + this.lastPosition = undefined; + this.over = false; + this.storedFromMouseEvent.forEach((message) => { + this.listener.sendMessage(TouchHandler.createEmulatedMessage(MotionEvent.ACTION_UP, message)); + }); + this.storedFromMouseEvent.clear(); + this.clearCanvas(); + }; + + public release(): void { + super.release(); + this.tag.removeEventListener('mouseleave', this.onMouseLeave); + this.tag.removeEventListener('mouseenter', this.onMouseEnter); + this.storedFromMouseEvent.clear(); + this.storedFromMouseEvent.clear(); + } +} diff --git a/src/app/touchHandler/SimpleTouchHandler.ts b/src/app/touchHandler/SimpleTouchHandler.ts new file mode 100644 index 00000000..15d15673 --- /dev/null +++ b/src/app/touchHandler/SimpleTouchHandler.ts @@ -0,0 +1,81 @@ +import { TouchEventNames, TouchHandler } from './TouchHandler'; +import { BasePlayer } from '../player/BasePlayer'; +import ScreenInfo from '../ScreenInfo'; +import Position from '../Position'; + +export interface TouchHandlerListener { + performClick: (position: Position) => void; + performScroll: (from: Position, to: Position) => void; +} + +const TAG = '[SimpleTouchHandler]'; + +export class SimpleTouchHandler extends TouchHandler { + private startPosition?: Position; + private endPosition?: Position; + private static readonly touchEventsNames: TouchEventNames[] = ['mousedown', 'mouseup', 'mousemove']; + private storage = new Map(); + + constructor(player: BasePlayer, private readonly listener: TouchHandlerListener) { + super(player, SimpleTouchHandler.touchEventsNames, []); + } + + protected onTouchEvent(e: MouseEvent | TouchEvent): void { + let handled = false; + if (!(e instanceof MouseEvent)) { + return; + } + if (e.target === this.tag) { + const screenInfo: ScreenInfo = this.player.getScreenInfo() as ScreenInfo; + if (!screenInfo) { + return; + } + const events = this.buildTouchEvent(e, screenInfo, this.storage); + if (events.length > 1) { + console.log(TAG, 'Too many events', events); + return; + } + if (events.length === 1) { + handled = true; + if (e.type === 'mousedown') { + this.startPosition = events[0].position; + } else { + this.endPosition = events[0].position; + } + if (this.startPosition) { + this.drawPointer(this.startPosition.point); + } + if (this.endPosition) { + this.drawPointer(this.endPosition.point); + if (this.startPosition) { + this.drawLine(this.startPosition.point, this.endPosition.point); + } + } + if (e.type === 'mouseup') { + if (this.startPosition && this.endPosition) { + this.clearCanvas(); + if (this.startPosition.point.distance(this.endPosition.point) < 10) { + this.listener.performClick(this.endPosition); + } else { + this.listener.performScroll(this.startPosition, this.endPosition); + } + } + } + } + if (handled) { + if (e.cancelable) { + e.preventDefault(); + } + e.stopPropagation(); + } + } + if (e.type === 'mouseup') { + this.startPosition = undefined; + this.endPosition = undefined; + } + } + + protected onKey(): void { + throw Error(`${TAG} Unsupported`); + } +} diff --git a/src/app/touchHandler/TouchHandler.ts b/src/app/touchHandler/TouchHandler.ts new file mode 100644 index 00000000..5bb7580b --- /dev/null +++ b/src/app/touchHandler/TouchHandler.ts @@ -0,0 +1,560 @@ +import MotionEvent from '../MotionEvent'; +import ScreenInfo from '../ScreenInfo'; +import { TouchControlMessage } from '../controlMessage/TouchControlMessage'; +import Size from '../Size'; +import Point from '../Point'; +import Position from '../Position'; +import TouchPointPNG from '../../public/images/multitouch/touch_point.png'; +import CenterPointPNG from '../../public/images/multitouch/center_point.png'; +import Util from '../Util'; +import { BasePlayer } from '../player/BasePlayer'; + +interface Touch { + action: number; + position: Position; + buttons: number; + invalid: boolean; +} + +interface TouchOnClient { + client: { + width: number; + height: number; + }; + touch: Touch; +} + +interface CommonTouchAndMouse { + clientX: number; + clientY: number; + type: string; + target: EventTarget | null; + buttons: number; +} + +interface MiniMouseEvent extends CommonTouchAndMouse { + ctrlKey: boolean; + shiftKey: boolean; + buttons: number; +} + +const TAG = '[TouchHandler]'; + +export type TouchEventNames = + | 'touchstart' + | 'touchend' + | 'touchmove' + | 'touchcancel' + | 'mousedown' + | 'mouseup' + | 'mousemove'; +export type KeyEventNames = 'keydown' | 'keyup'; + +export abstract class TouchHandler { + protected static readonly SIMULATE_MULTI_TOUCH = 'SIMULATE_MULTI_TOUCH'; + protected static readonly STROKE_STYLE: string = '#00BEA4'; + protected static EVENT_ACTION_MAP: Record = { + touchstart: MotionEvent.ACTION_DOWN, + touchend: MotionEvent.ACTION_UP, + touchmove: MotionEvent.ACTION_MOVE, + touchcancel: MotionEvent.ACTION_UP, + mousedown: MotionEvent.ACTION_DOWN, + mousemove: MotionEvent.ACTION_MOVE, + mouseup: MotionEvent.ACTION_UP, + [TouchHandler.SIMULATE_MULTI_TOUCH]: -1, + }; + private static options = Util.supportsPassive() ? { passive: false } : false; + private static idToPointerMap: Map = new Map(); + private static pointerToIdMap: Map = new Map(); + private static touchPointRadius = 10; + private static centerPointRadius = 5; + private static touchPointImage?: HTMLImageElement; + private static centerPointImage?: HTMLImageElement; + private static pointImagesLoaded = false; + private static eventListeners: Map> = new Map(); + private multiTouchActive = false; + private multiTouchCenter?: Point; + private multiTouchShift = false; + private dirtyPlace: Point[] = []; + protected readonly ctx: CanvasRenderingContext2D | null; + protected readonly tag: HTMLCanvasElement; + protected over = false; + protected lastPosition?: MouseEvent; + + protected constructor( + public readonly player: BasePlayer, + public readonly touchEventsNames: TouchEventNames[], + public readonly keyEventsNames: KeyEventNames[], + ) { + this.tag = player.getTouchableElement(); + this.ctx = this.tag.getContext('2d'); + TouchHandler.loadImages(); + TouchHandler.bindGlobalListeners(this); + } + + protected abstract onTouchEvent(e: MouseEvent | TouchEvent): void; + protected abstract onKey(e: KeyboardEvent): void; + + protected static bindGlobalListeners(touchHandler: TouchHandler): void { + touchHandler.touchEventsNames.forEach((eventName) => { + let set: Set | undefined = TouchHandler.eventListeners.get(eventName); + if (!set) { + set = new Set(); + set.add(touchHandler); + document.body.addEventListener(eventName, this.onMouseOrTouchEvent, TouchHandler.options); + } + this.eventListeners.set(eventName, set); + }); + touchHandler.keyEventsNames.forEach((eventName) => { + let set = TouchHandler.eventListeners.get(eventName); + if (!set) { + set = new Set(); + set.add(touchHandler); + document.body.addEventListener(eventName, this.onKeyEvent); + } + this.eventListeners.set(eventName, set); + }); + } + + protected static unbindListeners(touchHandler: TouchHandler): void { + touchHandler.touchEventsNames.forEach((eventName) => { + const set = TouchHandler.eventListeners.get(eventName); + if (!set) { + return; + } + set.delete(touchHandler); + if (set.size <= 0) { + this.eventListeners.delete(eventName); + document.body.removeEventListener(eventName, this.onMouseOrTouchEvent); + } + }); + touchHandler.keyEventsNames.forEach((eventName) => { + const set = TouchHandler.eventListeners.get(eventName); + if (!set) { + return; + } + set.delete(touchHandler); + if (set.size <= 0) { + this.eventListeners.delete(eventName); + document.body.removeEventListener(eventName, this.onKeyEvent); + } + }); + } + + protected static onMouseOrTouchEvent = (e: MouseEvent | TouchEvent): void => { + const set = TouchHandler.eventListeners.get(e.type as TouchEventNames); + if (!set) { + return; + } + set.forEach((instance) => { + instance.onTouchEvent(e); + }); + }; + + protected static onKeyEvent = (e: KeyboardEvent): void => { + const set = TouchHandler.eventListeners.get(e.type as KeyEventNames); + if (!set) { + return; + } + set.forEach((instance) => { + instance.onKey(e); + }); + }; + + protected static loadImages(): void { + if (this.pointImagesLoaded) { + return; + } + const total = 2; + let current = 0; + + const onload = (e: Event) => { + if (++current === total) { + this.pointImagesLoaded = true; + } + if (e.target === this.touchPointImage) { + this.touchPointRadius = this.touchPointImage.width / 2; + } else if (e.target === this.centerPointImage) { + this.centerPointRadius = this.centerPointImage.width / 2; + } + }; + const touch = (this.touchPointImage = new Image()); + touch.src = TouchPointPNG; + touch.onload = onload; + const center = (this.centerPointImage = new Image()); + center.src = CenterPointPNG; + center.onload = onload; + } + + protected static getPointerId(type: string, identifier: number): number { + if (this.idToPointerMap.has(identifier)) { + const pointerId = this.idToPointerMap.get(identifier) as number; + if (type === 'touchend' || type === 'touchcancel') { + this.idToPointerMap.delete(identifier); + this.pointerToIdMap.delete(pointerId); + } + return pointerId; + } + let pointerId = 0; + while (this.pointerToIdMap.has(pointerId)) { + pointerId++; + } + this.idToPointerMap.set(identifier, pointerId); + this.pointerToIdMap.set(pointerId, identifier); + return pointerId; + } + + protected static calculateCoordinates(e: CommonTouchAndMouse, screenInfo: ScreenInfo): TouchOnClient | null { + const action = this.mapTypeToAction(e.type); + if (typeof action === 'undefined' || !screenInfo) { + return null; + } + const htmlTag = document.getElementsByTagName('html')[0] as HTMLElement; + const { width, height } = screenInfo.videoSize; + const target: HTMLElement = e.target as HTMLElement; + const { scrollTop, scrollLeft } = htmlTag; + let { clientWidth, clientHeight } = target; + let touchX = e.clientX - target.offsetLeft + scrollLeft; + let touchY = e.clientY - target.offsetTop + scrollTop; + let invalid = false; + if (touchX < 0 || touchX > clientWidth || touchY < 0 || touchY > clientHeight) { + invalid = true; + } + const eps = 1e5; + const ratio = width / height; + const shouldBe = Math.round(eps * ratio); + const haveNow = Math.round((eps * clientWidth) / clientHeight); + if (shouldBe > haveNow) { + const realHeight = Math.ceil(clientWidth / ratio); + const top = (clientHeight - realHeight) / 2; + if (touchY < top || touchY > top + realHeight) { + invalid = true; + } + touchY -= top; + clientHeight = realHeight; + } else if (shouldBe < haveNow) { + const realWidth = Math.ceil(clientHeight * ratio); + const left = (clientWidth - realWidth) / 2; + if (touchX < left || touchX > left + realWidth) { + invalid = true; + } + touchX -= left; + clientWidth = realWidth; + } + const x = (touchX * width) / clientWidth; + const y = (touchY * height) / clientHeight; + const size = new Size(width, height); + const point = new Point(x, y); + const position = new Position(point, size); + if (x < 0 || y < 0 || x > width || y > height) { + invalid = true; + } + return { + client: { + width: clientWidth, + height: clientHeight, + }, + touch: { + invalid, + action, + position, + buttons: e.buttons, + }, + }; + } + + private static validateMessage( + originalEvent: MiniMouseEvent | TouchEvent, + message: TouchControlMessage, + storage: Map, + logPrefix: string, + ): TouchControlMessage[] { + const messages: TouchControlMessage[] = []; + const { action, pointerId } = message; + const previous = storage.get(pointerId); + if (action === MotionEvent.ACTION_UP) { + if (!previous) { + console.warn(logPrefix, 'Received ACTION_UP while there are no DOWN stored'); + } else { + storage.delete(pointerId); + messages.push(message); + } + } else if (action === MotionEvent.ACTION_DOWN) { + if (previous) { + console.warn(logPrefix, 'Received ACTION_DOWN while already has one stored'); + } else { + storage.set(pointerId, message); + messages.push(message); + } + } else if (action === MotionEvent.ACTION_MOVE) { + if (!previous) { + if ( + (originalEvent instanceof MouseEvent && originalEvent.buttons) || + originalEvent instanceof TouchEvent + ) { + console.warn(logPrefix, 'Received ACTION_MOVE while there are no DOWN stored'); + const emulated = TouchHandler.createEmulatedMessage(MotionEvent.ACTION_DOWN, message); + messages.push(emulated); + storage.set(pointerId, emulated); + } + } else { + messages.push(message); + storage.set(pointerId, message); + } + } + return messages; + } + + protected static createEmulatedMessage(action: number, event: TouchControlMessage): TouchControlMessage { + const { pointerId, position, buttons } = event; + let pressure = event.pressure; + if (action === MotionEvent.ACTION_UP) { + pressure = 0; + } + return new TouchControlMessage(action, pointerId, position, pressure, buttons); + } + + public static mapTypeToAction(type: string): number { + return this.EVENT_ACTION_MAP[type]; + } + + protected getTouch( + e: CommonTouchAndMouse, + screenInfo: ScreenInfo, + ctrlKey: boolean, + shiftKey: boolean, + ): Touch[] | null { + const touchOnClient = TouchHandler.calculateCoordinates(e, screenInfo); + if (!touchOnClient) { + return null; + } + const { client, touch } = touchOnClient; + const result: Touch[] = [touch]; + if (!ctrlKey) { + this.multiTouchActive = false; + this.multiTouchCenter = undefined; + this.multiTouchShift = false; + this.clearCanvas(); + return result; + } + const { position, action, buttons } = touch; + const { point, screenSize } = position; + const { width, height } = screenSize; + const { x, y } = point; + if (!this.multiTouchActive) { + if (shiftKey) { + this.multiTouchCenter = point; + this.multiTouchShift = true; + } else { + this.multiTouchCenter = new Point(client.width / 2, client.height / 2); + } + } + this.multiTouchActive = true; + let opposite: Point | undefined; + let invalid = false; + if (this.multiTouchShift && this.multiTouchCenter) { + const oppoX = 2 * this.multiTouchCenter.x - x; + const oppoY = 2 * this.multiTouchCenter.y - y; + opposite = new Point(oppoX, oppoY); + if (!(oppoX <= width && oppoX >= 0 && oppoY <= height && oppoY >= 0)) { + invalid = true; + } + } else { + opposite = new Point(client.width - x, client.height - y); + invalid = touch.invalid; + } + if (opposite) { + result.push({ + invalid, + action, + buttons, + position: new Position(opposite, screenSize), + }); + } + return result; + } + + protected drawCircle(ctx: CanvasRenderingContext2D, point: Point, radius: number): void { + ctx.beginPath(); + ctx.arc(point.x, point.y, radius, 0, Math.PI * 2, true); + ctx.stroke(); + } + + public drawLine(point1: Point, point2: Point): void { + if (!this.ctx) { + return; + } + this.ctx.save(); + this.ctx.strokeStyle = TouchHandler.STROKE_STYLE; + this.ctx.beginPath(); + this.ctx.moveTo(point1.x, point1.y); + this.ctx.lineTo(point2.x, point2.y); + this.ctx.stroke(); + this.ctx.restore(); + } + + protected drawPoint(point: Point, radius: number, image?: HTMLImageElement): void { + if (!this.ctx) { + return; + } + let { lineWidth } = this.ctx; + if (TouchHandler.pointImagesLoaded && image) { + radius = image.width / 2; + lineWidth = 0; + this.ctx.drawImage(image, point.x - radius, point.y - radius); + } else { + this.drawCircle(this.ctx, point, radius); + } + + const topLeft = new Point(point.x - radius - lineWidth, point.y - radius - lineWidth); + const bottomRight = new Point(point.x + radius + lineWidth, point.y + radius + lineWidth); + this.updateDirty(topLeft, bottomRight); + } + + public drawPointer(point: Point): void { + this.drawPoint(point, TouchHandler.touchPointRadius, TouchHandler.touchPointImage); + if (this.multiTouchCenter) { + this.drawLine(this.multiTouchCenter, point); + } + } + + public drawCenter(point: Point): void { + this.drawPoint(point, TouchHandler.centerPointRadius, TouchHandler.centerPointImage); + } + + protected updateDirty(topLeft: Point, bottomRight: Point): void { + if (!this.dirtyPlace.length) { + this.dirtyPlace.push(topLeft, bottomRight); + return; + } + const currentTopLeft = this.dirtyPlace[0]; + const currentBottomRight = this.dirtyPlace[1]; + const newTopLeft = new Point(Math.min(currentTopLeft.x, topLeft.x), Math.min(currentTopLeft.y, topLeft.y)); + const newBottomRight = new Point( + Math.max(currentBottomRight.x, bottomRight.x), + Math.max(currentBottomRight.y, bottomRight.y), + ); + this.dirtyPlace.length = 0; + this.dirtyPlace.push(newTopLeft, newBottomRight); + } + + public clearCanvas(): void { + const { clientWidth, clientHeight } = this.tag; + const ctx = this.ctx; + if (ctx && this.dirtyPlace.length) { + const topLeft = this.dirtyPlace[0]; + const bottomRight = this.dirtyPlace[1]; + this.dirtyPlace.length = 0; + const x = Math.max(topLeft.x, 0); + const y = Math.max(topLeft.y, 0); + const w = Math.min(clientWidth, bottomRight.x - x); + const h = Math.min(clientHeight, bottomRight.y - y); + ctx.clearRect(x, y, w, h); + ctx.strokeStyle = TouchHandler.STROKE_STYLE; + } + } + + public formatTouchEvent( + e: TouchEvent, + screenInfo: ScreenInfo, + storage: Map, + ): TouchControlMessage[] { + const logPrefix = `${TAG}[formatTouchEvent]`; + const messages: TouchControlMessage[] = []; + const touches = e.changedTouches; + if (touches && touches.length) { + for (let i = 0, l = touches.length; i < l; i++) { + const touch = touches[i]; + const pointerId = TouchHandler.getPointerId(e.type, touch.identifier); + if (touch.target !== this.tag) { + continue; + } + const previous = storage.get(pointerId); + const item: CommonTouchAndMouse = { + clientX: touch.clientX, + clientY: touch.clientY, + type: e.type, + buttons: MotionEvent.BUTTON_PRIMARY, + target: e.target, + }; + const event = TouchHandler.calculateCoordinates(item, screenInfo); + if (event) { + const { action, buttons, position, invalid } = event.touch; + let pressure = 0; + if (action !== MotionEvent.ACTION_UP) { + pressure = touch.force; + } + if (!invalid) { + const message = new TouchControlMessage(action, pointerId, position, pressure, buttons); + messages.push(...TouchHandler.validateMessage(e, message, storage, `${logPrefix}[validate]`)); + } else { + if (previous) { + messages.push(TouchHandler.createEmulatedMessage(MotionEvent.ACTION_UP, previous)); + storage.delete(pointerId); + } + } + } else { + console.error(logPrefix, `Failed to format touch`, touch); + } + } + } else { + console.error(logPrefix, 'No "touches"', e); + } + return messages; + } + + public buildTouchEvent( + e: MiniMouseEvent, + screenInfo: ScreenInfo, + storage: Map, + ): TouchControlMessage[] { + const logPrefix = `${TAG}[buildTouchEvent]`; + const touches = this.getTouch(e, screenInfo, e.ctrlKey, e.shiftKey); + if (!touches) { + return []; + } + const messages: TouchControlMessage[] = []; + const points: Point[] = []; + this.clearCanvas(); + touches.forEach((touch: Touch, pointerId: number) => { + const { action, buttons, position } = touch; + const previous = storage.get(pointerId); + if (!touch.invalid) { + let pressure = 1.0; + if (action === MotionEvent.ACTION_UP) { + pressure = 0; + } + const message = new TouchControlMessage(action, pointerId, position, pressure, buttons); + messages.push(...TouchHandler.validateMessage(e, message, storage, `${logPrefix}[validate]`)); + points.push(touch.position.point); + } else { + if (previous) { + points.push(previous.position.point); + } + } + }); + if (this.multiTouchActive) { + if (this.multiTouchCenter) { + this.drawCenter(this.multiTouchCenter); + } + points.forEach((point) => { + this.drawPointer(point); + }); + } + const hasActionUp = messages.find((message) => { + return message.action === MotionEvent.ACTION_UP; + }); + if (hasActionUp && storage.size) { + console.warn(logPrefix, 'Looks like one of Multi-touch pointers was not raised up'); + storage.forEach((message) => { + messages.push(TouchHandler.createEmulatedMessage(MotionEvent.ACTION_UP, message)); + }); + storage.clear(); + } + return messages; + } + + public release(): void { + TouchHandler.unbindListeners(this); + } +}