Skip to content

Commit

Permalink
added usePinchZoom sensor hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhishek Mittal authored and Abhishek Mittal committed May 4, 2022
1 parent 3685b75 commit af8d46e
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
- [`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)
- [`createBreakpoint`](./docs/createBreakpoint.md) — tracks `innerWidth`
- [`useScrollbarWidth`](./docs/useScrollbarWidth.md) — detects browser's native scrollbars width. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescrollbarwidth--demo)
- [`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)
<br/>
<br/>
- [**UI**](./docs/UI.md)
Expand Down
36 changes: 36 additions & 0 deletions docs/usePinchZoom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `usePinchZoon`

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.

## Usage

```jsx
import { usePinchZoon } from "react-use";

const Demo = () => {
const [scale, setState] = useState(1);
const scaleRef = useRef();
const { zoomingState, pinchState } = usePinchZoom(scaleRef);

useEffect(() => {
if (zoomingState === "ZOOM_IN") {
// perform zoom in scaling
setState(scale + 0.1)
} else if (zoomingState === "ZOOM_OUT") {
// perform zoom out in scaling
setState(scale - 0.1)
}
}, [zoomingState]);

return (
<div ref={scaleRef}>
<img
src="https://www.olympus-imaging.co.in/content/000107506.jpg"
style={{
zoom: scale,
}}
/>
</div>
);
};
```
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export { useMultiStateValidator } from './useMultiStateValidator';
export { default as useWindowScroll } from './useWindowScroll';
export { default as useWindowSize } from './useWindowSize';
export { default as useMeasure } from './useMeasure';
export { default as usePinchZoom } from './usePinchZoom';
export { useRendersCount } from './useRendersCount';
export { useFirstMountState } from './useFirstMountState';
export { default as useSet } from './useSet';
Expand Down
109 changes: 109 additions & 0 deletions src/usePinchZoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { RefObject, useEffect, useMemo, useState } from 'react';

export type CacheRef = {
prevDiff: number;
evCache: Array<PointerEvent>;
};

export enum ZoomState {
'ZOOMING_IN' = 'ZOOMING_IN',
'ZOOMING_OUT' = 'ZOOMING_OUT',
}

export type ZoomStateType = ZoomState.ZOOMING_IN | ZoomState.ZOOMING_OUT;

const usePinchZoom = (ref: RefObject<HTMLElement>) => {
const cacheRef = useMemo<CacheRef>(
() => ({
evCache: [],
prevDiff: -1,
}),
[ref.current]
);

const [zoomingState, setZoomingState] = useState<[ZoomStateType, number]>();

const pointermove_handler = (ev: PointerEvent) => {
// This function implements a 2-pointer horizontal pinch/zoom gesture.
//
// If the distance between the two pointers has increased (zoom in),
// the target element's background is changed to 'pink' and if the
// distance is decreasing (zoom out), the color is changed to 'lightblue'.
//
// This function sets the target element's border to 'dashed' to visually
// indicate the pointer's target received a move event.
// Find this event in the cache and update its record with this event
for (let i = 0; i < cacheRef.evCache.length; i++) {
if (ev.pointerId == cacheRef.evCache[i].pointerId) {
cacheRef.evCache[i] = ev;
break;
}
}

// If two pointers are down, check for pinch gestures
if (cacheRef.evCache.length == 2) {
// console.log(prevDiff)
// Calculate the distance between the two pointers
const curDiff = Math.abs(cacheRef.evCache[0].clientX - cacheRef.evCache[1].clientX);

if (cacheRef.prevDiff > 0) {
if (curDiff > cacheRef.prevDiff) {
// The distance between the two pointers has increased
setZoomingState([ZoomState.ZOOMING_IN, curDiff]);
}
if (curDiff < cacheRef.prevDiff) {
// The distance between the two pointers has decreased
setZoomingState([ZoomState.ZOOMING_OUT, curDiff]);
}
}

// Cache the distance for the next move event
cacheRef.prevDiff = curDiff;
}
};

const pointerdown_handler = (ev: PointerEvent) => {
// The pointerdown event signals the start of a touch interaction.
// This event is cached to support 2-finger gestures
cacheRef.evCache.push(ev);
// console.log('pointerDown', ev);
};

const pointerup_handler = (ev: PointerEvent) => {
// Remove this pointer from the cache and reset the target's
// background and border
remove_event(ev);

// If the number of pointers down is less than two then reset diff tracker
if (cacheRef.evCache.length < 2) {
cacheRef.prevDiff = -1;
}
};

const remove_event = (ev: PointerEvent) => {
// Remove this event from the target's cache
for (let i = 0; i < cacheRef.evCache.length; i++) {
if (cacheRef.evCache[i].pointerId == ev.pointerId) {
cacheRef.evCache.splice(i, 1);
break;
}
}
};

useEffect(() => {
if (ref?.current) {
ref.current.onpointerdown = pointerdown_handler;
ref.current.onpointermove = pointermove_handler;
ref.current.onpointerup = pointerup_handler;
ref.current.onpointercancel = pointerup_handler;
ref.current.onpointerout = pointerup_handler;
ref.current.onpointerleave = pointerup_handler;
}
}, [ref?.current]);

return zoomingState
? { zoomingState: zoomingState[0], pinchState: zoomingState[1] }
: { zoomingState: null, pinchState: 0 };
};

export default usePinchZoom;
37 changes: 37 additions & 0 deletions stories/usePinchZoom.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { storiesOf } from '@storybook/react';
import { useEffect, useRef, useState } from 'react';
import { usePinchZoom } from '../src';
import { ZoomState } from '../src/usePinchZoom';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [scale, setState] = useState(1);
const scaleRef = useRef();
const { zoomingState, pinchState } = usePinchZoom(scaleRef);

useEffect(() => {
if (zoomingState === ZoomState.ZOOMING_IN) {
// perform zoom in scaling
setState(scale + 0.1);
} else if (zoomingState === ZoomState.ZOOMING_OUT) {
// perform zoom out in scaling
setState(scale - 0.1);
}
}, [zoomingState, pinchState]);

return (
<div ref={scaleRef}>
<img
src="https://www.olympus-imaging.co.in/content/000107506.jpg"
style={{
zoom: scale,
}}
alt="scale img"
/>
</div>
);
};

storiesOf('Sensors/usePinchZoom', module)
.add('Docs', () => <ShowDocs md={require('../docs/usePinchZoom.md')} />)
.add('Default', () => <Demo />);

0 comments on commit af8d46e

Please sign in to comment.