@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/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);
@@ -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
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "jsx": "react-jsx"
8
+ },
9
+ "include": [
10
+ "src"
11
+ ],
12
+ "references": [
13
+ {
14
+ "path": "../core"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler"
7
+ },
8
+ "include": [
9
+ "tsup.config.ts"
10
+ ]
11
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: {
7
+ compilerOptions: {
8
+ composite: false,
9
+ },
10
+ },
11
+ clean: true,
12
+ external: ["react"],
13
+ });