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

Add alternate touch event models #52

Merged
merged 10 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/modules/browser-event-manager/browser-event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class BrowserEventManager {
activatedEvents: string[] = [];
eventHandlers: [string, any][] = [];
bounds: DOMRect;

listening: boolean;
static eventPool = {
atlas: { x: 0, y: 0 },
};
Expand Down Expand Up @@ -50,6 +50,7 @@ export class BrowserEventManager {
this.runtime = runtime;
this.unsubscribe = runtime.world.addLayoutSubscriber(this.layoutSubscriber.bind(this));
this.bounds = element.getBoundingClientRect();
this.listening = false;
this.options = {
simulationRate: 0,
...(options || {}),
Expand All @@ -69,9 +70,6 @@ export class BrowserEventManager {
this.onPointerMove(this.pointerMoveEvent);
}
});

// @todo temp.
this.activateEvents();
abrin marked this conversation as resolved.
Show resolved Hide resolved
}

updateBounds() {
Expand All @@ -80,7 +78,7 @@ export class BrowserEventManager {
}

layoutSubscriber(type: string) {
if (type === 'event-activation') {
abrin marked this conversation as resolved.
Show resolved Hide resolved
if (type === 'event-activation' && this.listening == false) {
this.activateEvents();
}
}
Expand All @@ -92,6 +90,7 @@ export class BrowserEventManager {
}

activateEvents() {
this.listening = true;
this.element.addEventListener('pointermove', this._realPointerMove);
this.element.addEventListener('pointerup', this.onPointerUp);
this.element.addEventListener('pointerdown', this.onPointerDown);
Expand Down Expand Up @@ -292,6 +291,25 @@ export class BrowserEventManager {
}

stop() {
this.listening = false;
this.element.removeEventListener('pointermove', this._realPointerMove);
this.element.removeEventListener('pointerup', this.onPointerUp);
this.element.removeEventListener('pointerdown', this.onPointerDown);

// Normal events.
this.element.removeEventListener('mousedown', this.onPointerEvent);
this.element.removeEventListener('mouseup', this.onPointerEvent);
this.element.removeEventListener('pointercancel', this.onPointerEvent);

// Edge-cases
this.element.removeEventListener('wheel', this.onWheelEvent);

// Touch events.
this.element.removeEventListener('touchstart', this.onTouchEvent);
this.element.removeEventListener('touchcancel', this.onTouchEvent);
this.element.removeEventListener('touchend', this.onTouchEvent);
this.element.removeEventListener('touchmove', this.onTouchEvent);

// Unbind all events.
this.unsubscribe();
for (const [event, handler] of this.eventHandlers) {
Expand Down
98 changes: 88 additions & 10 deletions src/modules/popmotion-controller/popmotion-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { RuntimeController } from '../../types';
import { easingFunctions } from '../../utility/easing-functions';
import { toBox } from '../../utility/to-box';

const INTENT_PAN = 'pan';
const INTENT_SCROLL = 'scroll';
const INTENT_GESTURE = 'gesture';

export type PopmotionControllerConfig = {
zoomOutFactor?: number;
zoomInFactor?: number;
Expand All @@ -23,6 +27,11 @@ export type PopmotionControllerConfig = {
devicePixelRatio?: number;
enableWheel?: boolean;
enableClickToZoom?: boolean;
debug?: boolean;
ignoreSingleFingerTouch?: boolean;
enablePanOnWait?: boolean;
panOnWaitDelay?: number;
parentElement?: HTMLElement | null;
onPanInSketchMode?: () => void;
};

Expand All @@ -47,16 +56,30 @@ export const defaultConfig: Required<PopmotionControllerConfig> = {
devicePixelRatio: 1,
// Flags
enableWheel: true,
enableClickToZoom: false,
enableClickToZoom: true,
ignoreSingleFingerTouch: true,
enablePanOnWait: true,
panOnWaitDelay: 40,
debug: true,
abrin marked this conversation as resolved.
Show resolved Hide resolved
onPanInSketchMode: () => {
// no-op
},
parentElement: null,
};

export const popmotionController = (config: PopmotionControllerConfig = {}): RuntimeController => {
return {
start: function (runtime) {
const { zoomWheelConstant, enableWheel, enableClickToZoom } = {
const {
zoomWheelConstant,
enableWheel,
enableClickToZoom,
ignoreSingleFingerTouch,
enablePanOnWait,
panOnWaitDelay,
debug,
parentElement,
} = {
...defaultConfig,
...config,
};
Expand Down Expand Up @@ -165,9 +188,16 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
}
}

function resetState() {
currentDistance = 0;
intent = '';
setDebugBorder();
touchStartTime = 0;
}

function onMouseUp() {
runtime.world.constraintBounds();
currentDistance = 0;
resetState();
}

function onMouseDown(e: MouseEvent & { atlas: { x: number; y: number } }) {
Expand All @@ -187,6 +217,7 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
}

function onWindowMouseUp() {
resetState();
if (state.isPressing) {
if (runtime.mode === 'explore') {
runtime.world.constraintBounds();
Expand All @@ -196,14 +227,23 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
}

let currentDistance = 0;
// the performance.now() time at 'touch-start'
let touchStartTime = 0;
// what the user's intent would be for the behavior
let intent = '';
function onTouchStart(e: TouchEvent & { atlasTouches: Array<{ id: number; x: number; y: number }> }) {
if (runtime.mode === 'explore') {
if (e.atlasTouches.length === 1) {
e.preventDefault();
touchStartTime = performance.now();
if (ignoreSingleFingerTouch == false) {
// this prevents the touch propagation to the window, and thus doesn't drag the page
e.preventDefault();
}
state.pointerStart.x = e.atlasTouches[0].x;
state.pointerStart.y = e.atlasTouches[0].y;
}
if (e.atlasTouches.length === 2) {
intent = INTENT_GESTURE;
e.preventDefault();
const x1 = e.atlasTouches[0].x;
const x2 = e.atlasTouches[1].x;
Expand All @@ -224,12 +264,21 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
}
}

function setDebugBorder(border = '1px solid transparent') {
abrin marked this conversation as resolved.
Show resolved Hide resolved
if (debug == false) {
return;
}
const el = document.querySelector('.atlas') as HTMLElement;
if (el) {
el.style.border = border;
}
}

function onTouchMove(e: TouchEvent & { atlasTouches: Array<{ id: number; x: number; y: number }> }) {
let clientX = null;
let clientY = null;
let isMulti = false;
let newDistance = 0;

if (state.isPressing && e.touches.length === 2) {
// We have 2?
const x1 = e.touches[0].clientX;
Expand All @@ -244,8 +293,27 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
{ x: e.touches[1].clientX, y: e.touches[1].clientY }
);
isMulti = true;
setDebugBorder('1px solid blue');
}

if (state.isPressing && e.touches.length === 1) {
if (enablePanOnWait) {
// if there is a delay between the touch-start and the 1st touch-move of < xms, then treat that as a PAN,
// anything faster is a window scroll
if (performance.now() - touchStartTime < panOnWaitDelay && intent == '') {
intent = INTENT_SCROLL;
setDebugBorder('1px solid red');
}
if (intent == '') {
setDebugBorder('1px solid green');
intent = INTENT_PAN;
}
}
// if we are ignoring a single finger touch, or it's a window-scroll, just 'return'
if ((intent == '' && ignoreSingleFingerTouch == true) || intent == INTENT_SCROLL) {
// have CanvasPanel do nothing... scroll the page
return;
}
abrin marked this conversation as resolved.
Show resolved Hide resolved
const touch = e.touches[0];
clientX = touch.clientX;
clientY = touch.clientY;
Expand Down Expand Up @@ -277,6 +345,12 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
}
currentDistance = newDistance;
}

if (intent == INTENT_PAN) {
// if we're panning, prevent default
// this does the same thing as touchEvents: none; pointerEvents: none;
e.preventDefault();
}
}

function onMouseMove(e: MouseEvent | PointerEvent) {
Expand Down Expand Up @@ -327,11 +401,14 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
runtime.world.addEventListener('touchstart', onTouchStart);
runtime.world.addEventListener('mousedown', onMouseDown);

window.addEventListener('touchend', onWindowMouseUp);
runtime.world.addEventListener('touchend', onWindowMouseUp);
abrin marked this conversation as resolved.
Show resolved Hide resolved
window.addEventListener('mouseup', onWindowMouseUp);

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove as any);

if (parentElement) {
parentElement.addEventListener('touchmove', onTouchMove as any);
}

if (enableClickToZoom) {
runtime.world.activatedEvents.push('onClick');
Expand Down Expand Up @@ -378,12 +455,13 @@ export const popmotionController = (config: PopmotionControllerConfig = {}): Run
runtime.world.removeEventListener('touchstart', onTouchStart);
runtime.world.removeEventListener('mousedown', onMouseDown);

window.removeEventListener('touchend', onWindowMouseUp);
runtime.world.removeEventListener('touchend', onWindowMouseUp);
window.removeEventListener('mouseup', onWindowMouseUp);

runtime.world.removeEventListener('mousemove', onMouseMove);
runtime.world.removeEventListener('touchmove', onMouseMove);

if (parentElement) {
(parentElement as any).removeEventListener('touchmove', onMouseMove);
}
if (enableClickToZoom) {
runtime.world.removeEventListener('click', onClick);
}
Expand Down
2 changes: 1 addition & 1 deletion src/modules/react-reconciler/Atlas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ export const Atlas: React.FC<
<style>{`.atlas-width-${widthClassName} { width: ${restProps.width}px; height: ${restProps.height}px; }`}</style>
) : (
<style>{`
.atlas { position: relative; user-select: none; display: flex; background: var(--atlas-background, #000); z-index: var(--atlas-z-index, 10); touch-action: none; }
.atlas { position: relative; display: flex; background: var(--atlas-background, #000); z-index: var(--atlas-z-index, 10); -webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
.atlas-width-${widthClassName} { width: ${restProps.width}px; height: ${restProps.height}px; }
.atlas-canvas { flex: 1 1 0px; }
.atlas-canvas:focus, .atlas-static-container:focus { outline: none }
stephenwf marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
1 change: 1 addition & 0 deletions src/modules/react-reconciler/presets/default-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function defaultPreset({
minZoomFactor: 0.5,
maxZoomFactor: 3,
enableClickToZoom: false,
parentElement: canvasElement,
...(controllerConfig || {}),
})
: undefined;
Expand Down
1 change: 1 addition & 0 deletions src/modules/react-reconciler/presets/static-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function staticPreset({
minZoomFactor: 0.5,
maxZoomFactor: 3,
enableClickToZoom: false,
parentElement: containerElement,
...(controllerConfig || {}),
})
: undefined;
Expand Down
13 changes: 12 additions & 1 deletion stories/annotations.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,12 @@ export const mobileSize = () => {
<>
<style>{`
.my-atlas {
--atlas-background: #fff;
--atlas-focus: 5px solid green;
}
body {
/* this background helps with identifying whether the window or the image is moving */
background: repeating-linear-gradient( 45deg, #606dbc, #606dbc 10px,#465298 10px, #465298 20px);
}
`}</style>
<div className="my-atlas" style={{ height: '100vh', width: '100%', background: 'red' }}>
<AtlasAuto renderPreset={['default-preset', { canvasBox: true }]} height={'100vh'}>
Expand All @@ -278,6 +281,14 @@ export const mobileSize = () => {
</AtlasAuto>
</div>
<style>{`body[style]{padding: 0 !important}`}</style>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<i>this allows testing of scrolling</i>
</>
);
};
Expand Down