@deibid/no-hands-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +503 -0
- package/dist/index.mjs +467 -0
- package/package.json +34 -0
- package/src/context/context.tsx +147 -0
- package/src/context/index.ts +1 -0
- package/src/context/types.ts +22 -0
- package/src/detection-bounds-display.tsx +29 -0
- package/src/hooks.ts +239 -0
- package/src/index.ts +24 -0
- package/src/nhcomponent.ts +87 -0
- package/src/pointer.tsx +62 -0
- package/src/types.ts +25 -0
- package/tsconfig.json +17 -0
- package/tsconfig.node.json +11 -0
- package/tsup.config.ts +13 -0
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { Listener, Point } from "@deibid/no-hands";
|
|
3
|
+
import { NHEvent } from "@deibid/no-hands";
|
|
4
|
+
import type { NHEventProps } from "./types";
|
|
5
|
+
import { useNH } from "./context";
|
|
6
|
+
|
|
7
|
+
export function useEvents<T extends HTMLElement>(props: NHEventProps) {
|
|
8
|
+
const ref = useRef<T>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!ref.current) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (props.onNHMouseEnter) {
|
|
16
|
+
ref.current.addEventListener(NHEvent.MOUSE_ENTER, props.onNHMouseEnter);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (props.onNHMouseLeave) {
|
|
20
|
+
ref.current.addEventListener(NHEvent.MOUSE_LEAVE, props.onNHMouseLeave);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (props.onNHClickGestureBegin) {
|
|
24
|
+
ref.current.addEventListener(
|
|
25
|
+
NHEvent.CLICK_GESTURE_BEGIN,
|
|
26
|
+
props.onNHClickGestureBegin,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (props.onNHClickGestureEnd) {
|
|
30
|
+
ref.current.addEventListener(
|
|
31
|
+
NHEvent.CLICK_GESTURE_END,
|
|
32
|
+
props.onNHClickGestureEnd,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
if (!ref.current) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (props.onNHMouseEnter) {
|
|
42
|
+
ref.current.removeEventListener(
|
|
43
|
+
NHEvent.MOUSE_ENTER,
|
|
44
|
+
props.onNHMouseEnter,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (props.onNHMouseLeave) {
|
|
49
|
+
ref.current.removeEventListener(
|
|
50
|
+
NHEvent.MOUSE_LEAVE,
|
|
51
|
+
props.onNHMouseLeave,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (props.onNHClickGestureBegin) {
|
|
56
|
+
ref.current.removeEventListener(
|
|
57
|
+
NHEvent.CLICK_GESTURE_BEGIN,
|
|
58
|
+
props.onNHClickGestureBegin,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (props.onNHClickGestureEnd) {
|
|
63
|
+
ref.current.removeEventListener(
|
|
64
|
+
NHEvent.CLICK_GESTURE_END,
|
|
65
|
+
props.onNHClickGestureEnd,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, [
|
|
70
|
+
props.onNHClickGestureBegin,
|
|
71
|
+
props.onNHClickGestureEnd,
|
|
72
|
+
props.onNHMouseEnter,
|
|
73
|
+
props.onNHMouseLeave
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ref,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useCamera() {
|
|
82
|
+
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
83
|
+
const [loading, setLoading] = useState(true);
|
|
84
|
+
const [error, setError] = useState<Error | null>(null);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
let localStream: MediaStream | null = null;
|
|
88
|
+
let active = true;
|
|
89
|
+
|
|
90
|
+
async function getCamera() {
|
|
91
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
92
|
+
console.warn("device does not support camera");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const s = await navigator.mediaDevices.getUserMedia({
|
|
98
|
+
video: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!active) {
|
|
102
|
+
s.getTracks().forEach((t) => t.stop());
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
localStream = s;
|
|
107
|
+
setStream(s);
|
|
108
|
+
setLoading(false);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("could not initiate camera", error);
|
|
111
|
+
setLoading(false);
|
|
112
|
+
setError(error as Error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getCamera();
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
active = false;
|
|
120
|
+
localStream?.getTracks().forEach((t) => t.stop());
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
stream,
|
|
126
|
+
loading,
|
|
127
|
+
error,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function useNHPoint() {
|
|
132
|
+
const [point, setPoint] = useState<Point | null>(null);
|
|
133
|
+
const { faceTracker } = useNH();
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!faceTracker) return;
|
|
137
|
+
|
|
138
|
+
const subscriptionId = faceTracker.subscribe(setPoint);
|
|
139
|
+
return () => {
|
|
140
|
+
faceTracker.unsubscribe(subscriptionId);
|
|
141
|
+
};
|
|
142
|
+
}, [faceTracker]);
|
|
143
|
+
|
|
144
|
+
return point;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface UseNHEventNotificationProps {
|
|
148
|
+
onNHMouseEnter?: Listener<'nh:mouseenter'>
|
|
149
|
+
onNHMouseLeave?: Listener<'nh:mouseleave'>
|
|
150
|
+
onNHClickGestureBegin?: Listener<'nh:click-gesture-begin'>
|
|
151
|
+
onNHClickGestureEnd?: Listener<'nh:click-gesture-end'>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function useNHEventNotification({
|
|
155
|
+
onNHClickGestureBegin,
|
|
156
|
+
onNHClickGestureEnd,
|
|
157
|
+
onNHMouseEnter,
|
|
158
|
+
onNHMouseLeave
|
|
159
|
+
}: UseNHEventNotificationProps) {
|
|
160
|
+
const { eventDispatcher } = useNH();
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
|
|
164
|
+
const unsubscribeFns: Array<(() => void) | undefined> = [];
|
|
165
|
+
|
|
166
|
+
if (eventDispatcher && onNHMouseEnter) {
|
|
167
|
+
const unsubscribe = eventDispatcher.onEvent(
|
|
168
|
+
NHEvent.MOUSE_ENTER,
|
|
169
|
+
onNHMouseEnter
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
unsubscribeFns.push(unsubscribe);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (eventDispatcher && onNHMouseLeave) {
|
|
176
|
+
const unsubscribe = eventDispatcher.onEvent(
|
|
177
|
+
NHEvent.MOUSE_LEAVE,
|
|
178
|
+
onNHMouseLeave
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
unsubscribeFns.push(unsubscribe);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (eventDispatcher && onNHClickGestureBegin) {
|
|
185
|
+
const unsubscribe = eventDispatcher.onEvent(
|
|
186
|
+
NHEvent.CLICK_GESTURE_BEGIN,
|
|
187
|
+
onNHClickGestureBegin
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
unsubscribeFns.push(unsubscribe);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (eventDispatcher && onNHClickGestureEnd) {
|
|
194
|
+
const unsubscribe = eventDispatcher.onEvent(
|
|
195
|
+
NHEvent.CLICK_GESTURE_END,
|
|
196
|
+
onNHClickGestureEnd
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
unsubscribeFns.push(unsubscribe);
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
unsubscribeFns.forEach(unsubscribe => unsubscribe?.())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
}, [
|
|
208
|
+
eventDispatcher,
|
|
209
|
+
onNHMouseEnter,
|
|
210
|
+
onNHMouseLeave,
|
|
211
|
+
onNHClickGestureBegin,
|
|
212
|
+
onNHClickGestureEnd,
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
export function useWindowSize() {
|
|
218
|
+
|
|
219
|
+
const computeWindowSize = (window: Window) => ({
|
|
220
|
+
width: window.innerWidth,
|
|
221
|
+
height: window.innerHeight,
|
|
222
|
+
centerX: window.innerWidth / 2,
|
|
223
|
+
centerY: window.innerHeight / 2
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const [windowSize, setWindowSize] = useState(computeWindowSize(window));
|
|
227
|
+
|
|
228
|
+
const handleWindowResize = useCallback(() => {
|
|
229
|
+
setWindowSize(computeWindowSize(window))
|
|
230
|
+
}, [computeWindowSize]);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
window.addEventListener('resize', handleWindowResize);
|
|
234
|
+
return () => window.removeEventListener('resize', handleWindowResize);
|
|
235
|
+
}, [handleWindowResize]);
|
|
236
|
+
|
|
237
|
+
return windowSize;
|
|
238
|
+
|
|
239
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NH } from './nhcomponent';
|
|
2
|
+
import { useEvents, useCamera, useNHPoint, useWindowSize } from "./hooks";
|
|
3
|
+
import { NHProvider, useNH } from "./context";
|
|
4
|
+
import DetectionBoundsDisplay from "./detection-bounds-display";
|
|
5
|
+
import Pointer from "./pointer";
|
|
6
|
+
import { NHEvent } from "@deibid/no-hands";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
// context
|
|
11
|
+
NHProvider,
|
|
12
|
+
// hooks
|
|
13
|
+
useCamera,
|
|
14
|
+
useEvents,
|
|
15
|
+
useNH,
|
|
16
|
+
useNHPoint,
|
|
17
|
+
useWindowSize,
|
|
18
|
+
// components
|
|
19
|
+
NH,
|
|
20
|
+
DetectionBoundsDisplay,
|
|
21
|
+
Pointer,
|
|
22
|
+
// re-exports from core package
|
|
23
|
+
NHEvent
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Element,
|
|
4
|
+
HTMLElementType,
|
|
5
|
+
NHComponent,
|
|
6
|
+
NHComponentProps,
|
|
7
|
+
NHNamespace,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { useEvents } from "./hooks";
|
|
10
|
+
import { mergeRefs } from "react-merge-refs";
|
|
11
|
+
|
|
12
|
+
const htmlElementTags: HTMLElementType[] = [
|
|
13
|
+
'a',
|
|
14
|
+
'article',
|
|
15
|
+
'aside',
|
|
16
|
+
'audio',
|
|
17
|
+
'button',
|
|
18
|
+
'canvas',
|
|
19
|
+
'details',
|
|
20
|
+
'dialog',
|
|
21
|
+
'div',
|
|
22
|
+
'figure',
|
|
23
|
+
'footer',
|
|
24
|
+
'form',
|
|
25
|
+
'h1',
|
|
26
|
+
'h2',
|
|
27
|
+
'h3',
|
|
28
|
+
'h4',
|
|
29
|
+
'h5',
|
|
30
|
+
'h6',
|
|
31
|
+
'header',
|
|
32
|
+
'img',
|
|
33
|
+
'input',
|
|
34
|
+
'label',
|
|
35
|
+
'li',
|
|
36
|
+
'main',
|
|
37
|
+
'nav',
|
|
38
|
+
'ol',
|
|
39
|
+
'p',
|
|
40
|
+
'picture',
|
|
41
|
+
'section',
|
|
42
|
+
'select',
|
|
43
|
+
'span',
|
|
44
|
+
'summary',
|
|
45
|
+
'svg',
|
|
46
|
+
'table',
|
|
47
|
+
'td',
|
|
48
|
+
'textarea',
|
|
49
|
+
'th',
|
|
50
|
+
'ul',
|
|
51
|
+
'video',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
function NHComponentFactory<T extends HTMLElementType>(tag: T): NHComponent<T> {
|
|
55
|
+
return forwardRef<Element<T>, NHComponentProps<T>>(
|
|
56
|
+
(props, ref) => {
|
|
57
|
+
const {
|
|
58
|
+
onNHMouseEnter,
|
|
59
|
+
onNHMouseLeave,
|
|
60
|
+
onNHClickGestureBegin,
|
|
61
|
+
onNHClickGestureEnd,
|
|
62
|
+
...htmlProps
|
|
63
|
+
} = props;
|
|
64
|
+
|
|
65
|
+
const { ref: nhRef } = useEvents<Element<T>>({
|
|
66
|
+
onNHMouseEnter,
|
|
67
|
+
onNHMouseLeave,
|
|
68
|
+
onNHClickGestureBegin,
|
|
69
|
+
onNHClickGestureEnd,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return React.createElement(tag, {
|
|
73
|
+
...htmlProps,
|
|
74
|
+
ref: mergeRefs([ref, nhRef]),
|
|
75
|
+
"data-nh-component": "true",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
) as unknown as NHComponent<T>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const NH = htmlElementTags.reduce((result, tag) => {
|
|
82
|
+
const NHComponent = NHComponentFactory(tag);
|
|
83
|
+
|
|
84
|
+
NHComponent.displayName = `NH.${tag}`;
|
|
85
|
+
result[tag] = NHComponent;
|
|
86
|
+
return result;
|
|
87
|
+
}, {} as NHNamespace);
|
package/src/pointer.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { useNHEventNotification, useNHPoint } from "./hooks";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface PointerProps extends React.PropsWithChildren {
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Pointer({
|
|
10
|
+
children,
|
|
11
|
+
style
|
|
12
|
+
}: PointerProps) {
|
|
13
|
+
const point = useNHPoint();
|
|
14
|
+
|
|
15
|
+
const [, setColor] = useState("red");
|
|
16
|
+
|
|
17
|
+
const handleNHClickGestureBegin = useCallback(() => {
|
|
18
|
+
setColor((prevColor) => (prevColor === "red" ? "blue" : "red"));
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const handeNHClickGestureEnd = useCallback(() => {
|
|
22
|
+
setColor('green');
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
useNHEventNotification({
|
|
27
|
+
onNHClickGestureEnd: handeNHClickGestureEnd,
|
|
28
|
+
onNHClickGestureBegin: handleNHClickGestureBegin,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
style={{
|
|
34
|
+
pointerEvents: "none",
|
|
35
|
+
position: "absolute",
|
|
36
|
+
...(point === null && { visibility: 'hidden' }),
|
|
37
|
+
...(point !== null && { left: point.x, top: point.y }),
|
|
38
|
+
...style,
|
|
39
|
+
// backgroundColor: color,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{children ?? <DefaultPointerIndicator />}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
function DefaultPointerIndicator() {
|
|
49
|
+
|
|
50
|
+
return <div
|
|
51
|
+
style={{
|
|
52
|
+
height: 24,
|
|
53
|
+
width: 24,
|
|
54
|
+
borderRadius: '50%',
|
|
55
|
+
transform: 'translate(-50%, -50%)',
|
|
56
|
+
boxShadow: 'rgba(0, 0, 0, 0.24) 0px 3px 8px',
|
|
57
|
+
background: 'black',
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface NHEventProps {
|
|
4
|
+
onNHMouseEnter?: EventListener;
|
|
5
|
+
onNHMouseLeave?: EventListener;
|
|
6
|
+
onNHClickGestureBegin?: EventListener;
|
|
7
|
+
onNHClickGestureEnd?: EventListener;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type HTMLElementType = keyof React.JSX.IntrinsicElements;
|
|
11
|
+
export type Element<T extends HTMLElementType> =
|
|
12
|
+
React.ComponentRef<T> extends HTMLElement
|
|
13
|
+
? React.ComponentRef<T>
|
|
14
|
+
: HTMLElement;
|
|
15
|
+
|
|
16
|
+
export type NHComponentProps<T extends HTMLElementType> =
|
|
17
|
+
React.ComponentPropsWithoutRef<T> & NHEventProps;
|
|
18
|
+
|
|
19
|
+
export type NHComponent<T extends HTMLElementType> = React.FC<
|
|
20
|
+
NHComponentProps<T>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export type NHNamespace = {
|
|
24
|
+
[K in HTMLElementType]: NHComponent<K>;
|
|
25
|
+
};
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED