@react-three/drei 9.73.4 → 9.74.0
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/README.md +137 -28
- package/core/FaceControls.cjs.js +1 -0
- package/core/FaceControls.d.ts +42 -0
- package/core/FaceControls.js +315 -0
- package/core/FaceLandmarker.cjs.js +1 -0
- package/core/FaceLandmarker.d.ts +14 -0
- package/core/FaceLandmarker.js +43 -0
- package/core/Facemesh.cjs.js +1 -1
- package/core/Facemesh.d.ts +113 -4
- package/core/Facemesh.js +2472 -47
- package/core/index.cjs.js +1 -1
- package/core/index.d.ts +2 -0
- package/core/index.js +6 -1
- package/index.cjs.js +1 -1
- package/index.js +3 -1
- package/native/index.cjs.js +1 -1
- package/native/index.js +6 -1
- package/package.json +2 -1
- package/web/index.cjs.js +1 -1
- package/web/index.js +5 -1
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
|
|
|
52
52
|
<li><a href="#scrollcontrols">ScrollControls</a></li>
|
|
53
53
|
<li><a href="#presentationcontrols">PresentationControls</a></li>
|
|
54
54
|
<li><a href="#keyboardcontrols">KeyboardControls</a></li>
|
|
55
|
+
<li><a href="#FaceControls">FaceControls</a></li>
|
|
55
56
|
</ul>
|
|
56
57
|
<li><a href="#gizmos">Gizmos</a></li>
|
|
57
58
|
<ul>
|
|
@@ -120,6 +121,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
|
|
|
120
121
|
<li><a href="#useboxprojectedenv">useBoxProjectedEnv</a></li>
|
|
121
122
|
<li><a href="#useTrail">useTrail</a></li>
|
|
122
123
|
<li><a href="#useSurfaceSampler">useSurfaceSampler</a></li>
|
|
124
|
+
<li><a href="#facelandmarker">FaceLandmarker</a></li>
|
|
123
125
|
</ul>
|
|
124
126
|
<li><a href="#loading">Loaders</a></li>
|
|
125
127
|
<ul>
|
|
@@ -359,7 +361,7 @@ If available controls have damping enabled by default, they manage their own upd
|
|
|
359
361
|
const controls = useThree((state) => state.controls)
|
|
360
362
|
```
|
|
361
363
|
|
|
362
|
-
Drei currently exports OrbitControls [](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story)
|
|
364
|
+
Drei currently exports OrbitControls [](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) CameraControls [](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story) and FaceControls [](https://drei.vercel.app/?path=/story/controls-facecontrols)
|
|
363
365
|
|
|
364
366
|
All controls react to the default camera. If you have a `<PerspectiveCamera makeDefault />` in your scene, they will control it. If you need to inject an imperative camera or one that isn't the default, use the `camera` prop: `<OrbitControls camera={MyCamera} />`.
|
|
365
367
|
|
|
@@ -596,6 +598,82 @@ function Foo() {
|
|
|
596
598
|
}
|
|
597
599
|
```
|
|
598
600
|
|
|
601
|
+
#### FaceControls
|
|
602
|
+
|
|
603
|
+
The camera follows your face.
|
|
604
|
+
|
|
605
|
+
<p>
|
|
606
|
+
<a href="https://codesandbox.io/s/bf01sb"><img width="20%" src="https://github.com/abernier/abernier/assets/76580/2138ef30-48f3-4ae9-b2bb-4f600de0a35e" alt="demo"/></a>
|
|
607
|
+
</p>
|
|
608
|
+
|
|
609
|
+
Pre-requisite: wrap into a `FaceLandmarker` provider
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
<FaceLandmarker>...</FaceLandmarker>
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
<FaceControls />
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
type FaceControlsProps = {
|
|
621
|
+
/** The camera to be controlled, default: global state camera */
|
|
622
|
+
camera?: THREE.Camera
|
|
623
|
+
/** Whether to autostart the webcam, default: true */
|
|
624
|
+
autostart?: boolean
|
|
625
|
+
/** Enable/disable the webcam, default: true */
|
|
626
|
+
webcam?: boolean
|
|
627
|
+
/** A custom video URL or mediaStream, default: undefined */
|
|
628
|
+
webcamVideoTextureSrc?: VideoTextureSrc
|
|
629
|
+
/** Disable the rAF camera position/rotation update, default: false */
|
|
630
|
+
manualUpdate?: boolean
|
|
631
|
+
/** Disable the rVFC face-detection, default: false */
|
|
632
|
+
manualDetect?: boolean
|
|
633
|
+
/** Callback function to call on "videoFrame" event, default: undefined */
|
|
634
|
+
onVideoFrame?: (e: THREE.Event) => void
|
|
635
|
+
/** Reference this FaceControls instance as state's `controls` */
|
|
636
|
+
makeDefault?: boolean
|
|
637
|
+
/** Approximate time to reach the target. A smaller value will reach the target faster. */
|
|
638
|
+
smoothTime?: number
|
|
639
|
+
/** Apply position offset extracted from `facialTransformationMatrix` */
|
|
640
|
+
offset?: boolean
|
|
641
|
+
/** Offset sensitivity factor, less is more sensible, default: 80 */
|
|
642
|
+
offsetScalar?: number
|
|
643
|
+
/** Enable eye-tracking */
|
|
644
|
+
eyes?: boolean
|
|
645
|
+
/** Force Facemesh's `origin` to be the middle of the 2 eyes, default: true */
|
|
646
|
+
eyesAsOrigin?: boolean
|
|
647
|
+
/** Constant depth of the Facemesh, default: .15 */
|
|
648
|
+
depth?: number
|
|
649
|
+
/** Enable debug mode, default: false */
|
|
650
|
+
debug?: boolean
|
|
651
|
+
/** Facemesh options, default: undefined */
|
|
652
|
+
facemesh?: FacemeshProps
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
type FaceControlsApi = THREE.EventDispatcher & {
|
|
658
|
+
/** Detect faces from the video */
|
|
659
|
+
detect: (video: HTMLVideoElement, time: number) => void
|
|
660
|
+
/** Compute the target for the camera */
|
|
661
|
+
computeTarget: () => THREE.Object3D
|
|
662
|
+
/** Update camera's position/rotation to the `target` */
|
|
663
|
+
update: (delta: number, target?: THREE.Object3D) => void
|
|
664
|
+
/** <Facemesh> ref api */
|
|
665
|
+
facemeshApiRef: RefObject<FacemeshApi>
|
|
666
|
+
/** <Webcam> ref api */
|
|
667
|
+
webcamApiRef: RefObject<WebcamApi>
|
|
668
|
+
/** Play the video */
|
|
669
|
+
play: () => void
|
|
670
|
+
/** Pause the video */
|
|
671
|
+
pause: () => void
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
> **Note** <br>`FaceControls` uses [`requestVideoFrameCallback`](https://caniuse.com/mdn-api_htmlvideoelement_requestvideoframecallback), you may need [a polyfill](https://github.com/ThaUnknown/rvfc-polyfill) (for Firefox).
|
|
676
|
+
|
|
599
677
|
# Gizmos
|
|
600
678
|
|
|
601
679
|
#### GizmoHelper
|
|
@@ -938,42 +1016,59 @@ Renders a THREE.Line2 using THREE.CatmullRomCurve3 for interpolation.
|
|
|
938
1016
|
|
|
939
1017
|
[](https://drei.vercel.app/?path=/story/shapes-facemesh--facemesh-st)
|
|
940
1018
|
|
|
941
|
-
Renders an oriented [MediaPipe
|
|
942
|
-
|
|
943
|
-
```jsx
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1019
|
+
Renders an oriented [MediaPipe face mesh](https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results):
|
|
1020
|
+
|
|
1021
|
+
```jsx
|
|
1022
|
+
const faceLandmarkerResult = {
|
|
1023
|
+
"faceLandmarks": [
|
|
1024
|
+
[
|
|
1025
|
+
{ "x": 0.5760777592658997, "y": 0.8639070391654968, "z": -0.030997956171631813 },
|
|
1026
|
+
{ "x": 0.572094738483429, "y": 0.7886289358139038, "z": -0.07189624011516571 },
|
|
1027
|
+
// ...
|
|
1028
|
+
],
|
|
1029
|
+
// ...
|
|
1030
|
+
],
|
|
1031
|
+
"faceBlendshapes": [
|
|
1032
|
+
// ...
|
|
1033
|
+
],
|
|
1034
|
+
"facialTransformationMatrixes": [
|
|
1035
|
+
// ...
|
|
1036
|
+
]
|
|
957
1037
|
},
|
|
958
1038
|
}
|
|
1039
|
+
const points = faceLandmarkerResult.faceLandmarks[0]
|
|
959
1040
|
|
|
960
|
-
<Facemesh
|
|
1041
|
+
<Facemesh points={points} />
|
|
961
1042
|
```
|
|
962
1043
|
|
|
963
1044
|
```tsx
|
|
964
|
-
type FacemeshProps = {
|
|
965
|
-
/**
|
|
1045
|
+
export type FacemeshProps = {
|
|
1046
|
+
/** an array of 468+ keypoints as returned by google/mediapipe tasks-vision, default: a sample face */
|
|
1047
|
+
points?: MediaPipePoints
|
|
1048
|
+
/** @deprecated an face object as returned by tensorflow/tfjs-models face-landmarks-detection */
|
|
966
1049
|
face?: MediaPipeFaceMesh
|
|
967
|
-
/** width of the mesh, default: undefined */
|
|
1050
|
+
/** constant width of the mesh, default: undefined */
|
|
968
1051
|
width?: number
|
|
969
|
-
/** or height of the mesh, default: undefined */
|
|
1052
|
+
/** or constant height of the mesh, default: undefined */
|
|
970
1053
|
height?: number
|
|
971
|
-
/** or depth of the mesh, default: 1 */
|
|
1054
|
+
/** or constant depth of the mesh, default: 1 */
|
|
972
1055
|
depth?: number
|
|
973
1056
|
/** a landmarks tri supposed to be vertical, default: [159, 386, 200] (see: https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection#mediapipe-facemesh-keypoints) */
|
|
974
1057
|
verticalTri?: [number, number, number]
|
|
975
|
-
/** a landmark index to be the origin of the mesh. default: undefined (ie. the bbox center) */
|
|
976
|
-
origin?: number
|
|
1058
|
+
/** a landmark index (to get the position from) or a vec3 to be the origin of the mesh. default: undefined (ie. the bbox center) */
|
|
1059
|
+
origin?: number | THREE.Vector3
|
|
1060
|
+
/** A facial transformation matrix, as returned by FaceLandmarkerResult.facialTransformationMatrixes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */
|
|
1061
|
+
facialTransformationMatrix?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes[0]
|
|
1062
|
+
/** Apply position offset extracted from `facialTransformationMatrix` */
|
|
1063
|
+
offset?: boolean
|
|
1064
|
+
/** Offset sensitivity factor, less is more sensible */
|
|
1065
|
+
offsetScalar?: number
|
|
1066
|
+
/** Fface blendshapes, as returned by FaceLandmarkerResult.faceBlendshapes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */
|
|
1067
|
+
faceBlendshapes?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes[0]
|
|
1068
|
+
/** whether to enable eyes (nb. `faceBlendshapes` is required for), default: true */
|
|
1069
|
+
eyes?: boolean
|
|
1070
|
+
/** Force `origin` to be the middle of the 2 eyes (nb. `eyes` is required for), default: false */
|
|
1071
|
+
eyesAsOrigin?: boolean
|
|
977
1072
|
/** debug mode, default: false */
|
|
978
1073
|
debug?: boolean
|
|
979
1074
|
}
|
|
@@ -984,20 +1079,28 @@ Ref-api:
|
|
|
984
1079
|
```tsx
|
|
985
1080
|
const api = useRef<FacemeshApi>()
|
|
986
1081
|
|
|
987
|
-
<Facemesh ref={api}
|
|
1082
|
+
<Facemesh ref={api} points={points} />
|
|
988
1083
|
```
|
|
989
1084
|
|
|
990
1085
|
```tsx
|
|
991
1086
|
type FacemeshApi = {
|
|
992
1087
|
meshRef: React.RefObject<THREE.Mesh>
|
|
993
1088
|
outerRef: React.RefObject<THREE.Group>
|
|
1089
|
+
eyeRightRef: React.RefObject<FacemeshEyeApi>
|
|
1090
|
+
eyeLeftRef: React.RefObject<FacemeshEyeApi>
|
|
994
1091
|
}
|
|
995
1092
|
```
|
|
996
1093
|
|
|
997
|
-
|
|
1094
|
+
You can for example get face mesh world direction:
|
|
998
1095
|
|
|
999
1096
|
```tsx
|
|
1000
|
-
meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
|
|
1097
|
+
api.meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
or get L/R iris direction:
|
|
1101
|
+
|
|
1102
|
+
```tsx
|
|
1103
|
+
api.eyeRightRef.current.irisDirRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
|
|
1001
1104
|
```
|
|
1002
1105
|
|
|
1003
1106
|
# Abstractions
|
|
@@ -2432,6 +2535,12 @@ const buffer = useSurfaceSampler(
|
|
|
2432
2535
|
)
|
|
2433
2536
|
```
|
|
2434
2537
|
|
|
2538
|
+
### FaceLandmarker
|
|
2539
|
+
|
|
2540
|
+

|
|
2541
|
+
|
|
2542
|
+
A @mediapipe/tasks-vision [`FaceLandmarker`](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.facelandmarker) provider, as well as a `useFaceLandmarker` hook.
|
|
2543
|
+
|
|
2435
2544
|
# Loading
|
|
2436
2545
|
|
|
2437
2546
|
#### Loader
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@babel/runtime/helpers/extends"),t=require("three"),r=require("react"),a=require("@react-three/fiber"),n=require("maath"),o=require("suspend-react"),c=require("./useVideoTexture.cjs.js"),s=require("./Facemesh.cjs.js"),u=require("./FaceLandmarker.cjs.js");function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}function l(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var a=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,a.get?a:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}require("three/src/math/MathUtils"),require("./Line.cjs.js"),require("three-stdlib"),require("@mediapipe/tasks-vision");var d=i(e),f=l(t),p=l(r);function m(e,t){return e.clone().add(t).multiplyScalar(.5)}function v(e,t,r){const a=e.localToWorld(t);return r.worldToLocal(a)}const b=r.createContext({}),h=r.forwardRef((({camera:e,autostart:t=!0,webcam:o=!0,webcamVideoTextureSrc:c,manualUpdate:i=!1,manualDetect:l=!1,onVideoFrame:h,smoothTime:y=.25,offset:x=!0,offsetScalar:T=80,eyes:E=!1,eyesAsOrigin:j=!0,depth:k=.15,debug:V=!1,facemesh:g,makeDefault:F},R)=>{var q,S;const O=a.useThree((e=>e.scene)),C=a.useThree((e=>e.camera)),M=a.useThree((e=>e.set)),D=a.useThree((e=>e.get)),A=e||C,L=r.useRef(null),P=r.useRef(null),[I]=r.useState((()=>new f.Object3D)),[_]=r.useState((()=>new f.Vector3)),[B]=r.useState((()=>new f.Vector3)),[H]=r.useState((()=>new f.Vector3)),[U]=r.useState((()=>new f.Vector3)),W=r.useCallback((()=>{I.parent=A.parent;const e=P.current;if(e){const{outerRef:t,eyeRightRef:r,eyeLeftRef:a}=e;if(r.current&&a.current){const{irisDirRef:e}=r.current,{irisDirRef:n}=a.current;e.current&&n.current&&t.current&&(_.copy(v(e.current,new f.Vector3(0,0,0),t.current)),B.copy(v(n.current,new f.Vector3(0,0,0),t.current)),I.position.copy(v(t.current,m(_,B),A.parent||O)),H.copy(v(e.current,new f.Vector3(0,0,1),t.current)),U.copy(v(n.current,new f.Vector3(0,0,1),t.current)),I.lookAt(t.current.localToWorld(m(H,U))))}else t.current&&(I.position.copy(v(t.current,new f.Vector3(0,0,0),A.parent||O)),I.lookAt(t.current.localToWorld(new f.Vector3(0,0,1))))}return I}),[A,B,U,_,H,O,I]),[z]=r.useState((()=>new f.Object3D)),G=r.useCallback((function(e,t){if(A){var r;if(null!==(r=t)&&void 0!==r||(t=W()),y>0){const r=1e-9;n.easing.damp3(z.position,t.position,y,e,void 0,void 0,r),n.easing.dampE(z.rotation,t.rotation,y,e,void 0,void 0,r)}else z.position.copy(t.position),z.rotation.copy(t.rotation);A.position.copy(z.position),A.rotation.copy(z.rotation)}}),[A,W,y,z.position,z.rotation]),[J,K]=r.useState(),N=u.useFaceLandmarker(),Q=r.useCallback(((e,t)=>{const r=null==N?void 0:N.detectForVideo(e,t);K(r)}),[N]);a.useFrame(((e,t)=>{i||G(t)}));const X=r.useMemo((()=>Object.assign(Object.create(f.EventDispatcher.prototype),{detect:Q,computeTarget:W,update:G,facemeshApiRef:P,webcamApiRef:L,play:()=>{var e,t;null==(e=L.current)||null==(t=e.videoTextureApiRef.current)||t.texture.source.data.play()},pause:()=>{var e,t;null==(e=L.current)||null==(t=e.videoTextureApiRef.current)||t.texture.source.data.pause()}})),[Q,W,G]);r.useImperativeHandle(R,(()=>X),[X]),r.useEffect((()=>{const e=e=>{l||Q(e.texture.source.data,e.time),h&&h(e)};return X.addEventListener("videoFrame",e),()=>{X.removeEventListener("videoFrame",e)}}),[X,Q,N,l,h]),r.useEffect((()=>{if(F){const e=D().controls;return M({controls:X}),()=>M({controls:e})}}),[F,X,D,M]);const Y=null==J?void 0:J.faceLandmarks[0],Z=null==J||null==(q=J.facialTransformationMatrixes)?void 0:q[0],$=null==J||null==(S=J.faceBlendshapes)?void 0:S[0];return p.createElement(b.Provider,{value:X},o&&p.createElement(r.Suspense,{fallback:null},p.createElement(w,{ref:L,autostart:t,videoTextureSrc:c})),p.createElement(s.Facemesh,d.default({ref:P},g,{points:Y,depth:k,facialTransformationMatrix:Z,faceBlendshapes:$,eyes:E,eyesAsOrigin:j,offset:x,offsetScalar:T,debug:V,"rotation-z":Math.PI,visible:V}),p.createElement("meshBasicMaterial",{side:f.DoubleSide})))})),y=()=>r.useContext(b),w=r.forwardRef((({videoTextureSrc:e,autostart:t=!0},a)=>{const n=r.useRef(null),c=y(),s=o.suspend((async()=>e?Promise.resolve(null):await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:"user"}})),[e]);r.useEffect((()=>(c.dispatchEvent({type:"stream",stream:s}),()=>{null==s||s.getTracks().forEach((e=>e.stop())),o.clear([e])})),[s,c,e]);const u=r.useMemo((()=>({videoTextureApiRef:n})),[]);return r.useImperativeHandle(a,(()=>u),[u]),p.createElement(r.Suspense,{fallback:null},p.createElement(x,{ref:n,src:e||s,start:t}))})),x=r.forwardRef((({src:e,start:t},a)=>{const n=c.useVideoTexture(e,{start:t}),o=n.source.data,s=y(),u=r.useCallback((e=>{s.dispatchEvent({type:"videoFrame",texture:n,time:e})}),[n,s]);T(o,u);const i=r.useMemo((()=>({texture:n})),[n]);return r.useImperativeHandle(a,(()=>i),[i]),p.createElement(p.Fragment,null)})),T=(e,t)=>{r.useEffect((()=>{if(!e||!e.requestVideoFrameCallback)return;let r;return e.requestVideoFrameCallback((function a(...n){t(...n),r=e.requestVideoFrameCallback(a)})),()=>e.cancelVideoFrameCallback(r)}),[e,t])};exports.FaceControls=h,exports.useFaceControls=y;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { RefObject } from 'react';
|
|
4
|
+
import { useVideoTexture } from './useVideoTexture';
|
|
5
|
+
import { FacemeshApi, FacemeshProps } from './Facemesh';
|
|
6
|
+
declare type VideoTextureSrc = Parameters<typeof useVideoTexture>[0];
|
|
7
|
+
export declare type FaceControlsProps = {
|
|
8
|
+
camera?: THREE.Camera;
|
|
9
|
+
autostart?: boolean;
|
|
10
|
+
webcam?: boolean;
|
|
11
|
+
webcamVideoTextureSrc?: VideoTextureSrc;
|
|
12
|
+
manualUpdate?: boolean;
|
|
13
|
+
manualDetect?: boolean;
|
|
14
|
+
onVideoFrame?: (e: THREE.Event) => void;
|
|
15
|
+
makeDefault?: boolean;
|
|
16
|
+
smoothTime?: number;
|
|
17
|
+
offset?: boolean;
|
|
18
|
+
offsetScalar?: number;
|
|
19
|
+
eyes?: boolean;
|
|
20
|
+
eyesAsOrigin?: boolean;
|
|
21
|
+
depth?: number;
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
facemesh?: FacemeshProps;
|
|
24
|
+
};
|
|
25
|
+
export declare type FaceControlsApi = THREE.EventDispatcher & {
|
|
26
|
+
detect: (video: HTMLVideoElement, time: number) => void;
|
|
27
|
+
computeTarget: () => THREE.Object3D;
|
|
28
|
+
update: (delta: number, target?: THREE.Object3D) => void;
|
|
29
|
+
facemeshApiRef: RefObject<FacemeshApi>;
|
|
30
|
+
webcamApiRef: RefObject<WebcamApi>;
|
|
31
|
+
play: () => void;
|
|
32
|
+
pause: () => void;
|
|
33
|
+
};
|
|
34
|
+
export declare const FaceControls: React.ForwardRefExoticComponent<FaceControlsProps & React.RefAttributes<FaceControlsApi>>;
|
|
35
|
+
export declare const useFaceControls: () => FaceControlsApi;
|
|
36
|
+
declare type WebcamApi = {
|
|
37
|
+
videoTextureApiRef: RefObject<VideoTextureApi>;
|
|
38
|
+
};
|
|
39
|
+
declare type VideoTextureApi = {
|
|
40
|
+
texture: THREE.VideoTexture;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import _extends from '@babel/runtime/helpers/esm/extends';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { forwardRef, useRef, useState, useCallback, useMemo, useImperativeHandle, useEffect, Suspense, useContext, createContext } from 'react';
|
|
5
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
6
|
+
import { easing } from 'maath';
|
|
7
|
+
import { suspend, clear } from 'suspend-react';
|
|
8
|
+
import { useVideoTexture } from './useVideoTexture.js';
|
|
9
|
+
import { Facemesh } from './Facemesh.js';
|
|
10
|
+
import { useFaceLandmarker } from './FaceLandmarker.js';
|
|
11
|
+
|
|
12
|
+
// useVideoTexture 1st arg `src` type
|
|
13
|
+
function mean(v1, v2) {
|
|
14
|
+
return v1.clone().add(v2).multiplyScalar(0.5);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function localToLocal(objSrc, v, objDst) {
|
|
18
|
+
// see: https://discourse.threejs.org/t/object3d-localtolocal/51564
|
|
19
|
+
const v_world = objSrc.localToWorld(v);
|
|
20
|
+
return objDst.worldToLocal(v_world);
|
|
21
|
+
} //
|
|
22
|
+
//
|
|
23
|
+
//
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const FaceControlsContext = /*#__PURE__*/createContext({});
|
|
27
|
+
const FaceControls = /*#__PURE__*/forwardRef(({
|
|
28
|
+
camera,
|
|
29
|
+
autostart = true,
|
|
30
|
+
webcam = true,
|
|
31
|
+
webcamVideoTextureSrc,
|
|
32
|
+
manualUpdate = false,
|
|
33
|
+
manualDetect = false,
|
|
34
|
+
onVideoFrame,
|
|
35
|
+
smoothTime = 0.25,
|
|
36
|
+
offset = true,
|
|
37
|
+
offsetScalar = 80,
|
|
38
|
+
eyes = false,
|
|
39
|
+
eyesAsOrigin = true,
|
|
40
|
+
depth = 0.15,
|
|
41
|
+
debug = false,
|
|
42
|
+
facemesh,
|
|
43
|
+
makeDefault
|
|
44
|
+
}, fref) => {
|
|
45
|
+
var _faces$facialTransfor, _faces$faceBlendshape;
|
|
46
|
+
|
|
47
|
+
const scene = useThree(state => state.scene);
|
|
48
|
+
const defaultCamera = useThree(state => state.camera);
|
|
49
|
+
const set = useThree(state => state.set);
|
|
50
|
+
const get = useThree(state => state.get);
|
|
51
|
+
const explCamera = camera || defaultCamera;
|
|
52
|
+
const webcamApiRef = useRef(null);
|
|
53
|
+
const facemeshApiRef = useRef(null); //
|
|
54
|
+
// computeTarget()
|
|
55
|
+
//
|
|
56
|
+
// Compute `target` position and rotation for the camera (according to <Facemesh>)
|
|
57
|
+
//
|
|
58
|
+
// 1. 👀 either following the 2 eyes
|
|
59
|
+
// 2. 👤 or just the head mesh
|
|
60
|
+
//
|
|
61
|
+
|
|
62
|
+
const [target] = useState(() => new THREE.Object3D());
|
|
63
|
+
const [irisRightDirPos] = useState(() => new THREE.Vector3());
|
|
64
|
+
const [irisLeftDirPos] = useState(() => new THREE.Vector3());
|
|
65
|
+
const [irisRightLookAt] = useState(() => new THREE.Vector3());
|
|
66
|
+
const [irisLeftLookAt] = useState(() => new THREE.Vector3());
|
|
67
|
+
const computeTarget = useCallback(() => {
|
|
68
|
+
// same parent as the camera
|
|
69
|
+
target.parent = explCamera.parent;
|
|
70
|
+
const facemeshApi = facemeshApiRef.current;
|
|
71
|
+
|
|
72
|
+
if (facemeshApi) {
|
|
73
|
+
const {
|
|
74
|
+
outerRef,
|
|
75
|
+
eyeRightRef,
|
|
76
|
+
eyeLeftRef
|
|
77
|
+
} = facemeshApi;
|
|
78
|
+
|
|
79
|
+
if (eyeRightRef.current && eyeLeftRef.current) {
|
|
80
|
+
// 1. 👀
|
|
81
|
+
const {
|
|
82
|
+
irisDirRef: irisRightDirRef
|
|
83
|
+
} = eyeRightRef.current;
|
|
84
|
+
const {
|
|
85
|
+
irisDirRef: irisLeftDirRef
|
|
86
|
+
} = eyeLeftRef.current;
|
|
87
|
+
|
|
88
|
+
if (irisRightDirRef.current && irisLeftDirRef.current && outerRef.current) {
|
|
89
|
+
//
|
|
90
|
+
// position: mean of irisRightDirPos,irisLeftDirPos
|
|
91
|
+
//
|
|
92
|
+
irisRightDirPos.copy(localToLocal(irisRightDirRef.current, new THREE.Vector3(0, 0, 0), outerRef.current));
|
|
93
|
+
irisLeftDirPos.copy(localToLocal(irisLeftDirRef.current, new THREE.Vector3(0, 0, 0), outerRef.current));
|
|
94
|
+
target.position.copy(localToLocal(outerRef.current, mean(irisRightDirPos, irisLeftDirPos), explCamera.parent || scene)); //
|
|
95
|
+
// lookAt: mean of irisRightLookAt,irisLeftLookAt
|
|
96
|
+
//
|
|
97
|
+
|
|
98
|
+
irisRightLookAt.copy(localToLocal(irisRightDirRef.current, new THREE.Vector3(0, 0, 1), outerRef.current));
|
|
99
|
+
irisLeftLookAt.copy(localToLocal(irisLeftDirRef.current, new THREE.Vector3(0, 0, 1), outerRef.current));
|
|
100
|
+
target.lookAt(outerRef.current.localToWorld(mean(irisRightLookAt, irisLeftLookAt)));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// 2. 👤
|
|
104
|
+
if (outerRef.current) {
|
|
105
|
+
target.position.copy(localToLocal(outerRef.current, new THREE.Vector3(0, 0, 0), explCamera.parent || scene));
|
|
106
|
+
target.lookAt(outerRef.current.localToWorld(new THREE.Vector3(0, 0, 1)));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return target;
|
|
112
|
+
}, [explCamera, irisLeftDirPos, irisLeftLookAt, irisRightDirPos, irisRightLookAt, scene, target]); //
|
|
113
|
+
// update()
|
|
114
|
+
//
|
|
115
|
+
// Updating the camera `current` position and rotation, following `target`
|
|
116
|
+
//
|
|
117
|
+
|
|
118
|
+
const [current] = useState(() => new THREE.Object3D());
|
|
119
|
+
const update = useCallback(function (delta, target) {
|
|
120
|
+
if (explCamera) {
|
|
121
|
+
var _target;
|
|
122
|
+
|
|
123
|
+
(_target = target) !== null && _target !== void 0 ? _target : target = computeTarget();
|
|
124
|
+
|
|
125
|
+
if (smoothTime > 0) {
|
|
126
|
+
// damping current
|
|
127
|
+
const eps = 1e-9;
|
|
128
|
+
easing.damp3(current.position, target.position, smoothTime, delta, undefined, undefined, eps);
|
|
129
|
+
easing.dampE(current.rotation, target.rotation, smoothTime, delta, undefined, undefined, eps);
|
|
130
|
+
} else {
|
|
131
|
+
// instant
|
|
132
|
+
current.position.copy(target.position);
|
|
133
|
+
current.rotation.copy(target.rotation);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
explCamera.position.copy(current.position);
|
|
137
|
+
explCamera.rotation.copy(current.rotation);
|
|
138
|
+
}
|
|
139
|
+
}, [explCamera, computeTarget, smoothTime, current.position, current.rotation]); //
|
|
140
|
+
// detect()
|
|
141
|
+
//
|
|
142
|
+
|
|
143
|
+
const [faces, setFaces] = useState();
|
|
144
|
+
const faceLandmarker = useFaceLandmarker();
|
|
145
|
+
const detect = useCallback((video, time) => {
|
|
146
|
+
const faces = faceLandmarker == null ? void 0 : faceLandmarker.detectForVideo(video, time);
|
|
147
|
+
setFaces(faces);
|
|
148
|
+
}, [faceLandmarker]);
|
|
149
|
+
useFrame((_, delta) => {
|
|
150
|
+
if (!manualUpdate) {
|
|
151
|
+
update(delta);
|
|
152
|
+
}
|
|
153
|
+
}); // Ref API
|
|
154
|
+
|
|
155
|
+
const api = useMemo(() => Object.assign(Object.create(THREE.EventDispatcher.prototype), {
|
|
156
|
+
detect,
|
|
157
|
+
computeTarget,
|
|
158
|
+
update,
|
|
159
|
+
facemeshApiRef,
|
|
160
|
+
webcamApiRef,
|
|
161
|
+
// shorthands
|
|
162
|
+
play: () => {
|
|
163
|
+
var _webcamApiRef$current, _webcamApiRef$current2;
|
|
164
|
+
|
|
165
|
+
(_webcamApiRef$current = webcamApiRef.current) == null ? void 0 : (_webcamApiRef$current2 = _webcamApiRef$current.videoTextureApiRef.current) == null ? void 0 : _webcamApiRef$current2.texture.source.data.play();
|
|
166
|
+
},
|
|
167
|
+
pause: () => {
|
|
168
|
+
var _webcamApiRef$current3, _webcamApiRef$current4;
|
|
169
|
+
|
|
170
|
+
(_webcamApiRef$current3 = webcamApiRef.current) == null ? void 0 : (_webcamApiRef$current4 = _webcamApiRef$current3.videoTextureApiRef.current) == null ? void 0 : _webcamApiRef$current4.texture.source.data.pause();
|
|
171
|
+
}
|
|
172
|
+
}), [detect, computeTarget, update]);
|
|
173
|
+
useImperativeHandle(fref, () => api, [api]); //
|
|
174
|
+
// events callbacks
|
|
175
|
+
//
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const onVideoFrameCb = e => {
|
|
179
|
+
if (!manualDetect) detect(e.texture.source.data, e.time);
|
|
180
|
+
if (onVideoFrame) onVideoFrame(e);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
api.addEventListener('videoFrame', onVideoFrameCb);
|
|
184
|
+
return () => {
|
|
185
|
+
api.removeEventListener('videoFrame', onVideoFrameCb);
|
|
186
|
+
};
|
|
187
|
+
}, [api, detect, faceLandmarker, manualDetect, onVideoFrame]); // `controls` global state
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (makeDefault) {
|
|
191
|
+
const old = get().controls;
|
|
192
|
+
set({
|
|
193
|
+
controls: api
|
|
194
|
+
});
|
|
195
|
+
return () => set({
|
|
196
|
+
controls: old
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}, [makeDefault, api, get, set]);
|
|
200
|
+
const points = faces == null ? void 0 : faces.faceLandmarks[0];
|
|
201
|
+
const facialTransformationMatrix = faces == null ? void 0 : (_faces$facialTransfor = faces.facialTransformationMatrixes) == null ? void 0 : _faces$facialTransfor[0];
|
|
202
|
+
const faceBlendshapes = faces == null ? void 0 : (_faces$faceBlendshape = faces.faceBlendshapes) == null ? void 0 : _faces$faceBlendshape[0];
|
|
203
|
+
return /*#__PURE__*/React.createElement(FaceControlsContext.Provider, {
|
|
204
|
+
value: api
|
|
205
|
+
}, webcam && /*#__PURE__*/React.createElement(Suspense, {
|
|
206
|
+
fallback: null
|
|
207
|
+
}, /*#__PURE__*/React.createElement(Webcam, {
|
|
208
|
+
ref: webcamApiRef,
|
|
209
|
+
autostart: autostart,
|
|
210
|
+
videoTextureSrc: webcamVideoTextureSrc
|
|
211
|
+
})), /*#__PURE__*/React.createElement(Facemesh, _extends({
|
|
212
|
+
ref: facemeshApiRef
|
|
213
|
+
}, facemesh, {
|
|
214
|
+
points: points,
|
|
215
|
+
depth: depth,
|
|
216
|
+
facialTransformationMatrix: facialTransformationMatrix,
|
|
217
|
+
faceBlendshapes: faceBlendshapes,
|
|
218
|
+
eyes: eyes,
|
|
219
|
+
eyesAsOrigin: eyesAsOrigin,
|
|
220
|
+
offset: offset,
|
|
221
|
+
offsetScalar: offsetScalar,
|
|
222
|
+
debug: debug,
|
|
223
|
+
"rotation-z": Math.PI,
|
|
224
|
+
visible: debug
|
|
225
|
+
}), /*#__PURE__*/React.createElement("meshBasicMaterial", {
|
|
226
|
+
side: THREE.DoubleSide
|
|
227
|
+
})));
|
|
228
|
+
});
|
|
229
|
+
const useFaceControls = () => useContext(FaceControlsContext); //
|
|
230
|
+
// Webcam
|
|
231
|
+
//
|
|
232
|
+
|
|
233
|
+
const Webcam = /*#__PURE__*/forwardRef(({
|
|
234
|
+
videoTextureSrc,
|
|
235
|
+
autostart = true
|
|
236
|
+
}, fref) => {
|
|
237
|
+
const videoTextureApiRef = useRef(null);
|
|
238
|
+
const faceControls = useFaceControls();
|
|
239
|
+
const stream = suspend(async () => {
|
|
240
|
+
return !videoTextureSrc ? await navigator.mediaDevices.getUserMedia({
|
|
241
|
+
audio: false,
|
|
242
|
+
video: {
|
|
243
|
+
facingMode: 'user'
|
|
244
|
+
}
|
|
245
|
+
}) : Promise.resolve(null);
|
|
246
|
+
}, [videoTextureSrc]);
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
faceControls.dispatchEvent({
|
|
249
|
+
type: 'stream',
|
|
250
|
+
stream
|
|
251
|
+
});
|
|
252
|
+
return () => {
|
|
253
|
+
stream == null ? void 0 : stream.getTracks().forEach(track => track.stop());
|
|
254
|
+
clear([videoTextureSrc]);
|
|
255
|
+
};
|
|
256
|
+
}, [stream, faceControls, videoTextureSrc]); // ref-api
|
|
257
|
+
|
|
258
|
+
const api = useMemo(() => ({
|
|
259
|
+
videoTextureApiRef
|
|
260
|
+
}), []);
|
|
261
|
+
useImperativeHandle(fref, () => api, [api]);
|
|
262
|
+
return /*#__PURE__*/React.createElement(Suspense, {
|
|
263
|
+
fallback: null
|
|
264
|
+
}, /*#__PURE__*/React.createElement(VideoTexture, {
|
|
265
|
+
ref: videoTextureApiRef,
|
|
266
|
+
src: videoTextureSrc || stream,
|
|
267
|
+
start: autostart
|
|
268
|
+
}));
|
|
269
|
+
}); //
|
|
270
|
+
// VideoTexture
|
|
271
|
+
//
|
|
272
|
+
|
|
273
|
+
const VideoTexture = /*#__PURE__*/forwardRef(({
|
|
274
|
+
src,
|
|
275
|
+
start
|
|
276
|
+
}, fref) => {
|
|
277
|
+
const texture = useVideoTexture(src, {
|
|
278
|
+
start
|
|
279
|
+
});
|
|
280
|
+
const video = texture.source.data;
|
|
281
|
+
const faceControls = useFaceControls();
|
|
282
|
+
const onVideoFrame = useCallback(time => {
|
|
283
|
+
faceControls.dispatchEvent({
|
|
284
|
+
type: 'videoFrame',
|
|
285
|
+
texture,
|
|
286
|
+
time
|
|
287
|
+
});
|
|
288
|
+
}, [texture, faceControls]);
|
|
289
|
+
useVideoFrame(video, onVideoFrame); // ref-api
|
|
290
|
+
|
|
291
|
+
const api = useMemo(() => ({
|
|
292
|
+
texture
|
|
293
|
+
}), [texture]);
|
|
294
|
+
useImperativeHandle(fref, () => api, [api]);
|
|
295
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const useVideoFrame = (video, f) => {
|
|
299
|
+
// https://web.dev/requestvideoframecallback-rvfc/
|
|
300
|
+
// https://www.remotion.dev/docs/video-manipulation
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!video || !video.requestVideoFrameCallback) return;
|
|
303
|
+
let handle;
|
|
304
|
+
|
|
305
|
+
function callback(...args) {
|
|
306
|
+
f(...args);
|
|
307
|
+
handle = video.requestVideoFrameCallback(callback);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
video.requestVideoFrameCallback(callback);
|
|
311
|
+
return () => video.cancelVideoFrameCallback(handle);
|
|
312
|
+
}, [video, f]);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export { FaceControls, useFaceControls };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react"),t=require("@mediapipe/tasks-vision"),r=require("suspend-react");function a(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var a=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,a.get?a:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}var s=a(e);const n=e.createContext({}),o={basePath:"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm",options:{baseOptions:{modelAssetPath:"https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",delegate:"GPU"},runningMode:"VIDEO",outputFaceBlendshapes:!0,outputFacialTransformationMatrixes:!0}};exports.FaceLandmarker=function({basePath:a=o.basePath,options:i=o.options,children:c}){const u=JSON.stringify(i),l=r.suspend((async()=>await t.FilesetResolver.forVisionTasks(a).then((e=>t.FaceLandmarker.createFromOptions(e,i)))),[a,u]);return e.useEffect((()=>()=>{null==l||l.close(),r.clear([a,u])}),[l,a,u]),s.createElement(n.Provider,{value:l},c)},exports.FaceLandmarkerDefaults=o,exports.useFaceLandmarker=function(){return e.useContext(n)};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { FaceLandmarker as FaceLandmarkerImpl, FaceLandmarkerOptions } from '@mediapipe/tasks-vision';
|
|
3
|
+
declare type FaceLandmarkerProps = {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
options?: FaceLandmarkerOptions;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare const FaceLandmarkerDefaults: {
|
|
9
|
+
basePath: string;
|
|
10
|
+
options: FaceLandmarkerOptions;
|
|
11
|
+
};
|
|
12
|
+
export declare function FaceLandmarker({ basePath, options, children, }: FaceLandmarkerProps): JSX.Element;
|
|
13
|
+
export declare function useFaceLandmarker(): FaceLandmarkerImpl | undefined;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useEffect, useContext, createContext } from 'react';
|
|
3
|
+
import { FilesetResolver, FaceLandmarker as FaceLandmarker$1 } from '@mediapipe/tasks-vision';
|
|
4
|
+
import { suspend, clear } from 'suspend-react';
|
|
5
|
+
|
|
6
|
+
/* eslint react-hooks/exhaustive-deps: 1 */
|
|
7
|
+
const FaceLandmarkerContext = /*#__PURE__*/createContext({});
|
|
8
|
+
const FaceLandmarkerDefaults = {
|
|
9
|
+
basePath: 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm',
|
|
10
|
+
options: {
|
|
11
|
+
baseOptions: {
|
|
12
|
+
modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
|
|
13
|
+
delegate: 'GPU'
|
|
14
|
+
},
|
|
15
|
+
runningMode: 'VIDEO',
|
|
16
|
+
outputFaceBlendshapes: true,
|
|
17
|
+
outputFacialTransformationMatrixes: true
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function FaceLandmarker({
|
|
21
|
+
basePath = FaceLandmarkerDefaults.basePath,
|
|
22
|
+
options = FaceLandmarkerDefaults.options,
|
|
23
|
+
children
|
|
24
|
+
}) {
|
|
25
|
+
const opts = JSON.stringify(options);
|
|
26
|
+
const faceLandmarker = suspend(async () => {
|
|
27
|
+
return await FilesetResolver.forVisionTasks(basePath).then(vision => FaceLandmarker$1.createFromOptions(vision, options));
|
|
28
|
+
}, [basePath, opts]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
return () => {
|
|
31
|
+
faceLandmarker == null ? void 0 : faceLandmarker.close();
|
|
32
|
+
clear([basePath, opts]);
|
|
33
|
+
};
|
|
34
|
+
}, [faceLandmarker, basePath, opts]);
|
|
35
|
+
return /*#__PURE__*/React.createElement(FaceLandmarkerContext.Provider, {
|
|
36
|
+
value: faceLandmarker
|
|
37
|
+
}, children);
|
|
38
|
+
}
|
|
39
|
+
function useFaceLandmarker() {
|
|
40
|
+
return useContext(FaceLandmarkerContext);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { FaceLandmarker, FaceLandmarkerDefaults, useFaceLandmarker };
|