Skip to content

Commit

Permalink
Add pinch and rotate gestures.
Browse files Browse the repository at this point in the history
  • Loading branch information
hperrin committed Dec 3, 2023
1 parent 2ae9e4e commit 8d38cac
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 221 deletions.
16 changes: 7 additions & 9 deletions DemoPannable.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ export default function Pannable(node) {
return;
}
animationFrame = window.requestAnimationFrame(() => {
// Give an indication of whether we've passed the swiping threshold.
if (!gesture.swipingDirection.startsWith('pre-')) {
node.style.opacity = '0.7';
} else {
node.style.opacity = '1';
if (gesture.scale <= 1.1 && gesture.scale >= 0.9) {
// Give an indication of whether we've passed the swiping threshold.
if (!gesture.swipingDirection.startsWith('pre-')) {
node.style.opacity = '0.7';
} else {
node.style.opacity = '1';
}
}
// Give an indication of how far the user has pulled the target away from its origin.
node.style.transform = 'rotate(' + (gesture.touchMoveX / 8 + gesture.touchMoveY / 8) + 'deg)';
// Update the location to under the user's finger/mouse.
node.style.left = gesture.touchMoveX + 'px';
node.style.top = gesture.touchMoveY + 'px';
Expand All @@ -45,7 +45,6 @@ export default function Pannable(node) {
gesture.on('panend', () => {
animationFrame == null || window.cancelAnimationFrame(animationFrame);
animationFrame = null;
node.style.transform = '';
// Set left and top transitions so we smoothly animate back to the target's origin.
addTransition(node, 'left .3s ease');
addTransition(node, 'top .3s ease');
Expand All @@ -61,7 +60,6 @@ export default function Pannable(node) {
passive: false,
});
animationFrame == null || window.cancelAnimationFrame(animationFrame);
node.style.transform = '';
removeTransition(node, 'opacity');
removeTransition(node, 'left');
removeTransition(node, 'top');
Expand Down
69 changes: 69 additions & 0 deletions DemoPinchable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import TinyGesture from './dist/TinyGesture.js';
import { addTransition, removeTransition } from './DemoTransitions.js';

/**
* This function can be used as a Svelte action.
*/
export default function Pinchable(node) {
const gesture = new TinyGesture(node);
let backTimeout;
const preventDefault = (event) => {
event.preventDefault();
};

addTransition(node, 'transform .5s ease');
addTransition(node, 'transform-origin .5s ease');

// Don't allow the page to scroll when the target is first pressed.
node.addEventListener('touchstart', preventDefault, { passive: false });

let scale = 1;
let origin = null;
let myTransform = ` scale(${scale})`;
node.style.transform = '';

function resetTransform() {
node.style.transform = `${node.style.transform}`.replace(/\s*scale\([^)]*\)/, '');
myTransform = ` scale(${scale})`;
}

// When the target is pinched, scale it to the right size.
gesture.on('pinch', () => {
scale = gesture.scale;
if (origin == null) {
const box = node.getBoundingClientRect();
origin = [gesture.touchMove1.clientX - box.x, gesture.touchMove1.clientY - box.y];
node.style.transformOrigin = `${origin[0]}px ${origin[1]}px`;
}
resetTransform();
removeTransition(node, 'transform');
removeTransition(node, 'transform-origin');
node.style.transform = `${node.style.transform}` + myTransform;
});

// When the target is pinched, scale it to the right size.
gesture.on('pinchend', () => {
scale = gesture.scale;
origin = null;
addTransition(node, 'transform .5s ease');
addTransition(node, 'transform-origin .5s ease');
node.style.transformOrigin = 'center';
clearTimeout(backTimeout);
backTimeout = setTimeout(() => {
scale = 0;
resetTransform();
}, 1000);
});

return {
destroy() {
node.removeEventListener('touchstart', preventDefault, {
passive: false,
});
clearTimeout(backTimeout);
node.style.transform = '';
removeTransition(node, 'transform');
gesture.destroy();
},
};
}
69 changes: 69 additions & 0 deletions DemoRotatable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import TinyGesture from './dist/TinyGesture.js';
import { addTransition, removeTransition } from './DemoTransitions.js';

/**
* This function can be used as a Svelte action.
*/
export default function Rotatable(node) {
const gesture = new TinyGesture(node);
let backTimeout;
const preventDefault = (event) => {
event.preventDefault();
};

addTransition(node, 'transform .5s ease');
addTransition(node, 'transform-origin .5s ease');

// Don't allow the page to scroll when the target is first pressed.
node.addEventListener('touchstart', preventDefault, { passive: false });

let angle = 0;
let origin = null;
let myTransform = ` rotate(${angle}deg)`;
node.style.transform = '';

function resetTransform() {
node.style.transform = `${node.style.transform}`.replace(/\s*rotate\([^)]*\)/, '');
myTransform = ` rotate(${angle}deg)`;
}

// When the target is rotated, rotate it to the right angle.
gesture.on('rotate', () => {
angle = gesture.rotation;
if (origin == null) {
const box = node.getBoundingClientRect();
origin = [gesture.touchMove1.clientX - box.x, gesture.touchMove1.clientY - box.y];
node.style.transformOrigin = `${origin[0]}px ${origin[1]}px`;
}
resetTransform();
removeTransition(node, 'transform');
removeTransition(node, 'transform-origin');
node.style.transform = `${node.style.transform}` + myTransform;
});

// When the target is rotated, rotate it to the right angle.
gesture.on('rotateend', () => {
angle = gesture.angle;
origin = null;
addTransition(node, 'transform .5s ease');
addTransition(node, 'transform-origin .5s ease');
node.style.transformOrigin = 'center';
clearTimeout(backTimeout);
backTimeout = setTimeout(() => {
angle = 0;
resetTransform();
}, 1000);
});

return {
destroy() {
node.removeEventListener('touchstart', preventDefault, {
passive: false,
});
clearTimeout(backTimeout);
node.style.transform = '';
removeTransition(node, 'transform');
gesture.destroy();
},
};
}
30 changes: 27 additions & 3 deletions DemoSwipeable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,66 @@ export default function Swipeable(node) {
event.preventDefault();
};

addTransition(node, 'transform .3s ease');
addTransition(node, 'transform .5s ease');

// Don't allow the page to scroll when the target is first pressed.
node.addEventListener('touchstart', preventDefault, { passive: false });

let xpos = 0;
let ypos = 0;
let myTransform = ` translateX(${xpos}px) translateY(${ypos}px)`;
node.style.transform = '';

function resetTransform() {
node.style.transform = `${node.style.transform}`.replace(/\s*translateX\([^)]*\)/, '');
node.style.transform = `${node.style.transform}`.replace(/\s*translateY\([^)]*\)/, '');
myTransform = ` translateX(${xpos}px) translateY(${ypos}px)`;
}

function doTransform() {
node.style.transform = `perspective(1000px) translate3d(${xpos}px, ${ypos}px, 0)`;
node.style.transform = `${node.style.transform}` + myTransform;
clearTimeout(backTimeout);
backTimeout = setTimeout(() => {
xpos = 0;
ypos = 0;
node.style.transform = '';
resetTransform();
}, 1000);
}

// When the target is swiped, fling it really far in that direction before coming back to origin.
gesture.on('swiperight', () => {
if (gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
xpos = 2000;
resetTransform();
cancelAnimationFrame(goRaf);
goRaf = requestAnimationFrame(doTransform);
});
gesture.on('swipeleft', () => {
if (gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
xpos = -2000;
resetTransform();
cancelAnimationFrame(goRaf);
goRaf = requestAnimationFrame(doTransform);
});
gesture.on('swipeup', () => {
if (gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
ypos = -2000;
resetTransform();
cancelAnimationFrame(goRaf);
goRaf = requestAnimationFrame(doTransform);
});
gesture.on('swipedown', () => {
if (gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
ypos = 2000;
resetTransform();
cancelAnimationFrame(goRaf);
goRaf = requestAnimationFrame(doTransform);
});
Expand Down
10 changes: 7 additions & 3 deletions DemoTappable.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Tappable(
options = {
bgColor: 'transparent',
color: 'black',
}
},
) {
const gesture = new TinyGesture(node);
let tapTimeout;
Expand All @@ -22,8 +22,8 @@ export default function Tappable(

// Note: don't use the 'tap' event to detect when the user has finished a long press, because it doesn't always fire.
gesture.on('tap', () => {
// If the user long pressed, don't run the tap handler. This event fires after the user lifts their finger.
if (pressed) {
// If the user long pressed or pinched, don't run the tap handler. This event fires after the user lifts their finger.
if (pressed || gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
// Embiggen.
Expand All @@ -40,6 +40,10 @@ export default function Tappable(
gesture.on('longpress', () => {
// Indicate that this is a long press. This event fires before the user lifts their finger.
pressed = true;
// If the user pinched, don't run the handler.
if (gesture.scale > 1.1 || gesture.scale < 0.9) {
return;
}
// Change colors.
node.style.backgroundColor = options.bgColor;
node.style.color = options.color;
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# TinyGesture.js

Very small gesture recognizer for JavaScript. Swipe, pan, tap, doubletap, and longpress.
Very small gesture recognizer for JavaScript. Swipe, pan, tap, doubletap, longpress, pinch, and rotate.

## Installation

```sh
npm install --save tinygesture
```

If you're upgrading from 1.0, the only breaking change in 2.0 is the location of the file. It's now in a "dist" folder, hence the major version change.
- If you're upgrading from v2, the `diagonalLimit` option has changed meaning and there are new events for pinch and rotate. Also TS now exports ES2020 instead of ES6.
- If you're upgrading from v1, the location of the file has changed. It's now in a "dist" folder, hence the major version change.

## Usage

Expand Down Expand Up @@ -158,6 +159,29 @@ gesture.on('doubletap', (event) => {
gesture.on('longpress', (event) => {
// The gesture is currently ongoing, and is now a long press.
});

gesture.on('pinch', (event) => {
// The gesture is an ongoing pinch.

// This is the current scale of the pinch. <1 means the user is zooming out.
// >1 means the user is zooming in.
gesture.scale;
});

gesture.on('pinchend', (event) => {
// The pinch gesture is completed.
});

gesture.on('rotate', (event) => {
// The gesture is an ongoing rotate.

// This is the current angle of the rotation, in degrees.
gesture.rotation;
});

gesture.on('rotateend', (event) => {
// The rotate gesture is completed.
});
```

### Long Press without Tap
Expand Down
14 changes: 14 additions & 0 deletions dist/TinyGesture.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
export default class TinyGesture<Element extends HTMLElement = HTMLElement> {
element: Element;
opts: Options<Element>;
touch1: Touch | null;
touch2: Touch | null;
touchStartX: number | null;
touchStartY: number | null;
touchEndX: number | null;
touchEndY: number | null;
touchMove1: Touch | null;
touchMove2: Touch | null;
touchMoveX: number | null;
touchMoveY: number | null;
velocityX: number | null;
Expand All @@ -21,6 +25,12 @@ export default class TinyGesture<Element extends HTMLElement = HTMLElement> {
swipingDirection: SwipingDirection | null;
swipedHorizontal: boolean;
swipedVertical: boolean;
originalDistance: number | null;
newDistance: number | null;
scale: number | null;
originalAngle: number | null;
newAngle: number | null;
rotation: number | null;
handlers: Handlers;
private _onTouchStart;
private _onTouchMove;
Expand Down Expand Up @@ -61,6 +71,10 @@ export interface Events {
swiperight: MouseEvent | TouchEvent;
swipeup: MouseEvent | TouchEvent;
tap: MouseEvent | TouchEvent;
pinch: TouchEvent;
pinchend: TouchEvent;
rotate: TouchEvent;
rotateend: TouchEvent;
}
export type Handler<E> = (event: E) => void;
export type Handlers = {
Expand Down
Loading

0 comments on commit 8d38cac

Please sign in to comment.