Skip to content

Commit 66d5770

Browse files
committed
[IMPL] usePointerLock hook
1 parent 47b7fc9 commit 66d5770

11 files changed

Lines changed: 295 additions & 8 deletions

File tree

apps/react-tools-demo/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/react.svg" />
5+
<link rel="icon" type="image/svg+xml" href="/react-red.webp" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>React tools</title>
88
</head>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
2+
import { usePointerLock } from "../../../../../../packages/react-tools/src";
3+
4+
const canvasDraw = (x: number, y: number, canvas: HTMLCanvasElement) => {
5+
const ctx = canvas.getContext("2d");
6+
7+
function find2ndCenter(pos: number, max: number) {
8+
if (pos < 20) {
9+
pos += max;
10+
}
11+
else if (pos + 20 > max) {
12+
pos -= max;
13+
}
14+
else {
15+
pos = 0;
16+
}
17+
return pos;
18+
}
19+
20+
function drawBall(x: number, y: number) {
21+
ctx!.beginPath();
22+
ctx!.arc(x, y, 20, 0, 2 * Math.PI, true);
23+
ctx!.fill();
24+
}
25+
26+
const x2 = find2ndCenter(x, canvas.width);
27+
const y2 = find2ndCenter(y, canvas.height);
28+
29+
ctx!.fillStyle = "black";
30+
ctx!.fillRect(0, 0, canvas.width, canvas.height);
31+
ctx!.fillStyle = "#f00";
32+
33+
drawBall(x, y); // main ball
34+
if (x2) {
35+
drawBall(x2, y); // partial ball
36+
}
37+
if (y2) {
38+
drawBall(x, y2); // partial ball
39+
}
40+
if (x2 && y2) {
41+
drawBall(x2, y2); // partial ball
42+
}
43+
};
44+
/**
45+
The component uses _usePointerLock_ hook to acquire pointer lock. Clicking on canvas area and moving mouse you will directly control the ball inside the canvas. Pressing escape you return to expected state.
46+
*/
47+
export const UsePointerLock = () => {
48+
const [position, setPosition] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
49+
const canvasRef = useRef<HTMLCanvasElement>(null);
50+
51+
const mousemove = useRef((e: MouseEvent) => {
52+
function updateCoord(delta: {x: number, y: number}, max: {x: number, y: number}) {
53+
setPosition(t => {
54+
let x = t.x + delta.x;
55+
let y = t.y + delta.y;
56+
x %= max.x;
57+
y %= max.y;
58+
if (x < 0) {
59+
x += max.x;
60+
}
61+
if (y < 0) {
62+
y += max.y;
63+
}
64+
canvasDraw(x, y, canvasRef.current!);
65+
return { x, y };
66+
});
67+
}
68+
updateCoord({x:e.movementX, y: e.movementY}, {x:canvasRef.current!.width, y:canvasRef.current!.height});
69+
});
70+
71+
const onError = useCallback(() => alert("PointerLock API not supported."), []);
72+
73+
const onLock = useCallback(() => {
74+
canvasRef.current!.addEventListener('mousemove', mousemove.current);
75+
}, []);
76+
77+
const onUnlock = useCallback(() => canvasRef.current!.removeEventListener("mousemove", mousemove.current), []);
78+
79+
const { lock } = usePointerLock({
80+
target: canvasRef,
81+
onError,
82+
onLock,
83+
onUnlock
84+
});
85+
86+
useLayoutEffect(() => {
87+
canvasRef.current && canvasDraw(20, 20, canvasRef.current);
88+
}, []);
89+
90+
return (<>
91+
<div>
92+
{
93+
position && <p>Position X:{position.x} - Y:{position.y}</p>
94+
}
95+
</div>
96+
<canvas ref={canvasRef} width="640" height="360" onClick={lock}>
97+
Your browser does not support HTML5 canvas
98+
</canvas>
99+
</>);
100+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ export const COMPONENTS = [
6868
"useDoubleClick",
6969
"useScreen",
7070
"useHotKeys",
71-
"usePinchZoom"
71+
"usePinchZoom",
72+
"usePointerLock"
7273
],
7374
//API DOM
7475
[
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# usePointerLock
2+
Hook to use [PointerLock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UsePointerLock = () => {
8+
const [position, setPosition] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
9+
const canvasRef = useRef<HTMLCanvasElement>(null);
10+
11+
const mousemove = useRef((e: MouseEvent) => {
12+
function updateCoord(delta: {x: number, y: number}, max: {x: number, y: number}) {
13+
setPosition(t => {
14+
let x = t.x + delta.x;
15+
let y = t.y + delta.y;
16+
x %= max.x;
17+
y %= max.y;
18+
if (x < 0) {
19+
x += max.x;
20+
}
21+
if (y < 0) {
22+
y += max.y;
23+
}
24+
canvasDraw(x, y, canvasRef.current!);
25+
return { x, y };
26+
});
27+
}
28+
updateCoord({x:e.movementX, y: e.movementY}, {x:canvasRef.current!.width, y:canvasRef.current!.height});
29+
});
30+
31+
const onError = useCallback(() => alert("PointerLock API not supported."), []);
32+
33+
const onLock = useCallback(() => {
34+
canvasRef.current!.addEventListener('mousemove', mousemove.current);
35+
}, []);
36+
37+
const onUnlock = useCallback(() => canvasRef.current!.removeEventListener("mousemove", mousemove.current), []);
38+
39+
const { lock } = usePointerLock({
40+
target: canvasRef,
41+
onError,
42+
onLock,
43+
onUnlock
44+
});
45+
46+
useLayoutEffect(() => {
47+
canvasRef.current && canvasDraw(20, 20, canvasRef.current);
48+
}, []);
49+
50+
return (<>
51+
<div>
52+
{
53+
position && <p>Position X:{position.x} - Y:{position.y}</p>
54+
}
55+
</div>
56+
<canvas ref={canvasRef} width="640" height="360" onClick={lock}>
57+
Your browser does not support HTML5 canvas
58+
</canvas>
59+
</>);
60+
}
61+
```
62+
63+
> The component uses _usePointerLock_ hook to acquire pointer lock. Clicking on canvas area and moving mouse you will directly control the ball inside the canvas. Pressing escape you return to expected state.
64+
65+
66+
## API
67+
68+
```tsx
69+
usePointerLock<T extends HTMLElement>({ target, unadjustedMovement, onLock, onUnlock, onError }: UsePointerLockProps<T>): UsePointerLockResult
70+
```
71+
72+
> ### Params
73+
>
74+
> - __param__: _UsePointerLockProps_
75+
object
76+
> - __param.target__: _RefObject<T>|T_
77+
element that requires lock.
78+
> - __param.unadjustedMovement?__: _boolean_
79+
Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead. The default value is false; setting it to true will disable mouse acceleration.
80+
> - __param.onError__: _(e: unknown)=>void_
81+
function that will be executed when an error throwing during request.
82+
> - __param.onLock?__: _(target: RefObject<T>|T) => void_
83+
function that will be executed when lock has been acquired.
84+
> - __param.onUnlock?__: _() => void_
85+
function that will be executed when lock has been released.
86+
>
87+
88+
> ### Returns
89+
>
90+
> __result__: _UsePointerLockResult_
91+
> Object with two properties:
92+
> - __lock__: function to acquire lock.
93+
> - __unlock__: function to release lock.
94+
>

packages/react-tools/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
- [x] useScreen (orientation and ecc)
6969
- [x] useHotKeys
7070
- [x] usePinchZoom
71+
- [ ] usePointerLock
72+
- [ ] useContextMenu
7173
- [ ] useInfiniteScroll
7274
- [ ] useDragAndDrop (check for mobile usage)
7375

@@ -102,7 +104,6 @@
102104
- [x] useSpeechRecognition
103105
- [x] useSpeechSynthesis
104106
- [x] useFPS
105-
- [ ] usePointerLock (https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API)
106107
- [ ] usePIP (https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API)
107108
- [ ] useIdleDetection (https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API)
108109
- [ ] usePopover (https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,5 @@ export { useBluetooth } from './useBluetooth';
8080
export { useScreenWakeLock } from './useScreenWakeLock';
8181
export { useSpeechRecognition } from './useSpeechRecognition';
8282
export { useSpeechSynthesis } from './useSpeechSynthesis';
83-
export { useFPS } from './useFPS';
83+
export { useFPS } from './useFPS';
84+
export { usePointerLock } from './usePointerLock';

packages/react-tools/src/hooks/useFPS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const useFPS = ({ everySeconds, windowSize }:UseFPSProps={windowSize:10,
3636
fps = fps.slice(Math.max(state.current.fps.length - windowSize, 0));
3737
const avg = +(fps.reduce((a, b) => a + b, 0) / fps.length).toFixed(2);
3838
const maxFps = Math.max(currentFps, state.current.maxFps);
39-
currentFps = fps.at(-1) as number;
39+
currentFps = fps[fps.length-1] as number;
4040
if (fps.some((el, index) => state.current.fps[index] !== el) || avg !== state.current.avg || maxFps !== state.current.maxFps || currentFps !== state.current.currentFps) {
4141
state.current.fps = fps;
4242
state.current.currentFps = currentFps;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { RefObject, useCallback, useRef } from "react";
2+
import { UsePointerLockProps, UsePointerLockResult } from "../models";
3+
4+
/**
5+
* **`usePointerLock`**: Hook to use [PointerLock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API).
6+
* @param {UsePointerLockProps} param - object
7+
* @param {RefObject<T>|T} param.target - element that requires lock.
8+
* @param {boolean} [param.unadjustedMovement] - Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead. The default value is false; setting it to true will disable mouse acceleration.
9+
* @param {(e: unknown)=>void} param.onError - function that will be executed when an error throwing during request.
10+
* @param {(target: RefObject<T>|T) => void} [param.onLock] - function that will be executed when lock has been acquired.
11+
* @param {() => void} [param.onUnlock] - function that will be executed when lock has been released.
12+
* @returns {UsePointerLockResult} result
13+
* Object with two properties:
14+
* - __lock__: function to acquire lock.
15+
* - __unlock__: function to release lock.
16+
*/
17+
export const usePointerLock = <T extends HTMLElement>({ target, unadjustedMovement, onLock, onUnlock, onError }: UsePointerLockProps<T>): UsePointerLockResult => {
18+
const locked = useRef(false);
19+
20+
const onLockChange = useCallback(() => {
21+
if (document.pointerLockElement) {
22+
onLock && onLock(document.pointerLockElement as T)
23+
} else {
24+
locked.current = false;
25+
document.removeEventListener("pointerlockerror", onError, false);
26+
document.removeEventListener("pointerlockchange", onLockChange);
27+
onUnlock && onUnlock(((target as RefObject<T>)?.current
28+
? (target as RefObject<T>).current
29+
: target as T)!);
30+
}
31+
}, [onError, onLock, onUnlock, target]);
32+
33+
const unlock = useCallback(async () => {
34+
locked.current = false;
35+
await document.exitPointerLock();
36+
onUnlock && onUnlock(((target as RefObject<T>)?.current
37+
? (target as RefObject<T>).current
38+
: target as T)!);
39+
}, [target, onUnlock]);
40+
41+
const lock = useCallback(() => {
42+
const element = (target as RefObject<T>)?.current
43+
? (target as RefObject<T>).current
44+
: target as T;
45+
return new Promise<void>((res) => {
46+
if (!element || locked.current) {
47+
return res();
48+
}
49+
50+
document.addEventListener("pointerlockerror", onError, false);
51+
document.addEventListener("pointerlockchange", onLockChange);
52+
return (element as unknown as { requestPointerLock: (opt: { unadjustedMovement: boolean }) => Promise<void> }).requestPointerLock({
53+
unadjustedMovement: !!unadjustedMovement,
54+
})
55+
.then(() => {
56+
locked.current = true;
57+
})
58+
.catch(err => {
59+
onError(err);
60+
})
61+
.finally(() => {
62+
res();
63+
});
64+
})
65+
}, [target, onError, unadjustedMovement, onLockChange]);
66+
67+
return { lock, unlock };
68+
}

packages/react-tools/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export type {
4242
UseSpeechSynthesisProps,
4343
SpeechSynthesisSpeakParam,
4444
UseFPSProps,
45-
UseFPSResult
45+
UseFPSResult,
46+
UsePointerLockProps,
47+
UsePointerLockResult
4648
} from './models'
4749

4850
export {
@@ -128,7 +130,8 @@ export {
128130
useScreenWakeLock,
129131
useSpeechRecognition,
130132
useSpeechSynthesis,
131-
useFPS
133+
useFPS,
134+
usePointerLock
132135
} from './hooks'
133136

134137
export {

packages/react-tools/src/models/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export type { DeviceOrientationProps } from './useDeviceOrientation.model';
1111
export type { Bluetooth, BluetoothCharacteristicProperties, BluetoothCharacteristicUUID, BluetoothDescriptorUUID, BluetoothDevice, BluetoothDevicesOptions, BluetoothRemoteGATTCharacteristic, BluetoothRemoteGATTDescriptor, BluetoothRemoteGATTServer, BluetoothRemoteGATTService, BluetoothScanFilters, BluetoothServiceUUID } from './useBluetooth.model';
1212
export type { UseSpeechRecognitionProps, SpeechRecognition, SpeechRecognitionConfig, SpeechRecognitionState, SpeechGrammar, SpeechGrammarList, SpeechRecognitionErrorCode, SpeechRecognitionErrorEvent, SpeechRecognitionEvent } from './useSpeechRecognition.model';
1313
export type { SpeechSynthesisSpeakParam, UseSpeechSynthesis, UseSpeechSynthesisProps } from './useSpeechSynthesis.model';
14-
export type { UseFPSProps, UseFPSResult} from './useFPS.model';
14+
export type { UseFPSProps, UseFPSResult } from './useFPS.model';
15+
export type { UsePointerLockProps, UsePointerLockResult } from './usePointerLock.model';

0 commit comments

Comments
 (0)