Skip to content

Commit

Permalink
fix: synthetic browser events
Browse files Browse the repository at this point in the history
After a touchend event, some browsers may fire synthetic mouse events (mouseover, mousedown, mousemove, mouseup) if the touch interaction did not cause any default action (such as scrolling). This is done to simulate the behavior of a mouse for applications that do not support touch events.

This is a problem because it results in the JS code reporting duplicated pointer events to Rive:
 - Confirmed fix Chrome desktop (device simulation)
 - Confirmed fix on iOS Safari (real device)

This PR introduces a solution to keep track of the previous event and to not send the synthetic mouse events if the
touch event was a click (touchstart -> touchend).

This is only needed when `isTouchScrollEnabled` is false. When true, `preventDefault()` is called which prevents this behaviour.

Not helpful for the solution, but for context:
- https://stackoverflow.com/questions/9656990/how-to-prevent-simulated-mouse-events-in-mobile-browsers
- https://stackoverflow.com/questions/25572070/javascript-touchend-versus-click-dilemma

Reported on Jira: https://rive.atlassian.net/browse/RIV-4845

Fixed solution (with logs):
https://github.com/user-attachments/assets/83a77c64-24b4-4381-8e97-178dfbcabc1d

Diffs=
bc9aab464 fix: synthetic browser events (#7609)

Co-authored-by: Gordon <[email protected]>
  • Loading branch information
HayesGordon and HayesGordon committed Jul 15, 2024
1 parent 1389579 commit 8136940
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
44fae6bfe787063af6cb7390fd83f5de167e79e0
bc9aab464e494a9ccf1e9e899ac853aa5e5614ef
58 changes: 51 additions & 7 deletions js/src/utils/registerTouchInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,53 @@ export const registerTouchInteractions = ({
) {
return null;
}
/**
* After a touchend event, some browsers may fire synthetic mouse events
* (mouseover, mousedown, mousemove, mouseup) if the touch interaction did not cause
* any default action (such as scrolling).
*
* This is done to simulate the behavior of a mouse for applications that do not support
* touch events.
*
* We're keeping track of the previous event to not send the synthetic mouse events if the
* touch event was a click (touchstart -> touchend).
*
* This is only needed when `isTouchScrollEnabled` is false
* When true, `preventDefault()` is called which prevents this behaviour.
**/
let _prevEventType: string | null = null;
let _syntheticEventsActive = false;

const processEventCallback = (event: MouseEvent | TouchEvent) => {
// Exit early out of all synthetic mouse events
// https://stackoverflow.com/questions/9656990/how-to-prevent-simulated-mouse-events-in-mobile-browsers
// https://stackoverflow.com/questions/25572070/javascript-touchend-versus-click-dilemma
if (_syntheticEventsActive && event instanceof MouseEvent) {
// Synthetic event finished
if (event.type == "mouseup") {
_syntheticEventsActive = false;
}

return;
}

// Test if it's a "touch click". This could cause the browser to send
// synthetic mouse events.
_syntheticEventsActive =
isTouchScrollEnabled &&
event.type === "touchend" &&
_prevEventType === "touchstart";

_prevEventType = event.type;

const boundingRect = (
event.currentTarget as HTMLCanvasElement
).getBoundingClientRect();

const { clientX, clientY } = getClientCoordinates(event, isTouchScrollEnabled);
const { clientX, clientY } = getClientCoordinates(
event,
isTouchScrollEnabled,
);
if (!clientX && !clientY) {
return;
}
Expand All @@ -101,14 +141,14 @@ export const registerTouchInteractions = ({
maxX: boundingRect.width,
maxY: boundingRect.height,
},
artboard.bounds
artboard.bounds,
);
const invertedMatrix = new rive.Mat2D();
forwardMatrix.invert(invertedMatrix);
const canvasCoordinatesVector = new rive.Vec2D(canvasX, canvasY);
const transformedVector = rive.mapXY(
invertedMatrix,
canvasCoordinatesVector
canvasCoordinatesVector,
);
const transformedX = transformedVector.x();
const transformedY = transformedVector.y();
Expand All @@ -126,7 +166,7 @@ export const registerTouchInteractions = ({
* exit. We're therefore adding to the translated coordinates on mouseout of a canvas
* to ensure that we report the mouse has truly exited the hitarea.
* https://github.com/rive-app/rive-cpp/blob/master/src/animation/state_machine_instance.cpp#L336
*
*
* We add/subtract 10000 to account for when the graphic goes beyond the canvas bound
* due to for example, a fit: 'cover'. Not perfect, but helps reliably (for now) ensure
* we report going out of bounds when the mouse is out of the canvas
Expand All @@ -135,7 +175,7 @@ export const registerTouchInteractions = ({
for (const stateMachine of stateMachines) {
stateMachine.pointerMove(
transformedX < 0 ? transformedX - 10000 : transformedX + 10000,
transformedY < 0 ? transformedY - 10000 : transformedY + 10000
transformedY < 0 ? transformedY - 10000 : transformedY + 10000,
);
}
break;
Expand Down Expand Up @@ -174,8 +214,12 @@ export const registerTouchInteractions = ({
canvas.addEventListener("mousemove", callback);
canvas.addEventListener("mousedown", callback);
canvas.addEventListener("mouseup", callback);
canvas.addEventListener("touchmove", callback);
canvas.addEventListener("touchstart", callback);
canvas.addEventListener("touchmove", callback, {
passive: isTouchScrollEnabled,
});
canvas.addEventListener("touchstart", callback, {
passive: isTouchScrollEnabled,
});
canvas.addEventListener("touchend", callback);
return () => {
canvas.removeEventListener("mouseover", callback);
Expand Down

0 comments on commit 8136940

Please sign in to comment.