@react-three/drei 9.73.3 → 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 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://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) and CameraControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story)
364
+ Drei currently exports OrbitControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) CameraControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story) and FaceControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](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://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/shapes-facemesh--facemesh-st)
940
1018
 
941
- Renders an oriented [MediaPipe `face` mesh](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection):
942
-
943
- ```jsx
944
- const face = {
945
- keypoints: [
946
- {x: 406.53152857172876, y: 256.8054528661723, z: 10.2, name: "lips"},
947
- {x: 406.544237446397, y: 230.06933367750395, z: 8},
948
- ...
949
- ],
950
- box: {
951
- xMin: 304.6476503248806,
952
- xMax: 502.5079975897382,
953
- yMin: 102.16298762367356,
954
- yMax: 349.035215984403,
955
- width: 197.86034726485758,
956
- height: 246.87222836072945
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 face={face} />
1041
+ <Facemesh points={points} />
961
1042
  ```
962
1043
 
963
1044
  ```tsx
964
- type FacemeshProps = {
965
- /** a MediaPipeFaceMesh object, default: a lambda face */
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} face={face} />
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
- NB: `outerRef` group is oriented as your `face`. You can for example get its world direction:
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
+ ![](https://img.shields.io/badge/-suspense-brightgreen)
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 };