Skip to content

Commit af8d46e

Browse files
Abhishek MittalAbhishek Mittal
authored andcommitted
added usePinchZoom sensor hook
1 parent 3685b75 commit af8d46e

File tree

5 files changed

+184
-0
lines changed

5 files changed

+184
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
- [`useMeasure`](./docs/useMeasure.md) and [`useSize`](./docs/useSize.md) — tracks an HTML element's dimensions. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo)
7878
- [`createBreakpoint`](./docs/createBreakpoint.md) — tracks `innerWidth`
7979
- [`useScrollbarWidth`](./docs/useScrollbarWidth.md) — detects browser's native scrollbars width. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescrollbarwidth--demo)
80+
- [`usePinchZoom`](./docs/usePinchZoom.md) — tracks pointer events to detect pinch zoom in and out status. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usePinchZoom--demo)
8081
<br/>
8182
<br/>
8283
- [**UI**](./docs/UI.md)

docs/usePinchZoom.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# `usePinchZoon`
2+
3+
React sensor hook that tracks the changes in pointer touch events and detects value of pinch difference and tell if user is zooming in or out.
4+
5+
## Usage
6+
7+
```jsx
8+
import { usePinchZoon } from "react-use";
9+
10+
const Demo = () => {
11+
const [scale, setState] = useState(1);
12+
const scaleRef = useRef();
13+
const { zoomingState, pinchState } = usePinchZoom(scaleRef);
14+
15+
useEffect(() => {
16+
if (zoomingState === "ZOOM_IN") {
17+
// perform zoom in scaling
18+
setState(scale + 0.1)
19+
} else if (zoomingState === "ZOOM_OUT") {
20+
// perform zoom out in scaling
21+
setState(scale - 0.1)
22+
}
23+
}, [zoomingState]);
24+
25+
return (
26+
<div ref={scaleRef}>
27+
<img
28+
src="https://www.olympus-imaging.co.in/content/000107506.jpg"
29+
style={{
30+
zoom: scale,
31+
}}
32+
/>
33+
</div>
34+
);
35+
};
36+
```

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export { useMultiStateValidator } from './useMultiStateValidator';
109109
export { default as useWindowScroll } from './useWindowScroll';
110110
export { default as useWindowSize } from './useWindowSize';
111111
export { default as useMeasure } from './useMeasure';
112+
export { default as usePinchZoom } from './usePinchZoom';
112113
export { useRendersCount } from './useRendersCount';
113114
export { useFirstMountState } from './useFirstMountState';
114115
export { default as useSet } from './useSet';

src/usePinchZoom.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { RefObject, useEffect, useMemo, useState } from 'react';
2+
3+
export type CacheRef = {
4+
prevDiff: number;
5+
evCache: Array<PointerEvent>;
6+
};
7+
8+
export enum ZoomState {
9+
'ZOOMING_IN' = 'ZOOMING_IN',
10+
'ZOOMING_OUT' = 'ZOOMING_OUT',
11+
}
12+
13+
export type ZoomStateType = ZoomState.ZOOMING_IN | ZoomState.ZOOMING_OUT;
14+
15+
const usePinchZoom = (ref: RefObject<HTMLElement>) => {
16+
const cacheRef = useMemo<CacheRef>(
17+
() => ({
18+
evCache: [],
19+
prevDiff: -1,
20+
}),
21+
[ref.current]
22+
);
23+
24+
const [zoomingState, setZoomingState] = useState<[ZoomStateType, number]>();
25+
26+
const pointermove_handler = (ev: PointerEvent) => {
27+
// This function implements a 2-pointer horizontal pinch/zoom gesture.
28+
//
29+
// If the distance between the two pointers has increased (zoom in),
30+
// the target element's background is changed to 'pink' and if the
31+
// distance is decreasing (zoom out), the color is changed to 'lightblue'.
32+
//
33+
// This function sets the target element's border to 'dashed' to visually
34+
// indicate the pointer's target received a move event.
35+
// Find this event in the cache and update its record with this event
36+
for (let i = 0; i < cacheRef.evCache.length; i++) {
37+
if (ev.pointerId == cacheRef.evCache[i].pointerId) {
38+
cacheRef.evCache[i] = ev;
39+
break;
40+
}
41+
}
42+
43+
// If two pointers are down, check for pinch gestures
44+
if (cacheRef.evCache.length == 2) {
45+
// console.log(prevDiff)
46+
// Calculate the distance between the two pointers
47+
const curDiff = Math.abs(cacheRef.evCache[0].clientX - cacheRef.evCache[1].clientX);
48+
49+
if (cacheRef.prevDiff > 0) {
50+
if (curDiff > cacheRef.prevDiff) {
51+
// The distance between the two pointers has increased
52+
setZoomingState([ZoomState.ZOOMING_IN, curDiff]);
53+
}
54+
if (curDiff < cacheRef.prevDiff) {
55+
// The distance between the two pointers has decreased
56+
setZoomingState([ZoomState.ZOOMING_OUT, curDiff]);
57+
}
58+
}
59+
60+
// Cache the distance for the next move event
61+
cacheRef.prevDiff = curDiff;
62+
}
63+
};
64+
65+
const pointerdown_handler = (ev: PointerEvent) => {
66+
// The pointerdown event signals the start of a touch interaction.
67+
// This event is cached to support 2-finger gestures
68+
cacheRef.evCache.push(ev);
69+
// console.log('pointerDown', ev);
70+
};
71+
72+
const pointerup_handler = (ev: PointerEvent) => {
73+
// Remove this pointer from the cache and reset the target's
74+
// background and border
75+
remove_event(ev);
76+
77+
// If the number of pointers down is less than two then reset diff tracker
78+
if (cacheRef.evCache.length < 2) {
79+
cacheRef.prevDiff = -1;
80+
}
81+
};
82+
83+
const remove_event = (ev: PointerEvent) => {
84+
// Remove this event from the target's cache
85+
for (let i = 0; i < cacheRef.evCache.length; i++) {
86+
if (cacheRef.evCache[i].pointerId == ev.pointerId) {
87+
cacheRef.evCache.splice(i, 1);
88+
break;
89+
}
90+
}
91+
};
92+
93+
useEffect(() => {
94+
if (ref?.current) {
95+
ref.current.onpointerdown = pointerdown_handler;
96+
ref.current.onpointermove = pointermove_handler;
97+
ref.current.onpointerup = pointerup_handler;
98+
ref.current.onpointercancel = pointerup_handler;
99+
ref.current.onpointerout = pointerup_handler;
100+
ref.current.onpointerleave = pointerup_handler;
101+
}
102+
}, [ref?.current]);
103+
104+
return zoomingState
105+
? { zoomingState: zoomingState[0], pinchState: zoomingState[1] }
106+
: { zoomingState: null, pinchState: 0 };
107+
};
108+
109+
export default usePinchZoom;

stories/usePinchZoom.story.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { storiesOf } from '@storybook/react';
2+
import { useEffect, useRef, useState } from 'react';
3+
import { usePinchZoom } from '../src';
4+
import { ZoomState } from '../src/usePinchZoom';
5+
import ShowDocs from './util/ShowDocs';
6+
7+
const Demo = () => {
8+
const [scale, setState] = useState(1);
9+
const scaleRef = useRef();
10+
const { zoomingState, pinchState } = usePinchZoom(scaleRef);
11+
12+
useEffect(() => {
13+
if (zoomingState === ZoomState.ZOOMING_IN) {
14+
// perform zoom in scaling
15+
setState(scale + 0.1);
16+
} else if (zoomingState === ZoomState.ZOOMING_OUT) {
17+
// perform zoom out in scaling
18+
setState(scale - 0.1);
19+
}
20+
}, [zoomingState, pinchState]);
21+
22+
return (
23+
<div ref={scaleRef}>
24+
<img
25+
src="https://www.olympus-imaging.co.in/content/000107506.jpg"
26+
style={{
27+
zoom: scale,
28+
}}
29+
alt="scale img"
30+
/>
31+
</div>
32+
);
33+
};
34+
35+
storiesOf('Sensors/usePinchZoom', module)
36+
.add('Docs', () => <ShowDocs md={require('../docs/usePinchZoom.md')} />)
37+
.add('Default', () => <Demo />);

0 commit comments

Comments
 (0)