vendor in kalidokit

This commit is contained in:
Brandon Thomas committed 2025-01-28 22:15:45 -05:00
1 parent 4b07553e3c
commit d637a16af5
60 files changed
+32107 -11

No files matched your search

+8 -2
View File
@@ -1,5 +1,11 @@
import * as THREE from "three";
//import * as Kalidokit from "kalidokit";
import * as Kalidokit from "kalidokit"
import { TFace } from "kalidokit";
//import { Pose } from "kalidokit";
//const Kalidokit = require('kalidokit');
import { FilesetResolver, PoseLandmarker, PoseLandmarkerResult } from "@mediapipe/tasks-vision"
import { CharacterPoseHelper } from "./Engines/Helpers/CharacterPoseHelper";
import { loadImageFromAnonymousOriginUrl } from "~/Helpers/ImageHelpers";
@@ -79,14 +85,14 @@ async function doTest(firstFrameUrl: string, characterRig: THREE.Object3D<THREE.
const poseWorld3DArray : any = solutions.worldLandmarks;
const poseLandmarkArray : any = solutions.landmarks;
/*let solution = Kalidokit.Pose.solve(poseWorld3DArray, poseLandmarkArray, {
let solution = Kalidokit.Pose.solve(poseWorld3DArray, poseLandmarkArray, {
runtime:'mediapipe', // default is 'mediapipe'
//video: HTMLVideoElement,// specify an html video or manually set image size
imageSize:{
width: 640,
height: 480,
}
});*/
});
console.log('kalidokit solution', solution);
}
+21
View File
@@ -0,0 +1,21 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: yeemachine
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+28
View File
@@ -0,0 +1,28 @@
name: Build Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm i -g npm@latest
- run: npm ci
- run: npm run jest
- run: npm run build
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 yeemachine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+283
View File
@@ -0,0 +1,283 @@
<img src="https://github.com/yeemachine/kalidokit/blob/main/docs/kalidokit-logo.png?raw=true" alt="Kalidokit Template" width="200ps" style="margin-bottom:10px"/>
[![NPM Package][npm]][npm-url]
[![NPM Bundle Size][minimized-size]][npm-url]
[![jsDelivr hits (npm)][js-delivr]][js-delivr-url]
[![NPM Downloads][npm-downloads]][npmtrends-url]
[![Website][website]][website-url]
[![Discord Shield][discord]][discord-url]
| :exclamation: KalidoKit is now officially deprecated and solutions will be directly integrated into [MediaPipe](https://mediapipe-studio.webapps.google.com/demo/face_landmarker) :exclamation:|
|--------------------------------------|
## Face, Pose, and Hand Tracking Calculator
Kalidokit is a blendshape and kinematics solver for Mediapipe/Tensorflow.js face, eyes, pose, and hand tracking models, compatible with [Facemesh](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection), [Blazepose](https://github.com/tensorflow/tfjs-models/tree/master/pose-detection), [Handpose](https://google.github.io/mediapipe/solutions/hands.html), and [Holistic](https://google.github.io/mediapipe/solutions/holistic.html). It takes predicted 3D landmarks and calculates simple euler rotations and blendshape face values.
As the core to Vtuber web apps, [Kalidoface](https://kalidoface.com) and [Kalidoface 3D](https://3d.kalidoface.com), KalidoKit is designed specifically for rigging 3D VRM models and Live2D avatars!
NOTE: This is a JS library meant for developers using Mediapipe pretrained models and not a complete app by itself. Please visit [Kalidoface](https://kalidoface.com) for the full vtuber experience 😉!
<a href="https://glitch.com/edit/#!/kalidokit"><img src="https://github.com/yeemachine/kalidokit/blob/main/docs/kalidokit_glitch.gif?raw=true" alt="Kalidokit Template" width="48%"/></a>
<a href="https://glitch.com/edit/#!/kalidokit-live2d"><img src="https://github.com/yeemachine/kalidokit/blob/main/docs/kalidokit-live2d_glitch.gif?raw=true" alt="Kalidokit Template" width="48%"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B75DIY1)
## Install
#### Via NPM
```
npm install kalidokit
```
```js
import * as Kalidokit from "kalidokit";
// or only import the class you need
import { Face, Pose, Hand } from "kalidokit";
```
#### Via CDN
```js
<script src="https://cdn.jsdelivr.net/npm/kalidokit@1.1/dist/kalidokit.umd.js"></script>
```
## Methods
Kalidokit is composed of 3 classes for Face, Pose, and Hand calculations. They accept landmark outputs from models like Facemesh, Blazepose, Handpose, and Holistic.
```js
// Accepts an array(468 or 478 with iris tracking) of vectors
Kalidokit.Face.solve(facelandmarkArray, {
runtime: "tfjs", // `mediapipe` or `tfjs`
video: HTMLVideoElement,
imageSize: { height: 0, width: 0 },
smoothBlink: false, // smooth left and right eye blink delays
blinkSettings: [0.25, 0.75], // adjust upper and lower bound blink sensitivity
});
// Accepts arrays(33) of Pose keypoints and 3D Pose keypoints
Kalidokit.Pose.solve(poseWorld3DArray, poseLandmarkArray, {
runtime: "tfjs", // `mediapipe` or `tfjs`
video: HTMLVideoElement,
imageSize: { height: 0, width: 0 },
enableLegs: true,
});
// Accepts array(21) of hand landmark vectors; specify 'Right' or 'Left' side
Kalidokit.Hand.solve(handLandmarkArray, "Right");
// Using exported classes directly
Face.solve(facelandmarkArray);
Pose.solve(poseWorld3DArray, poseLandmarkArray);
Hand.solve(handLandmarkArray, "Right");
```
Additional Utils
```js
// Stabilizes left/right blink delays + wink by providing blenshapes and head rotation
Kalidokit.Face.stabilizeBlink(
{ r: 0, l: 1 }, // left and right eye blendshape values
headRotationY, // head rotation in radians
{
noWink = false, // disables winking
maxRot = 0.5 // max head rotation in radians before interpolating obscured eyes
});
// The internal vector math class
Kalidokit.Vector();
```
## VRM and Live2D Sample Code
Quick-start your Vtuber app with these simple remixable examples.
### VRM Glitch Template
Face, full-body, and hand tracking in under 350 lines of javascript. This demo uses Mediapipe Holistic for body tracking, Three.js + Three-VRM for rendering models, and KalidoKit for the kinematic calculations. This [demo](https://glitch.com/edit/#!/kalidokit) uses a minimal amount of easing to smooth animations, but feel free to make it your own! VRM model from [Vroid Hub](https://hub.vroid.com/en/characters/6617701103638326208/models/8892157442595230149).
<a href="https://glitch.com/edit/#!/kalidokit"><img alt="Remix on Glitch" src="https://cdn.gomix.com/f3620a78-0ad3-4f81-a271-c8a4faa20f86%2Fremix-button.svg"></a>
### Live2D Glitch Template
This demo uses Mediapipe FaceMesh for face tracking, Pixi.js + pixi-live2d-display for rendering models, and KalidoKit for the kinematic calculations. This [demo](https://glitch.com/edit/#!/kalidokit-live2d) uses an official sample Live2D model, [Hiyori](https://www.live2d.com/en/download/sample-data/).
<a href="https://glitch.com/edit/#!/kalidokit-live2d"><img alt="Remix on Glitch" src="https://cdn.gomix.com/f3620a78-0ad3-4f81-a271-c8a4faa20f86%2Fremix-button.svg"></a>
### Local VRM and Live2D Examples
Interested in contributing? If you clone this project, the `docs` folder includes both VRM and Live2D KalidoKit templates. Run the `test` script to build the library and start up the dev server. The VRM demo will be hosted on `localhost:3000` and Live2D on `localhost:3000/live2d/`.
```
npm install
npm run test
```
## Basic Usage
<img src="https://github.com/yeemachine/kalidokit/blob/main/docs/kalidokit-pipeline.png?raw=true" alt="Kalidokit Template" width="100%"/>
The implementation may vary depending on what pose and face detection model you choose to use, but the principle is still the same. This example uses Mediapipe Holistic which concisely combines them together.
```js
import * as Kalidokit from 'kalidokit'
import '@mediapipe/holistic/holistic';
import '@mediapipe/camera_utils/camera_utils';
let holistic = new Holistic({locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.4.1633559476/${file}`;
}});
holistic.onResults(results=>{
// do something with prediction results
// landmark names may change depending on TFJS/Mediapipe model version
let facelm = results.faceLandmarks;
let poselm = results.poseLandmarks;
let poselm3D = results.ea;
let rightHandlm = results.rightHandLandmarks;
let leftHandlm = results.leftHandLandmarks;
let faceRig = Kalidokit.Face.solve(facelm,{runtime:'mediapipe',video:HTMLVideoElement})
let poseRig = Kalidokit.Pose.solve(poselm3d,poselm,{runtime:'mediapipe',video:HTMLVideoElement})
let rightHandRig = Kalidokit.Hand.solve(rightHandlm,"Right")
let leftHandRig = Kalidokit.Hand.solve(leftHandlm,"Left")
};
});
// use Mediapipe's webcam utils to send video to holistic every frame
const camera = new Camera(HTMLVideoElement, {
onFrame: async () => {
await holistic.send({image: HTMLVideoElement});
},
width: 640,
height: 480
});
camera.start();
```
## Slight differences with Mediapipe and Tensorflow.js
Due to slight differences in the results from Mediapipe and Tensorflow.js, it is recommended to specify which runtime version you are using as well as the video input/image size as a reference.
```js
Kalidokit.Pose.solve(poselm3D,poselm,{
runtime:'tfjs', // default is 'mediapipe'
video: HTMLVideoElement,// specify an html video or manually set image size
imageSize:{
width: 640,
height: 480,
};
})
Kalidokit.Face.solve(facelm,{
runtime:'mediapipe', // default is 'tfjs'
video: HTMLVideoElement,// specify an html video or manually set image size
imageSize:{
width: 640,
height: 480,
};
})
```
## Outputs
Below are the expected results from KalidoKit solvers.
```js
// Kalidokit.Face.solve()
// Head rotations in radians
// Degrees and normalized rotations also available
{
eye: {l: 1,r: 1},
mouth: {
x: 0,
y: 0,
shape: {A:0, E:0, I:0, O:0, U:0}
},
head: {
x: 0,
y: 0,
z: 0,
width: 0.3,
height: 0.6,
position: {x: 0.5, y: 0.5, z: 0}
},
brow: 0,
pupil: {x: 0, y: 0}
}
```
```js
// Kalidokit.Pose.solve()
// Joint rotations in radians, leg calculators are a WIP
{
RightUpperArm: {x: 0, y: 0, z: -1.25},
LeftUpperArm: {x: 0, y: 0, z: 1.25},
RightLowerArm: {x: 0, y: 0, z: 0},
LeftLowerArm: {x: 0, y: 0, z: 0},
LeftUpperLeg: {x: 0, y: 0, z: 0},
RightUpperLeg: {x: 0, y: 0, z: 0},
RightLowerLeg: {x: 0, y: 0, z: 0},
LeftLowerLeg: {x: 0, y: 0, z: 0},
LeftHand: {x: 0, y: 0, z: 0},
RightHand: {x: 0, y: 0, z: 0},
Spine: {x: 0, y: 0, z: 0},
Hips: {
worldPosition: {x: 0, y: 0, z: 0},
position: {x: 0, y: 0, z: 0},
rotation: {x: 0, y: 0, z: 0},
}
}
```
```js
// Kalidokit.Hand.solve()
// Joint rotations in radians
// only wrist and thumb have 3 degrees of freedom
// all other finger joints move in the Z axis only
{
RightWrist: {x: -0.13, y: -0.07, z: -1.04},
RightRingProximal: {x: 0, y: 0, z: -0.13},
RightRingIntermediate: {x: 0, y: 0, z: -0.4},
RightRingDistal: {x: 0, y: 0, z: -0.04},
RightIndexProximal: {x: 0, y: 0, z: -0.24},
RightIndexIntermediate: {x: 0, y: 0, z: -0.25},
RightIndexDistal: {x: 0, y: 0, z: -0.06},
RightMiddleProximal: {x: 0, y: 0, z: -0.09},
RightMiddleIntermediate: {x: 0, y: 0, z: -0.44},
RightMiddleDistal: {x: 0, y: 0, z: -0.06},
RightThumbProximal: {x: -0.23, y: -0.33, z: -0.12},
RightThumbIntermediate: {x: -0.2, y: -0.19, z: -0.01},
RightThumbDistal: {x: -0.2, y: 0.002, z: 0.15},
RightLittleProximal: {x: 0, y: 0, z: -0.09},
RightLittleIntermediate: {x: 0, y: 0, z: -0.22},
RightLittleDistal: {x: 0, y: 0, z: -0.1}
}
```
## Community Showcase + Contributing
If you'd like to share a creative use of KalidoKit, please reach out or send a pull request! Feel free to also use our Twitter hashtag, [#kalidokit](https://twitter.com/search?q=%23kalidokit).
Kalidoface/KalidoKit also now has a [Discord server][discord-url]! Come and join us!
The current library is a work in progress and contributions to improve it are very welcome! Some plans include: better facial blendshapes, full leg tracking, and improved efficiency.
[![Kalidoface virtual webcam](https://raw.githubusercontent.com/yeemachine/kalidoface-live2d-models/main/promo/TW-Promo-short.gif)](https://kalidoface.com) [![Kalidoface Pose Demo](https://cdn.glitch.me/29e07830-2317-4b15-a044-135e73c7f840%2Fkalidoface-pose-dance.gif?v=1633453098775)](https://3d.kalidoface.com)
[npm]: https://img.shields.io/npm/v/kalidokit
[npm-url]: https://www.npmjs.com/package/kalidokit
[minimized-size]: https://img.shields.io/bundlephobia/min/kalidokit
[js-delivr]: https://img.shields.io/jsdelivr/npm/hw/kalidokit
[js-delivr-url]: https://www.jsdelivr.com/package/npm/kalidokit
[website]: https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=brightgreen&up_message=online&url=https%3A%2F%2Fkalidoface.com
[website-url]: https://kalidoface.com
[npm-downloads]: https://img.shields.io/npm/dw/kalidokit
[npmtrends-url]: https://www.npmtrends.com/kalidokit
[discord]: https://discordapp.com/api/guilds/869435838746984468/widget.png?style=shield
[discord-url]: https://discord.gg/XMqdfzVZ2E
+1
View File
@@ -0,0 +1 @@
kit.kalidoface.com
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- HTML Meta Tags -->
<title>KalidoKit - Face and Body Rig Solver</title>
<meta
name="description"
content="Track face and body rigs just your browser webcam!"
/>
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="kit.kalidoface.com" />
<meta property="twitter:url" content="https://kit.kalidoface.com/" />
<meta name="twitter:title" content="KalidoKit - Face and Body Rig Solver" />
<meta
name="twitter:description"
content="Track face and body rigs just your browser webcam!"
/>
<meta
name="twitter:image"
content="https://cdn.glitch.com/239c5934-4d83-4c5c-bef6-44dcdb04c8fb%2Fkalidoface-meta.jpg?v=1630110302224"
/>
<meta
name="viewport"
content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"
/>
<link
rel="shortcut icon"
href="https://yeemachine.github.io/kalidoface-live2d-models/Icons/icon-circle.svg"
/>
<!-- Basic Three.js -->
<script src="https://unpkg.com/three@0.133.0/build/three.js"></script>
<!-- GLTF Loader for Three.js -->
<script src="https://unpkg.com/three@0.133.0/examples/js/loaders/GLTFLoader.js"></script>
<!-- Orbit Controls for Three.js -->
<script src="https://unpkg.com/three@0.133.0/examples/js/controls/OrbitControls.js"></script>
<!-- VRM Loader for Three.js -->
<script src="https://unpkg.com/@pixiv/three-vrm@0.6.7/lib/three-vrm.js"></script>
<!-- Mediapipe or Tensorflow.js -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/holistic.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Drawing Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Camera Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="preview">
<video class="input_video" width="1280px" height="720px"></video>
<canvas class="guides"></canvas>
<section>
<a href="/live2d/"><p>Live2D</p></a>
<a class="current" href="/"><p>VRM</p></a>
</section>
</div>
<h1 class="notranslate">
<a href="https://3d.kalidoface.com">kalidoface</a>
</h1>
<nav>
<a href="https://www.npmjs.com/package/kalidokit">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fnpm.png?v=1635133318451"
/>
</a>
<a href="https://github.com/yeemachine/kalidokit">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fgithub.png?v=1635133310517"
/>
</a>
<a href="https://twitter.com/yeemachine">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Ftwitter.png?v=1635133322561"
/>
</a>
</nav>
<p class="linkOut">
Visit
<a id="full" href="https://kalidoface.com">the full Vtuber App</a>!
</p>
<script src="./script.js" type="module" defer></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- HTML Meta Tags -->
<title>KalidoKit - Face and Body Rig Solver</title>
<meta
name="description"
content="Track face and body rigs just your browser webcam!"
/>
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="kit.kalidoface.com" />
<meta property="twitter:url" content="https://kit.kalidoface.com/" />
<meta name="twitter:title" content="KalidoKit - Face and Body Rig Solver" />
<meta
name="twitter:description"
content="Track face and body rigs just your browser webcam!"
/>
<meta
name="twitter:image"
content="https://cdn.glitch.com/239c5934-4d83-4c5c-bef6-44dcdb04c8fb%2Fkalidoface-meta.jpg?v=1630110302224"
/>
<meta
name="viewport"
content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"
/>
<link
rel="shortcut icon"
href="https://yeemachine.github.io/kalidoface-live2d-models/Icons/icon-circle.svg"
/>
<!-- Live2D SDK -->
<script src="https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/dylanNew/live2d/webgl/Live2D/lib/live2d.min.js"></script>
<!-- PixiJS Renderer -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.3/pixi.min.js"></script>
<!-- PixiJS Live2D Plugin -->
<script src="https://cdn.jsdelivr.net/npm/pixi-live2d-display/dist/index.min.js"></script>
<!-- Mediapipe or Tensorflow.js -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Drawing Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Camera Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
crossorigin="anonymous"
></script>
<!-- Kalidokit Kinematic Solver -->
<script src="https://cdn.jsdelivr.net/npm/kalidokit@1.1/dist/kalidokit.umd.js"></script>
<link rel="stylesheet" href="../style.css" />
</head>
<body>
<div class="preview">
<video class="input_video" width="1280px" height="720px"></video>
<canvas class="guides"></canvas>
<section>
<a class="current" href="/live2d/"><p>Live2D</p></a>
<a href="/"><p>VRM</p></a>
</section>
</div>
<h1 class="notranslate">
<a href="https://3d.kalidoface.com">kalidoface</a>
</h1>
<nav>
<a href="https://www.npmjs.com/package/kalidokit">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fnpm.png?v=1635133318451"
/>
</a>
<a href="https://github.com/yeemachine/kalidokit">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fgithub.png?v=1635133310517"
/>
</a>
<a href="https://twitter.com/yeemachine">
<img
src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Ftwitter.png?v=1635133322561"
/>
</a>
</nav>
<canvas id="live2d"></canvas>
<p class="linkOut">
Visit
<a id="full" href="https://kalidoface.com">the full Vtuber App</a>!
</p>
<script type="module" src="./script.js"></script>
</body>
</html>
+211
View File
@@ -0,0 +1,211 @@
const {
Application,
live2d: { Live2DModel },
} = PIXI;
// Kalidokit provides a simple easing function
// (linear interpolation) used for animation smoothness
// you can use a more advanced easing function if you want
const {
Face,
Vector: { lerp },
Utils: { clamp },
} = Kalidokit;
// Url to Live2D
const modelUrl = "../models/hiyori/hiyori_pro_t10.model3.json";
let currentModel, facemesh;
const videoElement = document.querySelector(".input_video"),
guideCanvas = document.querySelector("canvas.guides");
(async function main() {
// create pixi application
const app = new PIXI.Application({
view: document.getElementById("live2d"),
autoStart: true,
backgroundAlpha: 0,
backgroundColor: 0xffffff,
resizeTo: window,
});
// load live2d model
currentModel = await Live2DModel.from(modelUrl, { autoInteract: false });
currentModel.scale.set(0.4);
currentModel.interactive = true;
currentModel.anchor.set(0.5, 0.5);
currentModel.position.set(window.innerWidth * 0.5, window.innerHeight * 0.8);
// Add events to drag model
currentModel.on("pointerdown", (e) => {
currentModel.offsetX = e.data.global.x - currentModel.position.x;
currentModel.offsetY = e.data.global.y - currentModel.position.y;
currentModel.dragging = true;
});
currentModel.on("pointerup", (e) => {
currentModel.dragging = false;
});
currentModel.on("pointermove", (e) => {
if (currentModel.dragging) {
currentModel.position.set(e.data.global.x - currentModel.offsetX, e.data.global.y - currentModel.offsetY);
}
});
// Add mousewheel events to scale model
document.querySelector("#live2d").addEventListener("wheel", (e) => {
e.preventDefault();
currentModel.scale.set(clamp(currentModel.scale.x + event.deltaY * -0.001, -0.5, 10));
});
// add live2d model to stage
app.stage.addChild(currentModel);
// create media pipe facemesh instance
facemesh = new FaceMesh({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
},
});
// set facemesh config
facemesh.setOptions({
maxNumFaces: 1,
refineLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5,
});
// pass facemesh callback function
facemesh.onResults(onResults);
startCamera();
})();
const onResults = (results) => {
drawResults(results.multiFaceLandmarks[0]);
animateLive2DModel(results.multiFaceLandmarks[0]);
};
// draw connectors and landmarks on output canvas
const drawResults = (points) => {
if (!guideCanvas || !videoElement || !points) return;
guideCanvas.width = videoElement.videoWidth;
guideCanvas.height = videoElement.videoHeight;
let canvasCtx = guideCanvas.getContext("2d");
canvasCtx.save();
canvasCtx.clearRect(0, 0, guideCanvas.width, guideCanvas.height);
// Use `Mediapipe` drawing functions
drawConnectors(canvasCtx, points, FACEMESH_TESSELATION, {
color: "#C0C0C070",
lineWidth: 1,
});
if (points && points.length === 478) {
//draw pupils
drawLandmarks(canvasCtx, [points[468], points[468 + 5]], {
color: "#ffe603",
lineWidth: 2,
});
}
};
const animateLive2DModel = (points) => {
if (!currentModel || !points) return;
let riggedFace;
if (points) {
// use kalidokit face solver
riggedFace = Face.solve(points, {
runtime: "mediapipe",
video: videoElement,
});
rigFace(riggedFace, 0.5);
}
};
// update live2d model internal state
const rigFace = (result, lerpAmount = 0.7) => {
if (!currentModel || !result) return;
const coreModel = currentModel.internalModel.coreModel;
currentModel.internalModel.motionManager.update = (...args) => {
// disable default blink animation
currentModel.internalModel.eyeBlink = undefined;
coreModel.setParameterValueById(
"ParamEyeBallX",
lerp(result.pupil.x, coreModel.getParameterValueById("ParamEyeBallX"), lerpAmount)
);
coreModel.setParameterValueById(
"ParamEyeBallY",
lerp(result.pupil.y, coreModel.getParameterValueById("ParamEyeBallY"), lerpAmount)
);
// X and Y axis rotations are swapped for Live2D parameters
// because it is a 2D system and KalidoKit is a 3D system
coreModel.setParameterValueById(
"ParamAngleX",
lerp(result.head.degrees.y, coreModel.getParameterValueById("ParamAngleX"), lerpAmount)
);
coreModel.setParameterValueById(
"ParamAngleY",
lerp(result.head.degrees.x, coreModel.getParameterValueById("ParamAngleY"), lerpAmount)
);
coreModel.setParameterValueById(
"ParamAngleZ",
lerp(result.head.degrees.z, coreModel.getParameterValueById("ParamAngleZ"), lerpAmount)
);
// update body params for models without head/body param sync
const dampener = 0.3;
coreModel.setParameterValueById(
"ParamBodyAngleX",
lerp(result.head.degrees.y * dampener, coreModel.getParameterValueById("ParamBodyAngleX"), lerpAmount)
);
coreModel.setParameterValueById(
"ParamBodyAngleY",
lerp(result.head.degrees.x * dampener, coreModel.getParameterValueById("ParamBodyAngleY"), lerpAmount)
);
coreModel.setParameterValueById(
"ParamBodyAngleZ",
lerp(result.head.degrees.z * dampener, coreModel.getParameterValueById("ParamBodyAngleZ"), lerpAmount)
);
// Simple example without winking.
// Interpolate based on old blendshape, then stabilize blink with `Kalidokit` helper function.
let stabilizedEyes = Kalidokit.Face.stabilizeBlink(
{
l: lerp(result.eye.l, coreModel.getParameterValueById("ParamEyeLOpen"), 0.7),
r: lerp(result.eye.r, coreModel.getParameterValueById("ParamEyeROpen"), 0.7),
},
result.head.y
);
// eye blink
coreModel.setParameterValueById("ParamEyeLOpen", stabilizedEyes.l);
coreModel.setParameterValueById("ParamEyeROpen", stabilizedEyes.r);
// mouth
coreModel.setParameterValueById(
"ParamMouthOpenY",
lerp(result.mouth.y, coreModel.getParameterValueById("ParamMouthOpenY"), 0.3)
);
// Adding 0.3 to ParamMouthForm to make default more of a "smile"
coreModel.setParameterValueById(
"ParamMouthForm",
0.3 + lerp(result.mouth.x, coreModel.getParameterValueById("ParamMouthForm"), 0.3)
);
};
};
// start camera using mediapipe camera utils
const startCamera = () => {
const camera = new Camera(videoElement, {
onFrame: async () => {
await facemesh.send({ image: videoElement });
},
width: 640,
height: 480,
});
camera.start();
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@@ -0,0 +1,533 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "ParamGroupFace",
"Name": "Angle X"
},
{
"Id": "ParamAngleY",
"GroupId": "ParamGroupFace",
"Name": "Angle Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "ParamGroupFace",
"Name": "Angle Z"
},
{
"Id": "ParamCheek",
"GroupId": "ParamGroupFace",
"Name": "Cheek"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "ParamGroupEyes",
"Name": "Eye L Open"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "ParamGroupEyes",
"Name": "Eye L Smile"
},
{
"Id": "ParamEyeROpen",
"GroupId": "ParamGroupEyes",
"Name": "Eye R Open"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "ParamGroupEyes",
"Name": "Eye R Smile"
},
{
"Id": "ParamEyeBallX",
"GroupId": "ParamGroupEyeballs",
"Name": "Eyeball X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "ParamGroupEyeballs",
"Name": "Eyeball Y"
},
{
"Id": "ParamBrowLY",
"GroupId": "ParamGroupBrows",
"Name": "Brow L Y"
},
{
"Id": "ParamBrowRY",
"GroupId": "ParamGroupBrows",
"Name": "Brow R Y"
},
{
"Id": "ParamBrowLX",
"GroupId": "ParamGroupBrows",
"Name": "Brow L X"
},
{
"Id": "ParamBrowRX",
"GroupId": "ParamGroupBrows",
"Name": "Brow R X"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "ParamGroupBrows",
"Name": "Brow L Angle"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "ParamGroupBrows",
"Name": "Brow R Angle"
},
{
"Id": "ParamBrowLForm",
"GroupId": "ParamGroupBrows",
"Name": "Brow L Form"
},
{
"Id": "ParamBrowRForm",
"GroupId": "ParamGroupBrows",
"Name": "Brow R Form"
},
{
"Id": "ParamMouthForm",
"GroupId": "ParamGroupMouth",
"Name": "Mouth Form"
},
{
"Id": "ParamMouthOpenY",
"GroupId": "ParamGroupMouth",
"Name": "Mouth Open"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "ParamGroupBody",
"Name": "Body X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "ParamGroupBody",
"Name": "Body Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "ParamGroupBody",
"Name": "Body Z"
},
{
"Id": "ParamBreath",
"GroupId": "ParamGroupBody",
"Name": "Breath"
},
{
"Id": "ParamShoulder",
"GroupId": "ParamGroupBody",
"Name": "Shoulder"
},
{
"Id": "ParamLeg",
"GroupId": "ParamGroupBody",
"Name": "Leg"
},
{
"Id": "ParamArmLA",
"GroupId": "ParamGroupArms",
"Name": "Arm L A"
},
{
"Id": "ParamArmRA",
"GroupId": "ParamGroupArms",
"Name": "Arm R A"
},
{
"Id": "ParamArmLB",
"GroupId": "ParamGroupArms",
"Name": "Arm L B"
},
{
"Id": "ParamArmRB",
"GroupId": "ParamGroupArms",
"Name": "Arm R B"
},
{
"Id": "ParamHandLB",
"GroupId": "ParamGroupArms",
"Name": "Hand L B Rotation"
},
{
"Id": "ParamHandRB",
"GroupId": "ParamGroupArms",
"Name": "Hand R B Rotation"
},
{
"Id": "ParamHandL",
"GroupId": "ParamGroupArms",
"Name": "Hand L"
},
{
"Id": "ParamHandR",
"GroupId": "ParamGroupArms",
"Name": "Hand R"
},
{
"Id": "ParamBustY",
"GroupId": "ParamGroupSway",
"Name": "Bust Bounce"
},
{
"Id": "ParamHairAhoge",
"GroupId": "ParamGroupSway",
"Name": "Hair Move Ahoge"
},
{
"Id": "ParamHairFront",
"GroupId": "ParamGroupSway",
"Name": "Hair Move Front"
},
{
"Id": "ParamHairBack",
"GroupId": "ParamGroupSway",
"Name": "Hair Move Back"
},
{
"Id": "ParamSideupRibbon",
"GroupId": "ParamGroupSway",
"Name": "Move Side Up Ribbon"
},
{
"Id": "ParamRibbon",
"GroupId": "ParamGroupSway",
"Name": "Move Butterfly Tie"
},
{
"Id": "ParamSkirt",
"GroupId": "ParamGroupSway",
"Name": "Move Skirt"
},
{
"Id": "ParamSkirt2",
"GroupId": "ParamGroupSway",
"Name": "Curl Skirt"
},
{
"Id": "Param_Angle_Rotation_1_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[0]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_2_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[1]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_3_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[2]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_4_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[3]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_5_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[4]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_6_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[5]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_7_ArtMesh62",
"GroupId": "ParamGroup2",
"Name": "[6]Hair Side Up L"
},
{
"Id": "Param_Angle_Rotation_1_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[0]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_2_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[1]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_3_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[2]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_4_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[3]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_5_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[4]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_6_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[5]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_7_ArtMesh61",
"GroupId": "ParamGroup",
"Name": "[6]Hair Side Up R"
},
{
"Id": "Param_Angle_Rotation_1_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[0]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_2_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[1]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_3_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[2]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_4_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[3]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_5_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[4]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_6_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[5]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_7_ArtMesh55",
"GroupId": "ParamGroup4",
"Name": "[6]Hair Front L"
},
{
"Id": "Param_Angle_Rotation_1_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[0]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_2_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[1]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_3_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[2]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_4_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[3]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_5_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[4]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_6_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[5]Hair Front R"
},
{
"Id": "Param_Angle_Rotation_7_ArtMesh54",
"GroupId": "ParamGroup3",
"Name": "[6]Hair Front R"
}
],
"ParameterGroups": [
{
"Id": "ParamGroupFace",
"GroupId": "",
"Name": "Face"
},
{
"Id": "ParamGroupEyes",
"GroupId": "",
"Name": "Eye"
},
{
"Id": "ParamGroupEyeballs",
"GroupId": "",
"Name": "Eyeball"
},
{
"Id": "ParamGroupBrows",
"GroupId": "",
"Name": "Brow"
},
{
"Id": "ParamGroupMouth",
"GroupId": "",
"Name": "Mouth"
},
{
"Id": "ParamGroupBody",
"GroupId": "",
"Name": "Body"
},
{
"Id": "ParamGroupArms",
"GroupId": "",
"Name": "Arm"
},
{
"Id": "ParamGroupSway",
"GroupId": "",
"Name": "Move"
},
{
"Id": "ParamGroup2",
"GroupId": "",
"Name": "Move Hair Side Up L"
},
{
"Id": "ParamGroup",
"GroupId": "",
"Name": "Move Hair Side Up R"
},
{
"Id": "ParamGroup4",
"GroupId": "",
"Name": "Move Hair Front L"
},
{
"Id": "ParamGroup3",
"GroupId": "",
"Name": "Move Hair Front R"
}
],
"Parts": [
{
"Id": "PartCore",
"Name": "Core"
},
{
"Id": "PartCheek",
"Name": "Cheek"
},
{
"Id": "PartBrow",
"Name": "Brow"
},
{
"Id": "PartEye",
"Name": "Eye"
},
{
"Id": "PartNose",
"Name": "Nose"
},
{
"Id": "PartMouth",
"Name": "Mouth"
},
{
"Id": "PartFace",
"Name": "Face"
},
{
"Id": "PartEar",
"Name": "Ear"
},
{
"Id": "PartHairSide",
"Name": "Hair Side"
},
{
"Id": "PartHairFront",
"Name": "Hair Front"
},
{
"Id": "PartHairBack",
"Name": "Hair Back"
},
{
"Id": "PartNeck",
"Name": "Neck"
},
{
"Id": "PartArmA",
"Name": "Arm A"
},
{
"Id": "PartArmB",
"Name": "Arm B"
},
{
"Id": "PartBody",
"Name": "Body"
},
{
"Id": "PartBackground",
"Name": "Background"
},
{
"Id": "PartSketch",
"Name": "[Guide Image]"
},
{
"Id": "PartEyeBall",
"Name": "Eyeball"
},
{
"Id": "ArtMesh55_Skinning",
"Name": "Hair Front L Skinning"
},
{
"Id": "Part4",
"Name": "Hair Front L Rotation"
},
{
"Id": "ArtMesh54_Skinning",
"Name": "Hair Front R Skinning"
},
{
"Id": "Part3",
"Name": "Hair Front R Rotation"
},
{
"Id": "ArtMesh61_Skinning",
"Name": "Hair Side Up R Skinning"
},
{
"Id": "Part",
"Name": "Hair Side Up R Rotation"
},
{
"Id": "ArtMesh62_Skinning",
"Name": "Hair Side Up L Skinning"
},
{
"Id": "Part2",
"Name": "Hair Side Up L Rotation"
}
],
"CombinedParameters": [
[
"ParamAngleX",
"ParamAngleY"
],
[
"ParamMouthForm",
"ParamMouthOpenY"
]
]
}
Binary file not shown.
@@ -0,0 +1,82 @@
{
"Version": 3,
"FileReferences": {
"Moc": "hiyori_pro_t10.moc3",
"Textures": [
"hiyori_pro_t10.2048/texture_00.png",
"hiyori_pro_t10.2048/texture_01.png"
],
"Physics": "hiyori_pro_t10.physics3.json",
"Pose": "hiyori_pro_t10.pose3.json",
"DisplayInfo": "hiyori_pro_t10.cdi3.json",
"Motions": {
"Idle": [
{
"File": "motion/hiyori_m01.motion3.json"
},
{
"File": "motion/hiyori_m02.motion3.json"
},
{
"File": "motion/hiyori_m05.motion3.json"
}
],
"Flick": [
{
"File": "motion/hiyori_m03.motion3.json"
}
],
"FlickDown": [
{
"File": "motion/hiyori_m04.motion3.json"
}
],
"FlickUp": [
{
"File": "motion/hiyori_m06.motion3.json"
}
],
"Tap": [
{
"File": "motion/hiyori_m07.motion3.json"
},
{
"File": "motion/hiyori_m08.motion3.json"
}
],
"Tap@Body": [
{
"File": "motion/hiyori_m09.motion3.json"
}
],
"Flick@Body": [
{
"File": "motion/hiyori_m10.motion3.json"
}
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": [
"ParamMouthOpenY"
]
},
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
}
],
"HitAreas": [
{
"Id": "HitArea",
"Name": "Body"
}
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,15 @@
{
"Type": "Live2D Pose",
"Groups": [
[
{
"Id": "PartArmA",
"Link": []
},
{
"Id": "PartArmB",
"Link": []
}
]
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,882 @@
{
"Version": 3,
"Meta": {
"Duration": 4.43,
"Fps": 30.0,
"Loop": true,
"AreBeziersRestricted": false,
"CurveCount": 31,
"TotalSegmentCount": 106,
"TotalPointCount": 287,
"UserDataCount": 0,
"TotalUserDataSize": 0
},
"Curves": [
{
"Target": "Parameter",
"Id": "ParamAngleX",
"Segments": [
0,
1,
1,
0.211,
1,
0.422,
0,
0.633,
0,
1,
0.911,
0,
1.189,
5,
1.467,
5,
1,
1.689,
5,
1.911,
-16,
2.133,
-16,
1,
2.356,
-16,
2.578,
13.871,
2.8,
13.871,
1,
2.956,
13.871,
3.111,
0,
3.267,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamAngleY",
"Segments": [
0,
0,
1,
0.211,
0,
0.422,
0,
0.633,
0,
1,
0.911,
0,
1.189,
-25,
1.467,
-25,
1,
1.689,
-25,
1.911,
-15.225,
2.133,
-11,
1,
2.356,
-6.775,
2.578,
-5.127,
2.8,
-2.5,
1,
2.956,
-0.661,
3.111,
0,
3.267,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamAngleZ",
"Segments": [
0,
0,
1,
0.222,
0,
0.444,
0,
0.667,
0,
1,
0.756,
0,
0.844,
-4,
0.933,
-4,
1,
1.122,
-4,
1.311,
18,
1.5,
18,
1,
1.722,
18,
1.944,
-14,
2.167,
-14,
1,
2.567,
-14,
2.967,
-14,
3.367,
-14,
1,
3.511,
-14,
3.656,
-12,
3.8,
-12,
0,
4.433,
-12
]
},
{
"Target": "Parameter",
"Id": "ParamCheek",
"Segments": [
0,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLOpen",
"Segments": [
0,
1.2,
1,
0.389,
1.2,
0.778,
1.148,
1.167,
1,
1,
1.211,
0.983,
1.256,
0,
1.3,
0,
1,
1.322,
0,
1.344,
0,
1.367,
0,
1,
1.422,
0,
1.478,
1,
1.533,
1,
1,
1.944,
1,
2.356,
1,
2.767,
1,
1,
2.811,
1,
2.856,
0,
2.9,
0,
1,
2.922,
0,
2.944,
0,
2.967,
0,
1,
3.022,
0,
3.078,
1,
3.133,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLSmile",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeROpen",
"Segments": [
0,
1.2,
1,
0.389,
1.2,
0.778,
1.148,
1.167,
1,
1,
1.211,
0.983,
1.256,
0,
1.3,
0,
1,
1.322,
0,
1.344,
0,
1.367,
0,
1,
1.422,
0,
1.478,
1,
1.533,
1,
1,
1.944,
1,
2.356,
1,
2.767,
1,
1,
2.811,
1,
2.856,
0,
2.9,
0,
1,
2.922,
0,
2.944,
0,
2.967,
0,
1,
3.022,
0,
3.078,
1,
3.133,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamEyeRSmile",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallX",
"Segments": [
0,
0,
1,
0.211,
0,
0.422,
0,
0.633,
0,
1,
0.911,
0,
1.189,
-0.44,
1.467,
-0.44,
1,
1.689,
-0.44,
1.911,
0.79,
2.133,
0.79,
1,
2.511,
0.79,
2.889,
0,
3.267,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallY",
"Segments": [
0,
0,
1,
0.211,
0,
0.422,
0,
0.633,
0,
1,
0.911,
0,
1.189,
-1,
1.467,
-1,
1,
1.689,
-1,
1.911,
-1,
2.133,
-1,
1,
2.511,
-1,
2.889,
0,
3.267,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLY",
"Segments": [
0,
1,
1,
0.544,
1,
1.089,
1,
1.633,
1,
1,
1.856,
1,
2.078,
0,
2.3,
0,
1,
2.5,
0,
2.7,
1,
2.9,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRY",
"Segments": [
0,
1,
1,
0.544,
1,
1.089,
1,
1.633,
1,
1,
1.856,
1,
2.078,
0,
2.3,
0,
1,
2.5,
0,
2.7,
1,
2.9,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLX",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRX",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLAngle",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRAngle",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLForm",
"Segments": [
0,
-1,
0,
4.433,
-1
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRForm",
"Segments": [
0,
-1,
0,
4.433,
-1
]
},
{
"Target": "Parameter",
"Id": "ParamMouthForm",
"Segments": [
0,
-2,
0,
4.433,
-2
]
},
{
"Target": "Parameter",
"Id": "ParamMouthOpenY",
"Segments": [
0,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleX",
"Segments": [
0,
0,
1,
0.244,
0,
0.489,
0,
0.733,
0,
1,
0.933,
0,
1.133,
-7,
1.333,
-7,
1,
1.644,
-7,
1.956,
0,
2.267,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleY",
"Segments": [
0,
0,
1,
0.244,
0,
0.489,
0,
0.733,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleZ",
"Segments": [
0,
2,
1,
0.233,
2,
0.467,
0,
0.7,
0,
1,
0.733,
0,
0.767,
0,
0.8,
0,
1,
1,
0,
1.2,
-4,
1.4,
-4,
1,
1.711,
-4,
2.022,
5,
2.333,
5,
1,
2.567,
5,
2.8,
3.64,
3.033,
0,
1,
3.133,
-1.56,
3.233,
-3,
3.333,
-3,
1,
3.467,
-3,
3.6,
-2,
3.733,
-2,
0,
4.433,
-2
]
},
{
"Target": "Parameter",
"Id": "ParamBreath",
"Segments": [
0,
0,
1,
0.189,
0,
0.378,
1,
0.567,
1,
1,
0.711,
1,
0.856,
0,
1,
0,
1,
1.222,
0,
1.444,
1,
1.667,
1,
1,
1.889,
1,
2.111,
0,
2.333,
0,
1,
2.544,
0,
2.756,
1,
2.967,
1,
1,
3.167,
1,
3.367,
0,
3.567,
0,
0,
4.433,
0
]
},
{
"Target": "Parameter",
"Id": "ParamShoulder",
"Segments": [
0,
0.1,
1,
0.467,
0.1,
0.933,
1,
1.4,
1,
1,
1.844,
1,
2.289,
1,
2.733,
1,
1,
2.967,
1,
3.2,
-1,
3.433,
-1,
0,
4.433,
-1
]
},
{
"Target": "Parameter",
"Id": "ParamLeg",
"Segments": [
0,
1,
0,
4.433,
1
]
},
{
"Target": "Parameter",
"Id": "ParamArmLA",
"Segments": [
0,
-10,
0,
4.433,
-10
]
},
{
"Target": "Parameter",
"Id": "ParamArmRA",
"Segments": [
0,
-10,
0,
4.433,
-10
]
},
{
"Target": "Parameter",
"Id": "ParamHairAhoge",
"Segments": [
0,
0,
1,
0.3,
0,
0.6,
0,
0.9,
-0.012,
1,
1.067,
-0.019,
1.233,
-6.827,
1.4,
-6.827,
1,
1.511,
-6.827,
1.622,
7.958,
1.733,
7.958,
1,
1.944,
7.958,
2.156,
-7.565,
2.367,
-7.565,
1,
2.5,
-7.565,
2.633,
9.434,
2.767,
9.434,
1,
2.978,
9.434,
3.189,
-8.871,
3.4,
-8.871,
1,
3.5,
-8.871,
3.6,
7.588,
3.7,
7.588,
1,
3.789,
7.588,
3.878,
-3.904,
3.967,
-3.904,
1,
4.011,
-3.904,
4.056,
-0.032,
4.1,
-0.032,
0,
4.433,
-0.032
]
},
{
"Target": "PartOpacity",
"Id": "PartArmA",
"Segments": [
0,
1,
0,
4.43,
1
]
},
{
"Target": "PartOpacity",
"Id": "PartArmB",
"Segments": [
0,
0,
0,
4.43,
0
]
}
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,991 @@
{
"Version": 3,
"Meta": {
"Duration": 1.9,
"Fps": 30.0,
"Loop": true,
"AreBeziersRestricted": false,
"CurveCount": 32,
"TotalSegmentCount": 121,
"TotalPointCount": 331,
"UserDataCount": 0,
"TotalUserDataSize": 0
},
"Curves": [
{
"Target": "Parameter",
"Id": "ParamAngleX",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamAngleY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamAngleZ",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.444,
0,
0.556,
8,
0.667,
8,
0,
1.9,
8
]
},
{
"Target": "Parameter",
"Id": "ParamCheek",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLOpen",
"Segments": [
0,
1,
1,
0.111,
1,
0.222,
1,
0.333,
1,
1,
0.378,
1,
0.422,
0,
0.467,
0,
1,
0.522,
0,
0.578,
1.2,
0.633,
1.2,
1,
0.744,
1.2,
0.856,
1.2,
0.967,
1.2,
1,
0.989,
1.2,
1.011,
0,
1.033,
0,
1,
1.067,
0,
1.1,
1.2,
1.133,
1.2,
1,
1.167,
1.2,
1.2,
1.2,
1.233,
1.2,
1,
1.267,
1.2,
1.3,
0,
1.333,
0,
1,
1.356,
0,
1.378,
1.2,
1.4,
1.2,
0,
1.9,
1.2
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLSmile",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeROpen",
"Segments": [
0,
1,
1,
0.111,
1,
0.222,
1,
0.333,
1,
1,
0.378,
1,
0.422,
0,
0.467,
0,
1,
0.522,
0,
0.578,
1.2,
0.633,
1.2,
1,
0.744,
1.2,
0.856,
1.2,
0.967,
1.2,
1,
0.989,
1.2,
1.011,
0,
1.033,
0,
1,
1.067,
0,
1.1,
1.2,
1.133,
1.2,
1,
1.167,
1.2,
1.2,
1.2,
1.233,
1.2,
1,
1.267,
1.2,
1.3,
0,
1.333,
0,
1,
1.356,
0,
1.378,
1.2,
1.4,
1.2,
0,
1.9,
1.2
]
},
{
"Target": "Parameter",
"Id": "ParamEyeRSmile",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0,
0.667,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallX",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0,
0.667,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0,
0.667,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.26,
0.667,
0.26,
0,
1.9,
0.26
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.36,
0.667,
0.36,
0,
1.9,
0.36
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLX",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0,
0.667,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRX",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.27,
0.667,
0.27,
0,
1.9,
0.27
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLAngle",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.26,
0.667,
0.26,
0,
1.9,
0.26
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRAngle",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
-0.03,
0.667,
-0.03,
0,
1.9,
-0.03
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLForm",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.33,
0.667,
0.33,
0,
1.9,
0.33
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRForm",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.21,
0.667,
0.21,
0,
1.9,
0.21
]
},
{
"Target": "Parameter",
"Id": "ParamMouthForm",
"Segments": [
0,
1,
1,
0.111,
1,
0.222,
1,
0.333,
1,
1,
0.356,
1,
0.378,
1,
0.4,
1,
1,
0.489,
1,
0.578,
-2,
0.667,
-2,
0,
1.9,
-2
]
},
{
"Target": "Parameter",
"Id": "ParamMouthOpenY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.356,
0,
0.378,
0,
0.4,
0,
1,
0.489,
0,
0.578,
0.75,
0.667,
0.75,
0,
1.9,
0.75
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleX",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.444,
0,
0.556,
-6,
0.667,
-6,
0,
1.9,
-6
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleY",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.422,
0,
0.511,
10,
0.6,
10,
1,
0.667,
10,
0.733,
-6,
0.8,
-6,
1,
0.833,
-6,
0.867,
5,
0.9,
5,
1,
1.011,
5,
1.122,
0,
1.233,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleZ",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.444,
0,
0.556,
-3,
0.667,
-3,
0,
1.9,
-3
]
},
{
"Target": "Parameter",
"Id": "ParamBreath",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamShoulder",
"Segments": [
0,
-0.062,
1,
0.111,
-0.062,
0.222,
-0.103,
0.333,
0,
1,
0.589,
0.238,
0.844,
1,
1.1,
1,
0,
1.9,
1
]
},
{
"Target": "Parameter",
"Id": "ParamArmLA",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.478,
0,
0.622,
-10,
0.767,
-10,
1,
0.811,
-10,
0.856,
-8.2,
0.9,
-8.2,
0,
1.9,
-8.2
]
},
{
"Target": "Parameter",
"Id": "ParamArmRA",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
1,
0.478,
0,
0.622,
-10,
0.767,
-10,
1,
0.811,
-10,
0.856,
-7.2,
0.9,
-7.2,
0,
1.9,
-7.2
]
},
{
"Target": "Parameter",
"Id": "ParamArmLB",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamArmRB",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
0,
0.333,
0,
0,
1.9,
0
]
},
{
"Target": "Parameter",
"Id": "ParamHairAhoge",
"Segments": [
0,
0,
1,
0.111,
0,
0.222,
1.9,
0.333,
5.2,
1,
0.444,
8.5,
0.556,
9.926,
0.667,
9.926,
1,
0.744,
9.926,
0.822,
-10,
0.9,
-10,
1,
0.956,
-10,
1.011,
6,
1.067,
6,
1,
1.144,
6,
1.222,
-4,
1.3,
-4,
1,
1.367,
-4,
1.433,
0,
1.5,
0,
0,
1.9,
0
]
},
{
"Target": "PartOpacity",
"Id": "PartArmA",
"Segments": [
0,
1,
0,
1.9,
1
]
},
{
"Target": "PartOpacity",
"Id": "PartArmB",
"Segments": [
0,
0,
0,
1.9,
0
]
}
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,974 @@
{
"Version": 3,
"Meta": {
"Duration": 4.17,
"Fps": 30.0,
"Loop": true,
"AreBeziersRestricted": false,
"CurveCount": 33,
"TotalSegmentCount": 118,
"TotalPointCount": 321,
"UserDataCount": 0,
"TotalUserDataSize": 0
},
"Curves": [
{
"Target": "Parameter",
"Id": "ParamAngleX",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.4,
0,
0.6,
0,
0.8,
0,
1,
1.067,
0,
1.333,
1.041,
1.6,
1.041,
1,
1.844,
1.041,
2.089,
-8,
2.333,
-8,
1,
2.656,
-8,
2.978,
6,
3.3,
6,
0,
4.167,
6
]
},
{
"Target": "Parameter",
"Id": "ParamAngleY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.344,
0,
0.489,
-30,
0.633,
-30,
0,
4.167,
-30
]
},
{
"Target": "Parameter",
"Id": "ParamAngleZ",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamCheek",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLOpen",
"Segments": [
0,
1,
1,
0.067,
1,
0.133,
1,
0.2,
1,
1,
0.311,
1,
0.422,
0.988,
0.533,
0.8,
1,
0.589,
0.706,
0.644,
0,
0.7,
0,
1,
0.722,
0,
0.744,
0,
0.767,
0,
1,
0.822,
0,
0.878,
0.8,
0.933,
0.8,
1,
1.422,
0.8,
1.911,
0.8,
2.4,
0.8,
1,
2.456,
0.8,
2.511,
0,
2.567,
0,
1,
2.589,
0,
2.611,
0,
2.633,
0,
1,
2.689,
0,
2.744,
0.8,
2.8,
0.8,
0,
4.167,
0.8
]
},
{
"Target": "Parameter",
"Id": "ParamEyeLSmile",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeROpen",
"Segments": [
0,
1,
1,
0.067,
1,
0.133,
1,
0.2,
1,
1,
0.311,
1,
0.422,
0.988,
0.533,
0.8,
1,
0.589,
0.706,
0.644,
0,
0.7,
0,
1,
0.722,
0,
0.744,
0,
0.767,
0,
1,
0.822,
0,
0.878,
0.8,
0.933,
0.8,
1,
1.422,
0.8,
1.911,
0.8,
2.4,
0.8,
1,
2.456,
0.8,
2.511,
0,
2.567,
0,
1,
2.589,
0,
2.611,
0,
2.633,
0,
1,
2.689,
0,
2.744,
0.8,
2.8,
0.8,
0,
4.167,
0.8
]
},
{
"Target": "Parameter",
"Id": "ParamEyeRSmile",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0,
0.433,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallX",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0,
0.433,
0,
1,
0.667,
0,
0.9,
0.004,
1.133,
-0.01,
1,
1.4,
-0.025,
1.667,
-0.43,
1.933,
-0.43,
1,
2.211,
-0.43,
2.489,
0.283,
2.767,
0.283,
0,
4.167,
0.283
]
},
{
"Target": "Parameter",
"Id": "ParamEyeBallY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
-1,
0.433,
-1,
0,
4.167,
-1
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0.19,
0.433,
0.19,
0,
4.167,
0.19
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0.11,
0.433,
0.11,
0,
4.167,
0.11
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLX",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
-0.48,
0.433,
-0.48,
0,
4.167,
-0.48
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRX",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0.29,
0.433,
0.29,
0,
4.167,
0.29
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLAngle",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
1,
0.433,
1,
0,
4.167,
1
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRAngle",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
0.85,
0.433,
0.85,
0,
4.167,
0.85
]
},
{
"Target": "Parameter",
"Id": "ParamBrowLForm",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
-0.75,
0.433,
-0.75,
0,
4.167,
-0.75
]
},
{
"Target": "Parameter",
"Id": "ParamBrowRForm",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
-0.87,
0.433,
-0.87,
0,
4.167,
-0.87
]
},
{
"Target": "Parameter",
"Id": "ParamMouthForm",
"Segments": [
0,
1,
1,
0.067,
1,
0.133,
1,
0.2,
1,
1,
0.278,
1,
0.356,
-1,
0.433,
-1,
0,
4.167,
-1
]
},
{
"Target": "Parameter",
"Id": "ParamMouthOpenY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.278,
0,
0.356,
1,
0.433,
1,
0,
4.167,
1
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleX",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.444,
0,
0.689,
0,
0.933,
0,
1,
1.211,
0,
1.489,
0,
1.767,
0,
1,
2.056,
0,
2.344,
-6,
2.633,
-6,
1,
3.033,
-6,
3.433,
10,
3.833,
10,
0,
4.167,
10
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleY",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamBodyAngleZ",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.8,
0,
1.4,
-2,
2,
-2,
1,
2.456,
-2,
2.911,
8.125,
3.367,
8.125,
0,
4.167,
8.125
]
},
{
"Target": "Parameter",
"Id": "ParamBreath",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamShoulder",
"Segments": [
0,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamLeg",
"Segments": [
0,
1,
1,
0.667,
1,
1.333,
1,
2,
1,
1,
2.267,
1,
2.533,
0.948,
2.8,
0.948,
0,
4.167,
0.948
]
},
{
"Target": "Parameter",
"Id": "ParamArmLA",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.233,
0,
0.267,
0,
0.3,
0,
1,
0.478,
0,
0.656,
-10,
0.833,
-10,
1,
0.922,
-10,
1.011,
-8.846,
1.1,
-8.846,
1,
1.467,
-8.846,
1.833,
-8.835,
2.2,
-9.1,
1,
2.622,
-9.405,
3.044,
-10,
3.467,
-10,
0,
4.167,
-10
]
},
{
"Target": "Parameter",
"Id": "ParamArmRA",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.233,
0,
0.267,
0,
0.3,
0,
1,
0.478,
0,
0.656,
-10,
0.833,
-10,
1,
0.922,
-10,
1.011,
-8.972,
1.1,
-8.846,
1,
1.467,
-8.328,
1.833,
-8.2,
2.2,
-8.2,
1,
2.622,
-8.2,
3.044,
-10,
3.467,
-10,
0,
4.167,
-10
]
},
{
"Target": "Parameter",
"Id": "ParamArmLB",
"Segments": [
0,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamArmRB",
"Segments": [
0,
0,
0,
4.167,
0
]
},
{
"Target": "Parameter",
"Id": "ParamHairAhoge",
"Segments": [
0,
0,
1,
0.067,
0,
0.133,
0,
0.2,
0,
1,
0.233,
0,
0.267,
-5,
0.3,
-5,
1,
0.378,
-5,
0.456,
10,
0.533,
10,
1,
0.633,
10,
0.733,
4,
0.833,
4,
0,
4.167,
4
]
},
{
"Target": "PartOpacity",
"Id": "PartArmA",
"Segments": [
0,
1,
0,
4.17,
1
]
},
{
"Target": "PartOpacity",
"Id": "PartArmB",
"Segments": [
0,
0,
0,
4.17,
0
]
}
]
}
+330
View File
@@ -0,0 +1,330 @@
import * as Kalidokit from "../dist";
//Import Helper Functions from Kalidokit
const remap = Kalidokit.Utils.remap;
const clamp = Kalidokit.Utils.clamp;
const lerp = Kalidokit.Vector.lerp;
/* THREEJS WORLD SETUP */
let currentVrm;
// renderer
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// camera
const orbitCamera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
orbitCamera.position.set(0.0, 1.4, 0.7);
// controls
const orbitControls = new THREE.OrbitControls(orbitCamera, renderer.domElement);
orbitControls.screenSpacePanning = true;
orbitControls.target.set(0.0, 1.4, 0.0);
orbitControls.update();
// scene
const scene = new THREE.Scene();
// light
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(1.0, 1.0, 1.0).normalize();
scene.add(light);
// Main Render Loop
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
if (currentVrm) {
// Update model to render physics
currentVrm.update(clock.getDelta());
}
renderer.render(scene, orbitCamera);
}
animate();
/* VRM CHARACTER SETUP */
// Import Character VRM
const loader = new THREE.GLTFLoader();
loader.crossOrigin = "anonymous";
// Import model from URL, add your own model here
loader.load(
"https://cdn.glitch.com/29e07830-2317-4b15-a044-135e73c7f840%2FAshtra.vrm?v=1630342336981",
(gltf) => {
THREE.VRMUtils.removeUnnecessaryJoints(gltf.scene);
THREE.VRM.from(gltf).then((vrm) => {
scene.add(vrm.scene);
currentVrm = vrm;
currentVrm.scene.rotation.y = Math.PI; // Rotate model 180deg to face camera
});
},
(progress) => console.log("Loading model...", 100.0 * (progress.loaded / progress.total), "%"),
(error) => console.error(error)
);
// Animate Rotation Helper function
const rigRotation = (name, rotation = { x: 0, y: 0, z: 0 }, dampener = 1, lerpAmount = 0.3) => {
if (!currentVrm) {
return;
}
const Part = currentVrm.humanoid.getBoneNode(THREE.VRMSchema.HumanoidBoneName[name]);
if (!Part) {
return;
}
let euler = new THREE.Euler(
rotation.x * dampener,
rotation.y * dampener,
rotation.z * dampener,
rotation.rotationOrder || "XYZ"
);
let quaternion = new THREE.Quaternion().setFromEuler(euler);
Part.quaternion.slerp(quaternion, lerpAmount); // interpolate
};
// Animate Position Helper Function
const rigPosition = (name, position = { x: 0, y: 0, z: 0 }, dampener = 1, lerpAmount = 0.3) => {
if (!currentVrm) {
return;
}
const Part = currentVrm.humanoid.getBoneNode(THREE.VRMSchema.HumanoidBoneName[name]);
if (!Part) {
return;
}
let vector = new THREE.Vector3(position.x * dampener, position.y * dampener, position.z * dampener);
Part.position.lerp(vector, lerpAmount); // interpolate
};
let oldLookTarget = new THREE.Euler();
const rigFace = (riggedFace) => {
if (!currentVrm) {
return;
}
rigRotation("Neck", riggedFace.head, 0.7);
// Blendshapes and Preset Name Schema
const Blendshape = currentVrm.blendShapeProxy;
const PresetName = THREE.VRMSchema.BlendShapePresetName;
// Simple example without winking. Interpolate based on old blendshape, then stabilize blink with `Kalidokit` helper function.
// for VRM, 1 is closed, 0 is open.
riggedFace.eye.l = lerp(clamp(1 - riggedFace.eye.l, 0, 1), Blendshape.getValue(PresetName.Blink), 0.5);
riggedFace.eye.r = lerp(clamp(1 - riggedFace.eye.r, 0, 1), Blendshape.getValue(PresetName.Blink), 0.5);
riggedFace.eye = Kalidokit.Face.stabilizeBlink(riggedFace.eye, riggedFace.head.y);
Blendshape.setValue(PresetName.Blink, riggedFace.eye.l);
// Interpolate and set mouth blendshapes
Blendshape.setValue(PresetName.I, lerp(riggedFace.mouth.shape.I, Blendshape.getValue(PresetName.I), 0.5));
Blendshape.setValue(PresetName.A, lerp(riggedFace.mouth.shape.A, Blendshape.getValue(PresetName.A), 0.5));
Blendshape.setValue(PresetName.E, lerp(riggedFace.mouth.shape.E, Blendshape.getValue(PresetName.E), 0.5));
Blendshape.setValue(PresetName.O, lerp(riggedFace.mouth.shape.O, Blendshape.getValue(PresetName.O), 0.5));
Blendshape.setValue(PresetName.U, lerp(riggedFace.mouth.shape.U, Blendshape.getValue(PresetName.U), 0.5));
//PUPILS
//interpolate pupil and keep a copy of the value
let lookTarget = new THREE.Euler(
lerp(oldLookTarget.x, riggedFace.pupil.y, 0.4),
lerp(oldLookTarget.y, riggedFace.pupil.x, 0.4),
0,
"XYZ"
);
oldLookTarget.copy(lookTarget);
currentVrm.lookAt.applyer.lookAt(lookTarget);
};
/* VRM Character Animator */
const animateVRM = (vrm, results) => {
if (!vrm) {
return;
}
// Take the results from `Holistic` and animate character based on its Face, Pose, and Hand Keypoints.
let riggedPose, riggedLeftHand, riggedRightHand, riggedFace;
const faceLandmarks = results.faceLandmarks;
// Pose 3D Landmarks are with respect to Hip distance in meters
const pose3DLandmarks = results.ea;
// Pose 2D landmarks are with respect to videoWidth and videoHeight
const pose2DLandmarks = results.poseLandmarks;
// Be careful, hand landmarks may be reversed
const leftHandLandmarks = results.rightHandLandmarks;
const rightHandLandmarks = results.leftHandLandmarks;
// Animate Face
if (faceLandmarks) {
riggedFace = Kalidokit.Face.solve(faceLandmarks, {
runtime: "mediapipe",
video: videoElement,
});
rigFace(riggedFace);
}
// Animate Pose
if (pose2DLandmarks && pose3DLandmarks) {
riggedPose = Kalidokit.Pose.solve(pose3DLandmarks, pose2DLandmarks, {
runtime: "mediapipe",
video: videoElement,
});
rigRotation("Hips", riggedPose.Hips.rotation, 0.7);
rigPosition(
"Hips",
{
x: riggedPose.Hips.position.x, // Reverse direction
y: riggedPose.Hips.position.y + 1, // Add a bit of height
z: -riggedPose.Hips.position.z, // Reverse direction
},
1,
0.07
);
rigRotation("Chest", riggedPose.Spine, 0.25, 0.3);
rigRotation("Spine", riggedPose.Spine, 0.45, 0.3);
rigRotation("RightUpperArm", riggedPose.RightUpperArm, 1, 0.3);
rigRotation("RightLowerArm", riggedPose.RightLowerArm, 1, 0.3);
rigRotation("LeftUpperArm", riggedPose.LeftUpperArm, 1, 0.3);
rigRotation("LeftLowerArm", riggedPose.LeftLowerArm, 1, 0.3);
rigRotation("LeftUpperLeg", riggedPose.LeftUpperLeg, 1, 0.3);
rigRotation("LeftLowerLeg", riggedPose.LeftLowerLeg, 1, 0.3);
rigRotation("RightUpperLeg", riggedPose.RightUpperLeg, 1, 0.3);
rigRotation("RightLowerLeg", riggedPose.RightLowerLeg, 1, 0.3);
}
// Animate Hands
if (leftHandLandmarks) {
riggedLeftHand = Kalidokit.Hand.solve(leftHandLandmarks, "Left");
rigRotation("LeftHand", {
// Combine pose rotation Z and hand rotation X Y
z: riggedPose.LeftHand.z,
y: riggedLeftHand.LeftWrist.y,
x: riggedLeftHand.LeftWrist.x,
});
rigRotation("LeftRingProximal", riggedLeftHand.LeftRingProximal);
rigRotation("LeftRingIntermediate", riggedLeftHand.LeftRingIntermediate);
rigRotation("LeftRingDistal", riggedLeftHand.LeftRingDistal);
rigRotation("LeftIndexProximal", riggedLeftHand.LeftIndexProximal);
rigRotation("LeftIndexIntermediate", riggedLeftHand.LeftIndexIntermediate);
rigRotation("LeftIndexDistal", riggedLeftHand.LeftIndexDistal);
rigRotation("LeftMiddleProximal", riggedLeftHand.LeftMiddleProximal);
rigRotation("LeftMiddleIntermediate", riggedLeftHand.LeftMiddleIntermediate);
rigRotation("LeftMiddleDistal", riggedLeftHand.LeftMiddleDistal);
rigRotation("LeftThumbProximal", riggedLeftHand.LeftThumbProximal);
rigRotation("LeftThumbIntermediate", riggedLeftHand.LeftThumbIntermediate);
rigRotation("LeftThumbDistal", riggedLeftHand.LeftThumbDistal);
rigRotation("LeftLittleProximal", riggedLeftHand.LeftLittleProximal);
rigRotation("LeftLittleIntermediate", riggedLeftHand.LeftLittleIntermediate);
rigRotation("LeftLittleDistal", riggedLeftHand.LeftLittleDistal);
}
if (rightHandLandmarks) {
riggedRightHand = Kalidokit.Hand.solve(rightHandLandmarks, "Right");
rigRotation("RightHand", {
// Combine Z axis from pose hand and X/Y axis from hand wrist rotation
z: riggedPose.RightHand.z,
y: riggedRightHand.RightWrist.y,
x: riggedRightHand.RightWrist.x,
});
rigRotation("RightRingProximal", riggedRightHand.RightRingProximal);
rigRotation("RightRingIntermediate", riggedRightHand.RightRingIntermediate);
rigRotation("RightRingDistal", riggedRightHand.RightRingDistal);
rigRotation("RightIndexProximal", riggedRightHand.RightIndexProximal);
rigRotation("RightIndexIntermediate", riggedRightHand.RightIndexIntermediate);
rigRotation("RightIndexDistal", riggedRightHand.RightIndexDistal);
rigRotation("RightMiddleProximal", riggedRightHand.RightMiddleProximal);
rigRotation("RightMiddleIntermediate", riggedRightHand.RightMiddleIntermediate);
rigRotation("RightMiddleDistal", riggedRightHand.RightMiddleDistal);
rigRotation("RightThumbProximal", riggedRightHand.RightThumbProximal);
rigRotation("RightThumbIntermediate", riggedRightHand.RightThumbIntermediate);
rigRotation("RightThumbDistal", riggedRightHand.RightThumbDistal);
rigRotation("RightLittleProximal", riggedRightHand.RightLittleProximal);
rigRotation("RightLittleIntermediate", riggedRightHand.RightLittleIntermediate);
rigRotation("RightLittleDistal", riggedRightHand.RightLittleDistal);
}
};
/* SETUP MEDIAPIPE HOLISTIC INSTANCE */
let videoElement = document.querySelector(".input_video"),
guideCanvas = document.querySelector("canvas.guides");
const onResults = (results) => {
// Draw landmark guides
drawResults(results);
// Animate model
animateVRM(currentVrm, results);
};
const holistic = new Holistic({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/${file}`;
},
});
holistic.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7,
refineFaceLandmarks: true,
});
// Pass holistic a callback function
holistic.onResults(onResults);
const drawResults = (results) => {
guideCanvas.width = videoElement.videoWidth;
guideCanvas.height = videoElement.videoHeight;
let canvasCtx = guideCanvas.getContext("2d");
canvasCtx.save();
canvasCtx.clearRect(0, 0, guideCanvas.width, guideCanvas.height);
// Use `Mediapipe` drawing functions
drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS, {
color: "#00cff7",
lineWidth: 4,
});
drawLandmarks(canvasCtx, results.poseLandmarks, {
color: "#ff0364",
lineWidth: 2,
});
drawConnectors(canvasCtx, results.faceLandmarks, FACEMESH_TESSELATION, {
color: "#C0C0C070",
lineWidth: 1,
});
if (results.faceLandmarks && results.faceLandmarks.length === 478) {
//draw pupils
drawLandmarks(canvasCtx, [results.faceLandmarks[468], results.faceLandmarks[468 + 5]], {
color: "#ffe603",
lineWidth: 2,
});
}
drawConnectors(canvasCtx, results.leftHandLandmarks, HAND_CONNECTIONS, {
color: "#eb1064",
lineWidth: 5,
});
drawLandmarks(canvasCtx, results.leftHandLandmarks, {
color: "#00cff7",
lineWidth: 2,
});
drawConnectors(canvasCtx, results.rightHandLandmarks, HAND_CONNECTIONS, {
color: "#22c3e3",
lineWidth: 5,
});
drawLandmarks(canvasCtx, results.rightHandLandmarks, {
color: "#ff0364",
lineWidth: 2,
});
};
// Use `Mediapipe` utils to get camera - lower resolution = higher fps
const camera = new Camera(videoElement, {
onFrame: async () => {
await holistic.send({ image: videoElement });
},
width: 640,
height: 480,
});
camera.start();
+124
View File
@@ -0,0 +1,124 @@
@font-face {
font-family: "Jelly";
font-display: block;
src: url("https://yeemachine.github.io/kalidoface-live2d-models/Font/kalidoface-jelly.ttf") format("truetype");
}
h1 {
font-family: "Jelly";
font-size: 32px;
color: white;
position: absolute;
top: -12px;
left: 16px;
font-weight: normal;
-webkit-font-feature-settings: "liga";
font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1:hover {
color: #13a3f3;
}
a,
a:visited,
a:link {
text-decoration: none;
color: #222;
display: inline-block;
}
a:hover,
a:visited:hover,
a:link:hover {
color: #13a3f3;
cursor: pointer;
}
.preview {
display: flex;
flex-direction: column;
position: absolute;
bottom: 16px;
right: 16px;
overflow: hidden;
border-radius: 8px;
background: #222;
}
video {
max-width: 400px;
height: auto;
transform: scale(-1, 1);
}
.preview canvas {
transform: scale(-1, 1);
}
body {
margin: 0;
}
canvas {
display: block;
}
.guides {
position: absolute;
bottom: 0;
left: 0;
height: auto;
width: 100%;
z-index: 1;
}
nav {
position: absolute;
top: 12px;
right: 16px;
display: flex;
}
nav img {
width: 36px;
height: auto;
margin-left: 22px;
}
section {
position: absolute;
bottom: 0;
right: 0;
padding: 32px 0 16px 0;
display: flex;
justify-content: center;
width: 100%;
background: rgb(0, 0, 0);
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
color: white;
z-index: 2;
/* border-radius: 40px; */
}
p {
color: white;
padding: 8px 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue",
sans-serif;
margin: 0;
}
.preview a {
border-radius: 24px;
margin: 0 4px;
}
.preview a:hover {
background: #ffffff20;
}
a.current {
background: #13a3f3;
pointer-events: none;
}
.linkOut {
position: absolute;
bottom: 16px;
left: 16px;
display: inline;
color: black;
padding: 0;
}
@media only screen and (max-width: 600px) {
video {
max-width: 160px;
}
}
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+10024
View File
File diff suppressed because it is too large. Load diff
+61
View File
@@ -0,0 +1,61 @@
{
"name": "kalidokit",
"version": "1.1.5",
"repository": "github:yeemachine/kalidokit",
"main": "dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"dev": "vite docs",
"prebuild": "npm run fmt && rimraf dist",
"build": "vite build && npm run build:lib",
"build:watch": "vite build --watch && npm run build:lib:watch",
"build:lib": "tsc -p .",
"build:lib:watch": "tsc-watch -p .",
"serve": "vite",
"prepare": "npm run build",
"fmt": "prettier --write \"src/**/*.ts\"",
"test": "npm run build && npm run dev",
"lint": "eslint \"src/**/*.ts\" --fix",
"jest": "jest",
"jest:watch": "jest -o --watch "
},
"devDependencies": {
"@types/jest": "^27.0.3",
"@types/lodash.clonedeep": "^4.5.6",
"lodash.clonedeep": "^4.5.0",
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"rollup-plugin-friendly-type-imports": "^1.0.1",
"ts-jest": "^27.1.1",
"tsc-watch": "^4.5.0",
"typescript": "^4.4.4",
"vite": "2.7.1",
"vite-plugin-banner": "0.1.3",
"vite-plugin-ts": "^1.1.8"
},
"prettier": {
"tabWidth": 4,
"trailingComma": "es5",
"printWidth": 120,
"singleQuote": false
},
"description": "Blendshape and kinematics calculator for Mediapipe/Tensorflow.js Face, Eyes, Pose, and Finger tracking models.",
"bugs": {
"url": "https://github.com/yeemachine/kalidokit/issues"
},
"homepage": "https://github.com/yeemachine/kalidokit#readme",
"keywords": [
"mediapipe",
"tensorflow",
"face-tracking",
"pose-tracking",
"finger-tracking",
"detection"
],
"author": "yeemachine",
"license": "MIT",
"year": "2020-2021"
}
+277
View File
@@ -0,0 +1,277 @@
import Vector from "../utils/vector";
import { clamp, remap } from "../utils/helpers";
import { Results, XYZ, Side } from "../Types";
import { RIGHT, LEFT } from "./../constants";
/**
* Landmark points labeled for eye, brow, and pupils
*/
const points = {
eye: {
[LEFT]: [130, 133, 160, 159, 158, 144, 145, 153],
[RIGHT]: [263, 362, 387, 386, 385, 373, 374, 380],
},
brow: {
[LEFT]: [35, 244, 63, 105, 66, 229, 230, 231],
[RIGHT]: [265, 464, 293, 334, 296, 449, 450, 451],
},
pupil: {
[LEFT]: [468, 469, 470, 471, 472],
[RIGHT]: [473, 474, 475, 476, 477],
},
};
/**
* Calculate eye open ratios and remap to 0-1
* @param {Array} lm : array of results from tfjs or mediapipe
* @param {Side} side : designate left or right
* @param {Number} high : ratio at which eye is considered open
* @param {Number} low : ratio at which eye is comsidered closed
*/
export const getEyeOpen = (lm: Results, side: Side = LEFT, { high = 0.85, low = 0.55 } = {}) => {
const eyePoints = points.eye[side];
const eyeDistance = eyeLidRatio(
lm[eyePoints[0]],
lm[eyePoints[1]],
lm[eyePoints[2]],
lm[eyePoints[3]],
lm[eyePoints[4]],
lm[eyePoints[5]],
lm[eyePoints[6]],
lm[eyePoints[7]]
);
// human eye width to height ratio is roughly .3
const maxRatio = 0.285;
// compare ratio against max ratio
const ratio = clamp(eyeDistance / maxRatio, 0, 2);
// remap eye open and close ratios to increase sensitivity
const eyeOpenRatio = remap(ratio, low, high);
return {
// remapped ratio
norm: eyeOpenRatio,
// ummapped ratio
raw: ratio,
};
};
/**
* Calculate eyelid distance ratios based on landmarks on the face
*/
export const eyeLidRatio = (
eyeOuterCorner: XYZ | Vector | number[],
eyeInnerCorner: XYZ | Vector | number[],
eyeOuterUpperLid: XYZ | Vector | number[],
eyeMidUpperLid: XYZ | Vector | number[],
eyeInnerUpperLid: XYZ | Vector | number[],
eyeOuterLowerLid: XYZ | Vector | number[],
eyeMidLowerLid: XYZ | Vector | number[],
eyeInnerLowerLid: XYZ | Vector | number[]
) => {
eyeOuterCorner = new Vector(eyeOuterCorner);
eyeInnerCorner = new Vector(eyeInnerCorner);
eyeOuterUpperLid = new Vector(eyeOuterUpperLid);
eyeMidUpperLid = new Vector(eyeMidUpperLid);
eyeInnerUpperLid = new Vector(eyeInnerUpperLid);
eyeOuterLowerLid = new Vector(eyeOuterLowerLid);
eyeMidLowerLid = new Vector(eyeMidLowerLid);
eyeInnerLowerLid = new Vector(eyeInnerLowerLid);
//use 2D Distances instead of 3D for less jitter
const eyeWidth = (eyeOuterCorner as Vector).distance(eyeInnerCorner as Vector, 2);
const eyeOuterLidDistance = (eyeOuterUpperLid as Vector).distance(eyeOuterLowerLid as Vector, 2);
const eyeMidLidDistance = (eyeMidUpperLid as Vector).distance(eyeMidLowerLid as Vector, 2);
const eyeInnerLidDistance = (eyeInnerUpperLid as Vector).distance(eyeInnerLowerLid as Vector, 2);
const eyeLidAvg = (eyeOuterLidDistance + eyeMidLidDistance + eyeInnerLidDistance) / 3;
const ratio = eyeLidAvg / eyeWidth;
return ratio;
};
/**
* Calculate pupil position [-1,1]
* @param {Results} lm : array of results from tfjs or mediapipe
* @param {Side} side : left or right
*/
export const pupilPos = (lm: Results, side: Side = LEFT) => {
const eyeOuterCorner = new Vector(lm[points.eye[side][0]]);
const eyeInnerCorner = new Vector(lm[points.eye[side][1]]);
const eyeWidth = eyeOuterCorner.distance(eyeInnerCorner, 2);
const midPoint = eyeOuterCorner.lerp(eyeInnerCorner, 0.5);
const pupil = new Vector(lm[points.pupil[side][0]]);
const dx = midPoint.x - pupil.x;
//eye center y is slightly above midpoint
const dy = midPoint.y - eyeWidth * 0.075 - pupil.y;
let ratioX = dx / (eyeWidth / 2);
let ratioY = dy / (eyeWidth / 4);
ratioX *= 4;
ratioY *= 4;
return { x: ratioX, y: ratioY };
};
/**
* Method to stabilize blink speeds to fix inconsistent eye open/close timing
* @param {Object} eye : object with left and right eye values
* @param {Number} headY : head y axis rotation in radians
* @param {Object} options: Options for blink stabilization
*/
export const stabilizeBlink = (
eye: Record<"r" | "l", number>,
headY: number,
{
enableWink = true,
maxRot = 0.5,
}: {
/**
* Enable wink detection
* @default true
* @type {Boolean}
*/
enableWink?: boolean;
/**
* Maximum rotation of head to trigger wink
* @default 0.5
* @type {Number}
*/
maxRot?: number;
} = {}
) => {
eye.r = clamp(eye.r, 0, 1);
eye.l = clamp(eye.l, 0, 1);
//difference between each eye
const blinkDiff = Math.abs(eye.l - eye.r);
//theshold to which difference is considered a wink
const blinkThresh = enableWink ? 0.8 : 1.2;
//detect when both eyes are closing
const isClosing = eye.l < 0.3 && eye.r < 0.3;
//detect when both eyes are opening
const isOpen = eye.l > 0.6 && eye.r > 0.6;
// sets obstructed eye to the opposite eye value
if (headY > maxRot) {
return { l: eye.r, r: eye.r };
}
if (headY < -maxRot) {
return { l: eye.l, r: eye.l };
}
// returns either a wink or averaged blink values
return {
l:
blinkDiff >= blinkThresh && !isClosing && !isOpen
? eye.l
: eye.r > eye.l
? Vector.lerp(eye.r, eye.l, 0.95)
: Vector.lerp(eye.r, eye.l, 0.05),
r:
blinkDiff >= blinkThresh && !isClosing && !isOpen
? eye.r
: eye.r > eye.l
? Vector.lerp(eye.r, eye.l, 0.95)
: Vector.lerp(eye.r, eye.l, 0.05),
};
};
/**
* Calculate Eyes
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const calcEyes = (
lm: Results,
{
high = 0.85,
low = 0.55,
}: {
/**
* Upper bound for eye open ratio
* @default 0.85
* @type {Number}
*/
high?: number;
/**
* Lower bound for eye open ratio
* @default 0.55
* @type {Number}
**/
low?: number;
} = {}
) => {
//return early if no iris tracking
if (lm.length !== 478) {
return {
l: 1,
r: 1,
};
}
//open [0,1]
const leftEyeLid = getEyeOpen(lm, LEFT, { high: high, low: low });
const rightEyeLid = getEyeOpen(lm, RIGHT, { high: high, low: low });
return {
l: leftEyeLid.norm || 0,
r: rightEyeLid.norm || 0,
};
};
/**
* Calculate pupil location normalized to eye bounds
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const calcPupils = (lm: Results) => {
//pupil x:[-1,1],y:[-1,1]
if (lm.length !== 478) {
return { x: 0, y: 0 };
} else {
//track pupils using left eye
const pupilL = pupilPos(lm, LEFT);
const pupilR = pupilPos(lm, RIGHT);
return {
x: (pupilL.x + pupilR.x) * 0.5 || 0,
y: (pupilL.y + pupilR.y) * 0.5 || 0,
};
}
};
/**
* Calculate brow raise
* @param {Results} lm : array of results from tfjs or mediapipe
* @param {Side} side : designate left or right
*/
export const getBrowRaise = (lm: Results, side: Side = LEFT) => {
const browPoints = points.brow[side];
const browDistance = eyeLidRatio(
lm[browPoints[0]],
lm[browPoints[1]],
lm[browPoints[2]],
lm[browPoints[3]],
lm[browPoints[4]],
lm[browPoints[5]],
lm[browPoints[6]],
lm[browPoints[7]]
);
const maxBrowRatio = 1.15;
const browHigh = 0.125;
const browLow = 0.07;
const browRatio = browDistance / maxBrowRatio - 1;
const browRaiseRatio = (clamp(browRatio, browLow, browHigh) - browLow) / (browHigh - browLow);
return browRaiseRatio;
};
/**
* Take the average of left and right eyebrow raise values
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const calcBrow = (lm: Results) => {
if (lm.length !== 478) {
return 0;
} else {
const leftBrow = getBrowRaise(lm, LEFT);
const rightBrow = getBrowRaise(lm, RIGHT);
return (leftBrow + rightBrow) / 2 || 0;
}
};
+61
View File
@@ -0,0 +1,61 @@
import { Results } from "../Types";
import Vector from "../utils/vector";
import { PI } from "./../constants";
/**
* Calculate stable plane (triangle) from 4 face landmarks
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const createEulerPlane = (lm: Results) => {
//create face detection square bounds
const p1 = new Vector(lm[21]); //top left
const p2 = new Vector(lm[251]); //top right
const p3 = new Vector(lm[397]); //bottom right
const p4 = new Vector(lm[172]); //bottom left
const p3mid = p3.lerp(p4, 0.5); // bottom midpoint
return {
vector: [p1, p2, p3mid],
points: [p1, p2, p3, p4],
};
};
/**
* Calculate roll, pitch, yaw, centerpoint, and rough dimentions of face plane
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const calcHead = (lm: Results) => {
// find 3 vectors that form a plane to represent the head
const plane = createEulerPlane(lm).vector;
// calculate roll pitch and yaw from vectors
const rotate = Vector.rollPitchYaw(plane[0], plane[1], plane[2]);
// find the center of the face detection box
const midPoint = plane[0].lerp(plane[1], 0.5);
// find the dimensions roughly of the face detection box
const width = plane[0].distance(plane[1]);
const height = midPoint.distance(plane[2]);
//flip
rotate.x *= -1;
rotate.z *= -1;
return {
//defaults to radians for rotation around x,y,z axis
y: rotate.y * PI, //left right
x: rotate.x * PI, //up down
z: rotate.z * PI, //side to side
width: width,
height: height,
//center of face detection square
position: midPoint.lerp(plane[2], 0.5),
//returns euler angles normalized between -1 and 1
normalized: {
y: rotate.y,
x: rotate.x,
z: rotate.z,
},
degrees: {
y: rotate.y * 180,
x: rotate.x * 180,
z: rotate.z * 180,
},
};
};
+67
View File
@@ -0,0 +1,67 @@
import Vector from "../utils/vector";
import { remap, clamp } from "../utils/helpers";
import { Results } from "../Types";
/**
* Calculate Mouth Shape
* @param {Array} lm : array of results from tfjs or mediapipe
*/
export const calcMouth = (lm: Results) => {
// eye keypoints
const eyeInnerCornerL = new Vector(lm[133]);
const eyeInnerCornerR = new Vector(lm[362]);
const eyeOuterCornerL = new Vector(lm[130]);
const eyeOuterCornerR = new Vector(lm[263]);
// eye keypoint distances
const eyeInnerDistance = eyeInnerCornerL.distance(eyeInnerCornerR);
const eyeOuterDistance = eyeOuterCornerL.distance(eyeOuterCornerR);
// mouth keypoints
const upperInnerLip = new Vector(lm[13]);
const lowerInnerLip = new Vector(lm[14]);
const mouthCornerLeft = new Vector(lm[61]);
const mouthCornerRight = new Vector(lm[291]);
// mouth keypoint distances
const mouthOpen = upperInnerLip.distance(lowerInnerLip);
const mouthWidth = mouthCornerLeft.distance(mouthCornerRight);
// mouth open and mouth shape ratios
// let ratioXY = mouthWidth / mouthOpen;
let ratioY = mouthOpen / eyeInnerDistance;
let ratioX = mouthWidth / eyeOuterDistance;
// normalize and scale mouth open
ratioY = remap(ratioY, 0.15, 0.7);
// normalize and scale mouth shape
ratioX = remap(ratioX, 0.45, 0.9);
ratioX = (ratioX - 0.3) * 2;
// const mouthX = remap(ratioX - 0.4, 0, 0.5);
const mouthX = ratioX;
const mouthY = remap(mouthOpen / eyeInnerDistance, 0.17, 0.5);
//Depricated: Change sensitivity due to facemesh and holistic have different point outputs.
// const fixFacemesh = runtime === "tfjs" ? 1.3 : 0;
// let ratioI = remap(mouthXY, 1.3 + fixFacemesh * 0.8, 2.6 + fixFacemesh) * remap(mouthY, 0, 1);
const ratioI = clamp(remap(mouthX, 0, 1) * 2 * remap(mouthY, 0.2, 0.7), 0, 1);
const ratioA = mouthY * 0.4 + mouthY * (1 - ratioI) * 0.6;
const ratioU = mouthY * remap(1 - ratioI, 0, 0.3) * 0.1;
const ratioE = remap(ratioU, 0.2, 1) * (1 - ratioI) * 0.3;
const ratioO = (1 - ratioI) * remap(mouthY, 0.3, 1) * 0.4;
return {
x: ratioX || 0,
y: ratioY || 0,
shape: {
A: ratioA || 0,
E: ratioE || 0,
I: ratioI || 0,
O: ratioO || 0,
U: ratioU || 0,
},
};
};
+77
View File
@@ -0,0 +1,77 @@
import { calcHead } from "./calcHead";
import { calcEyes, stabilizeBlink, calcPupils, calcBrow } from "./calcEyes";
import { calcMouth } from "./calcMouth";
import { Results, IFaceSolveOptions, TFace } from "../Types";
/** Class representing face solver. */
export class FaceSolver {
/** expose blink stabilizer as a static method */
static stabilizeBlink = stabilizeBlink;
/**
* Combines head, eye, pupil, and eyebrow calcs into one method
* @param {Results} lm : array of results from tfjs or mediapipe
* @param {String} runtime: set as either "tfjs" or "mediapipe"
* @param {IFaceSolveOptions} options: options for face solver
*/
static solve(
lm: Results,
{
runtime = "tfjs",
video = null,
imageSize = null,
smoothBlink = false,
blinkSettings = [],
}: Partial<IFaceSolveOptions> = {}
): TFace | undefined {
if (!lm) {
console.error("Need Face Landmarks");
return;
}
// set image size based on video
if (video) {
const videoEl = (typeof video === "string" ? document.querySelector(video) : video) as HTMLVideoElement;
imageSize = {
width: videoEl.videoWidth,
height: videoEl.videoHeight,
};
}
//if runtime is mediapipe, we need the image dimentions for accurate calculations
if (runtime === "mediapipe" && imageSize) {
for (const e of lm) {
e.x *= imageSize.width;
e.y *= imageSize.height;
e.z *= imageSize.width;
}
}
const getHead = calcHead(lm);
const getMouth = calcMouth(lm);
//set high and low remapping values based on the runtime (tfjs vs mediapipe) of the results
blinkSettings = blinkSettings.length > 0 ? blinkSettings : runtime === "tfjs" ? [0.55, 0.85] : [0.35, 0.5];
let getEye = calcEyes(lm, {
high: blinkSettings[1],
low: blinkSettings[0],
});
// apply blink stabilizer if true
if (smoothBlink) {
getEye = stabilizeBlink(getEye, getHead.y);
}
const getPupils = calcPupils(lm);
const getBrow = calcBrow(lm);
return {
head: getHead,
eye: getEye,
brow: getBrow,
pupil: getPupils,
mouth: getMouth,
};
}
}
+118
View File
@@ -0,0 +1,118 @@
import Vector from "../utils/vector";
import { clamp } from "../utils/helpers";
import { Results, THand, THandUnsafe, Side } from "../Types";
import { RIGHT, LEFT, PI } from "./../constants";
/** Class representing hand solver. */
export class HandSolver {
/**
* Calculates finger and wrist as euler rotations
* @param {Array} lm : array of 3D hand vectors from tfjs or mediapipe
* @param {Side} side: left or right
*/
static solve(lm: Results, side: Side = RIGHT): THand<typeof side> | undefined {
if (!lm) {
console.error("Need Hand Landmarks");
return;
}
const palm = [
new Vector(lm[0]),
new Vector(lm[side === RIGHT ? 17 : 5]),
new Vector(lm[side === RIGHT ? 5 : 17]),
];
const handRotation = Vector.rollPitchYaw(palm[0], palm[1], palm[2]);
handRotation.y = handRotation.z;
handRotation.y -= side === LEFT ? 0.4 : 0.4;
let hand: Record<string, unknown> = {};
hand[side + "Wrist"] = { x: handRotation.x, y: handRotation.y, z: handRotation.z };
hand[side + "RingProximal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[0], lm[13], lm[14]) };
hand[side + "RingIntermediate"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[13], lm[14], lm[15]) };
hand[side + "RingDistal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[14], lm[15], lm[16]) };
hand[side + "IndexProximal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[0], lm[5], lm[6]) };
hand[side + "IndexIntermediate"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[5], lm[6], lm[7]) };
hand[side + "IndexDistal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[6], lm[7], lm[8]) };
hand[side + "MiddleProximal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[0], lm[9], lm[10]) };
hand[side + "MiddleIntermediate"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[9], lm[10], lm[11]) };
hand[side + "MiddleDistal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[10], lm[11], lm[12]) };
hand[side + "ThumbProximal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[0], lm[1], lm[2]) };
hand[side + "ThumbIntermediate"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[1], lm[2], lm[3]) };
hand[side + "ThumbDistal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[2], lm[3], lm[4]) };
hand[side + "LittleProximal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[0], lm[17], lm[18]) };
hand[side + "LittleIntermediate"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[17], lm[18], lm[19]) };
hand[side + "LittleDistal"] = { x: 0, y: 0, z: Vector.angleBetween3DCoords(lm[18], lm[19], lm[20]) };
hand = rigFingers(hand as THand<typeof side>, side);
return hand as THand<typeof side>;
}
}
/**
* Converts normalized rotation values into radians clamped by human limits
* @param {Object} hand : object of labeled joint with normalized rotation values
* @param {Side} side : left or right
*/
const rigFingers = (hand: THandUnsafe<typeof side>, side: Side = RIGHT): THand<typeof side> => {
// Invert modifier based on left vs right side
const invert = side === RIGHT ? 1 : -1;
const digits = ["Ring", "Index", "Little", "Thumb", "Middle"];
const segments = ["Proximal", "Intermediate", "Distal"];
hand[side + "Wrist"].x = clamp(hand[side + "Wrist"].x * 2 * invert, -0.3, 0.3); // twist
hand[side + "Wrist"].y = clamp(
hand[side + "Wrist"].y * 2.3,
side === RIGHT ? -1.2 : -0.6,
side === RIGHT ? 0.6 : 1.6
);
hand[side + "Wrist"].z = hand[side + "Wrist"].z * -2.3 * invert; //left right
digits.forEach((e) => {
segments.forEach((j) => {
const trackedFinger = hand[side + e + j];
if (e === "Thumb") {
//dampen thumb rotation depending on segment
const dampener = {
x: j === "Proximal" ? 2.2 : j === "Intermediate" ? 0 : 0,
y: j === "Proximal" ? 2.2 : j === "Intermediate" ? 0.7 : 1,
z: j === "Proximal" ? 0.5 : j === "Intermediate" ? 0.5 : 0.5,
};
const startPos = {
x: j === "Proximal" ? 1.2 : j === "Distal" ? -0.2 : -0.2,
y: j === "Proximal" ? 1.1 * invert : j === "Distal" ? 0.1 * invert : 0.1 * invert,
z: j === "Proximal" ? 0.2 * invert : j === "Distal" ? 0.2 * invert : 0.2 * invert,
};
const newThumb = { x: 0, y: 0, z: 0 };
if (j === "Proximal") {
newThumb.z = clamp(
startPos.z + trackedFinger.z * -PI * dampener.z * invert,
side === RIGHT ? -0.6 : -0.3,
side === RIGHT ? 0.3 : 0.6
);
newThumb.x = clamp(startPos.x + trackedFinger.z * -PI * dampener.x, -0.6, 0.3);
newThumb.y = clamp(
startPos.y + trackedFinger.z * -PI * dampener.y * invert,
side === RIGHT ? -1 : -0.3,
side === RIGHT ? 0.3 : 1
);
} else {
newThumb.z = clamp(startPos.z + trackedFinger.z * -PI * dampener.z * invert, -2, 2);
newThumb.x = clamp(startPos.x + trackedFinger.z * -PI * dampener.x, -2, 2);
newThumb.y = clamp(startPos.y + trackedFinger.z * -PI * dampener.y * invert, -2, 2);
}
trackedFinger.x = newThumb.x;
trackedFinger.y = newThumb.y;
trackedFinger.z = newThumb.z;
} else {
//will document human limits later
trackedFinger.z = clamp(
trackedFinger.z * -PI * invert,
side === RIGHT ? -PI : 0,
side === RIGHT ? 0 : PI
);
}
});
});
return hand as THand<typeof side>;
};
+100
View File
@@ -0,0 +1,100 @@
import Vector from "../utils/vector";
import { clamp } from "../utils/helpers";
import { Results, Side } from "../Types";
import { RIGHT, LEFT, PI } from "./../constants";
/**
* Calculates arm rotation as euler angles
* @param {Array} lm : array of 3D pose vectors from tfjs or mediapipe
*/
export const calcArms = (lm: Results) => {
//Pure Rotation Calculations
const UpperArm = {
r: Vector.findRotation(lm[11], lm[13]),
l: Vector.findRotation(lm[12], lm[14]),
};
UpperArm.r.y = Vector.angleBetween3DCoords(lm[12], lm[11], lm[13]);
UpperArm.l.y = Vector.angleBetween3DCoords(lm[11], lm[12], lm[14]);
const LowerArm = {
r: Vector.findRotation(lm[13], lm[15]),
l: Vector.findRotation(lm[14], lm[16]),
};
LowerArm.r.y = Vector.angleBetween3DCoords(lm[11], lm[13], lm[15]);
LowerArm.l.y = Vector.angleBetween3DCoords(lm[12], lm[14], lm[16]);
LowerArm.r.z = clamp(LowerArm.r.z, -2.14, 0);
LowerArm.l.z = clamp(LowerArm.l.z, -2.14, 0);
const Hand = {
r: Vector.findRotation(
Vector.fromArray(lm[15]),
Vector.lerp(Vector.fromArray(lm[17]), Vector.fromArray(lm[19]), 0.5)
),
l: Vector.findRotation(
Vector.fromArray(lm[16]),
Vector.lerp(Vector.fromArray(lm[18]), Vector.fromArray(lm[20]), 0.5)
),
};
//Modify Rotations slightly for more natural movement
const rightArmRig = rigArm(UpperArm.r, LowerArm.r, Hand.r, RIGHT);
const leftArmRig = rigArm(UpperArm.l, LowerArm.l, Hand.l, LEFT);
return {
//Scaled
UpperArm: {
r: rightArmRig.UpperArm,
l: leftArmRig.UpperArm,
},
LowerArm: {
r: rightArmRig.LowerArm,
l: leftArmRig.LowerArm,
},
Hand: {
r: rightArmRig.Hand,
l: leftArmRig.Hand,
},
//Unscaled
Unscaled: {
UpperArm: UpperArm,
LowerArm: LowerArm,
Hand: Hand,
},
};
};
/**
* Converts normalized rotation values into radians clamped by human limits
* @param {Object} UpperArm : normalized rotation values
* @param {Object} LowerArm : normalized rotation values
* @param {Object} Hand : normalized rotation values
* @param {Side} side : left or right
*/
export const rigArm = (UpperArm: Vector, LowerArm: Vector, Hand: Vector, side: Side = RIGHT) => {
// Invert modifier based on left vs right side
const invert = side === RIGHT ? 1 : -1;
UpperArm.z *= -2.3 * invert;
//Modify UpperArm rotationY by LowerArm X and Z rotations
UpperArm.y *= PI * invert;
UpperArm.y -= Math.max(LowerArm.x);
UpperArm.y -= -invert * Math.max(LowerArm.z, 0);
UpperArm.x -= 0.3 * invert;
LowerArm.z *= -2.14 * invert;
LowerArm.y *= 2.14 * invert;
LowerArm.x *= 2.14 * invert;
//Clamp values to human limits
UpperArm.x = clamp(UpperArm.x, -0.5, PI);
LowerArm.x = clamp(LowerArm.x, -0.3, 0.3);
Hand.y = clamp(Hand.z * 2, -0.6, 0.6); //side to side
Hand.z = Hand.z * -2.3 * invert; //up down
return {
//Returns Values in Radians for direct 3D usage
UpperArm: UpperArm,
LowerArm: LowerArm,
Hand: Hand,
};
};
+94
View File
@@ -0,0 +1,94 @@
import Vector from "../utils/vector";
import { clamp, remap } from "../utils/helpers";
import { IHips, XYZ, TFVectorPose } from "../Types";
import { PI } from "./../constants";
/**
* Calculates Hip rotation and world position
* @param {Array} lm3d : array of 3D pose vectors from tfjs or mediapipe
* @param {Array} lm2d : array of 2D pose vectors from tfjs or mediapipe
*/
export const calcHips = (lm3d: TFVectorPose, lm2d: Omit<TFVectorPose, "z">) => {
//Find 2D normalized Hip and Shoulder Joint Positions/Distances
const hipLeft2d = Vector.fromArray(lm2d[23]);
const hipRight2d = Vector.fromArray(lm2d[24]);
const shoulderLeft2d = Vector.fromArray(lm2d[11]);
const shoulderRight2d = Vector.fromArray(lm2d[12]);
const hipCenter2d = hipLeft2d.lerp(hipRight2d, 1);
const shoulderCenter2d = shoulderLeft2d.lerp(shoulderRight2d, 1);
const spineLength = hipCenter2d.distance(shoulderCenter2d);
const hips: IHips = {
position: {
x: clamp(hipCenter2d.x - 0.4, -1, 1), //subtract .4 to bring closer to 0,0 center
y: 0,
z: clamp(spineLength - 1, -2, 0),
},
};
hips.worldPosition = {
x: hips.position.x,
y: 0,
z: hips.position.z * Math.pow(hips.position.z * -2, 2),
};
hips.worldPosition.x *= hips.worldPosition.z;
hips.rotation = Vector.rollPitchYaw(lm3d[23], lm3d[24]);
//fix -PI, PI jumping
if (hips.rotation.y > 0.5) {
hips.rotation.y -= 2;
}
hips.rotation.y += 0.5;
//Stop jumping between left and right shoulder tilt
if (hips.rotation.z > 0) {
hips.rotation.z = 1 - hips.rotation.z;
}
if (hips.rotation.z < 0) {
hips.rotation.z = -1 - hips.rotation.z;
}
const turnAroundAmountHips = remap(Math.abs(hips.rotation.y), 0.2, 0.4);
hips.rotation.z *= 1 - turnAroundAmountHips;
hips.rotation.x = 0; //temp fix for inaccurate X axis
const spine = Vector.rollPitchYaw(lm3d[11], lm3d[12]);
//fix -PI, PI jumping
if (spine.y > 0.5) {
spine.y -= 2;
}
spine.y += 0.5;
//Stop jumping between left and right shoulder tilt
if (spine.z > 0) {
spine.z = 1 - spine.z;
}
if (spine.z < 0) {
spine.z = -1 - spine.z;
}
//fix weird large numbers when 2 shoulder points get too close
const turnAroundAmount = remap(Math.abs(spine.y), 0.2, 0.4);
spine.z *= 1 - turnAroundAmount;
spine.x = 0; //temp fix for inaccurate X axis
return rigHips(hips, spine);
};
/**
* Converts normalized rotations to radians and estimates world position of hips
* @param {Object} hips : hip position and rotation values
* @param {Object} spine : spine position and rotation values
*/
export const rigHips = (hips: IHips, spine: Vector | XYZ) => {
//convert normalized values to radians
if (hips.rotation) {
hips.rotation.x *= Math.PI;
hips.rotation.y *= Math.PI;
hips.rotation.z *= Math.PI;
}
spine.x *= PI;
spine.y *= PI;
spine.z *= PI;
return {
Hips: hips,
Spine: spine as XYZ,
};
};
+105
View File
@@ -0,0 +1,105 @@
import Vector from "../utils/vector";
import Euler from "../utils/euler";
import { clamp } from "../utils/helpers";
import { Results, Side } from "../Types";
import { RIGHT, LEFT, PI } from "./../constants";
export const offsets = {
upperLeg: {
z: 0.1,
},
};
/**
* Calculates leg rotation angles
* @param {Results} lm : array of 3D pose vectors from tfjs or mediapipe
*/
export const calcLegs = (lm: Results) => {
const rightUpperLegSphericalCoords = Vector.getSphericalCoords(lm[23], lm[25], { x: "y", y: "z", z: "x" });
const leftUpperLegSphericalCoords = Vector.getSphericalCoords(lm[24], lm[26], { x: "y", y: "z", z: "x" });
const rightLowerLegSphericalCoords = Vector.getRelativeSphericalCoords(lm[23], lm[25], lm[27], {
x: "y",
y: "z",
z: "x",
});
const leftLowerLegSphericalCoords = Vector.getRelativeSphericalCoords(lm[24], lm[26], lm[28], {
x: "y",
y: "z",
z: "x",
});
const hipRotation = Vector.findRotation(lm[23], lm[24]);
const UpperLeg = {
r: new Vector({
x: rightUpperLegSphericalCoords.theta,
y: rightLowerLegSphericalCoords.phi,
z: rightUpperLegSphericalCoords.phi - hipRotation.z,
}),
l: new Vector({
x: leftUpperLegSphericalCoords.theta,
y: leftLowerLegSphericalCoords.phi,
z: leftUpperLegSphericalCoords.phi - hipRotation.z,
}),
};
const LowerLeg = {
r: new Vector({
x: -Math.abs(rightLowerLegSphericalCoords.theta),
y: 0, // not relevant
z: 0, // not relevant
}),
l: new Vector({
x: -Math.abs(leftLowerLegSphericalCoords.theta),
y: 0, // not relevant
z: 0, // not relevant
}),
};
//Modify Rotations slightly for more natural movement
const rightLegRig = rigLeg(UpperLeg.r, LowerLeg.r, RIGHT);
const leftLegRig = rigLeg(UpperLeg.l, LowerLeg.l, LEFT);
return {
//Scaled
UpperLeg: {
r: rightLegRig.UpperLeg,
l: leftLegRig.UpperLeg,
},
LowerLeg: {
r: rightLegRig.LowerLeg,
l: leftLegRig.LowerLeg,
},
//Unscaled
Unscaled: {
UpperLeg,
LowerLeg,
},
};
};
/**
* Converts normalized rotation values into radians clamped by human limits
* @param {Object} UpperLeg : normalized rotation values
* @param {Object} LowerLeg : normalized rotation values
* @param {Side} side : left or right
*/
export const rigLeg = (UpperLeg: Vector, LowerLeg: Vector, side: Side = RIGHT) => {
const invert = side === RIGHT ? 1 : -1;
const rigedUpperLeg = new Euler({
x: clamp(UpperLeg.x, 0, 0.5) * PI,
y: clamp(UpperLeg.y, -0.25, 0.25) * PI,
z: clamp(UpperLeg.z, -0.5, 0.5) * PI + invert * offsets.upperLeg.z,
rotationOrder: "XYZ",
});
const rigedLowerLeg = new Euler({
x: LowerLeg.x * PI,
y: LowerLeg.y * PI,
z: LowerLeg.z * PI,
});
return {
UpperLeg: rigedUpperLeg,
LowerLeg: rigedLowerLeg,
};
};
+98
View File
@@ -0,0 +1,98 @@
import { TFVectorPose, IPoseSolveOptions, TPose } from "../Types";
import { RestingDefault } from "../utils/helpers";
import { calcArms } from "./calcArms";
import { calcHips } from "./calcHips";
import { calcLegs } from "./calcLegs";
/** Class representing pose solver. */
export class PoseSolver {
/** expose arm rotation calculator as a static method */
static calcArms = calcArms;
/** expose hips position and rotation calculator as a static method */
static calcHips = calcHips;
/** expose leg rotation calculator as a static method */
static calcLegs = calcLegs;
/**
* Combines arm, hips, and leg calcs into one method
* @param {Array} lm3d : array of 3D pose vectors from tfjs or mediapipe
* @param {Array} lm2d : array of 2D pose vectors from tfjs or mediapipe
* @param {String} runtime: set as either "tfjs" or "mediapipe"
* @param {IPoseSolveOptions} options: options object
*/
static solve(
lm3d: TFVectorPose,
lm2d: Omit<TFVectorPose, "z">,
{ runtime = "mediapipe", video = null, imageSize = null, enableLegs = true }: Partial<IPoseSolveOptions> = {}
): TPose | undefined {
if (!lm3d && !lm2d) {
console.error("Need both World Pose and Pose Landmarks");
return;
}
// format and normalize values given by tfjs output
if (video) {
const videoEl = (typeof video === "string" ? document.querySelector(video) : video) as HTMLVideoElement;
imageSize = {
width: videoEl.videoWidth,
height: videoEl.videoHeight,
};
}
if (runtime === "tfjs" && imageSize) {
for (const e of lm3d) {
e.visibility = e.score;
}
for (const e of lm2d) {
e.x /= imageSize.width;
e.y /= imageSize.height;
e.z = 0;
e.visibility = e.score;
}
}
const Arms = calcArms(lm3d);
const Hips = calcHips(lm3d, lm2d);
const Legs = enableLegs ? calcLegs(lm3d) : null;
//DETECT OFFSCREEN AND RESET VALUES TO DEFAULTS
const rightHandOffscreen = lm3d[15].y > 0.1 || (lm3d[15].visibility ?? 0) < 0.23 || 0.995 < lm2d[15].y;
const leftHandOffscreen = lm3d[16].y > 0.1 || (lm3d[16].visibility ?? 0) < 0.23 || 0.995 < lm2d[16].y;
const leftFootOffscreen = lm3d[23].y > 0.1 || (lm3d[23].visibility ?? 0) < 0.63 || Hips.Hips.position.z > -0.4;
const rightFootOffscreen = lm3d[24].y > 0.1 || (lm3d[24].visibility ?? 0) < 0.63 || Hips.Hips.position.z > -0.4;
Arms.UpperArm.l = Arms.UpperArm.l.multiply(leftHandOffscreen ? 0 : 1);
Arms.UpperArm.l.z = leftHandOffscreen ? RestingDefault.Pose.LeftUpperArm.z : Arms.UpperArm.l.z;
Arms.UpperArm.r = Arms.UpperArm.r.multiply(rightHandOffscreen ? 0 : 1);
Arms.UpperArm.r.z = rightHandOffscreen ? RestingDefault.Pose.RightUpperArm.z : Arms.UpperArm.r.z;
Arms.LowerArm.l = Arms.LowerArm.l.multiply(leftHandOffscreen ? 0 : 1);
Arms.LowerArm.r = Arms.LowerArm.r.multiply(rightHandOffscreen ? 0 : 1);
Arms.Hand.l = Arms.Hand.l.multiply(leftHandOffscreen ? 0 : 1);
Arms.Hand.r = Arms.Hand.r.multiply(rightHandOffscreen ? 0 : 1);
//skip calculations if disable legs
if (Legs) {
Legs.UpperLeg.l = Legs.UpperLeg.l.multiply(rightFootOffscreen ? 0 : 1);
Legs.UpperLeg.r = Legs.UpperLeg.r.multiply(leftFootOffscreen ? 0 : 1);
Legs.LowerLeg.l = Legs.LowerLeg.l.multiply(rightFootOffscreen ? 0 : 1);
Legs.LowerLeg.r = Legs.LowerLeg.r.multiply(leftFootOffscreen ? 0 : 1);
}
return {
RightUpperArm: Arms.UpperArm.r,
RightLowerArm: Arms.LowerArm.r,
LeftUpperArm: Arms.UpperArm.l,
LeftLowerArm: Arms.LowerArm.l,
RightHand: Arms.Hand.r,
LeftHand: Arms.Hand.l,
RightUpperLeg: Legs ? Legs.UpperLeg.r : RestingDefault.Pose.RightUpperLeg,
RightLowerLeg: Legs ? Legs.LowerLeg.r : RestingDefault.Pose.RightLowerLeg,
LeftUpperLeg: Legs ? Legs.UpperLeg.l : RestingDefault.Pose.LeftUpperLeg,
LeftLowerLeg: Legs ? Legs.LowerLeg.l : RestingDefault.Pose.LeftLowerLeg,
Hips: Hips.Hips,
Spine: Hips.Spine,
};
}
}
+148
View File
@@ -0,0 +1,148 @@
import Vector from "./utils/vector";
import Euler from "./utils/euler";
import { RIGHT, LEFT } from "./constants";
export interface ISolveOptions {
/**
* Runtime for the solver.
* @default "mediapipe"
* @type {"tfjs" | "mediapipe"}
*/
runtime: "tfjs" | "mediapipe";
/**
* HTML Video element or selector for the video element.
* @type {HTMLElement | string}
*/
video: null | HTMLVideoElement | string;
/**
* Set Manual Size
* @type {{ width: number, height: number }}
* @default null
*/
imageSize: null | { width: number; height: number };
}
export interface IFaceSolveOptions extends ISolveOptions {
/**
* Smooth Blink Toggle
* @type {boolean}
* @default false
*/
smoothBlink: boolean;
/**
* Blink settings
*/
blinkSettings: Array<number>;
}
export interface IPoseSolveOptions extends ISolveOptions {
/**
* Toggle Calculation of legs
* @type {boolean}
* @default true
*/
enableLegs: boolean;
}
/**
* The left or the right side
*/
export type Side = typeof RIGHT | typeof LEFT;
export type XYZ = Record<"x" | "y" | "z", number>;
export type LR<T = Vector> = Record<"l" | "r", T>;
export type RotationOrder = "XYZ" | "YZX" | "ZXY" | "XZY" | "YXZ" | "ZYX";
export type EulerRotation = XYZ & { rotationOrder?: RotationOrder };
export type AxisMap = Record<"x" | "y" | "z", "x" | "y" | "z">;
export interface IHips {
position: XYZ;
rotation?: Vector;
worldPosition?: XYZ;
}
export type TPose = {
RightUpperArm: Euler;
RightLowerArm: Euler;
LeftUpperArm: Euler;
LeftLowerArm: Euler;
RightHand: Vector;
LeftHand: Vector;
RightUpperLeg: Euler | XYZ;
RightLowerLeg: Euler | XYZ;
LeftUpperLeg: Euler | XYZ;
LeftLowerLeg: Euler | XYZ;
Hips: IHips;
Spine: Vector | XYZ;
};
export type HandKeys<S extends Side> = `${S}${
| "Wrist"
| "RingProximal"
| "RingIntermediate"
| "RingDistal"
| "IndexProximal"
| "IndexIntermediate"
| "IndexDistal"
| "MiddleProximal"
| "MiddleIntermediate"
| "MiddleDistal"
| "ThumbProximal"
| "ThumbIntermediate"
| "ThumbDistal"
| "LittleProximal"
| "LittleIntermediate"
| "LittleDistal"}`;
export type THand<S extends Side> = Record<HandKeys<S>, XYZ>;
export type THandUnsafe<S extends Side> = Record<HandKeys<S> | string, XYZ>;
export type TFace = {
head: {
x: number;
y: number;
z: number;
width: number;
height: number;
position: Vector | XYZ;
normalized: Vector | XYZ;
degrees: Vector | XYZ;
};
eye: {
l: number;
r: number;
};
brow: number;
pupil: {
x: number;
y: number;
};
mouth: {
x: number;
y: number;
shape: {
A: number;
E: number;
I: number;
O: number;
U: number;
};
};
};
/**
* TFJS 3D Pose Vector Type
*/
export type TFVectorPose = Array<{
x: number;
y: number;
z: number;
score?: number;
visibility?: number;
}>;
/**
* Array of results from TFJS or MediaPipe
*/
export type Results = Array<XYZ>;
+5
View File
@@ -0,0 +1,5 @@
export const RIGHT = "Right";
export const LEFT = "Left";
export const PI = Math.PI;
export const TWO_PI = Math.PI * 2;
+6
View File
@@ -0,0 +1,6 @@
export { PoseSolver as Pose } from "./PoseSolver";
export { HandSolver as Hand } from "./HandSolver";
export { FaceSolver as Face } from "./FaceSolver";
export { default as Vector } from "./utils/vector";
export * as Utils from "./utils/helpers";
export * from "./Types";
+33
View File
@@ -0,0 +1,33 @@
import { XYZ, RotationOrder, EulerRotation } from "../Types";
import { Vector } from "..";
/** Euler rotation class. */
export default class Euler {
public x: number;
public y: number;
public z: number;
public rotationOrder?: RotationOrder;
constructor(a?: number | EulerRotation, b?: number, c?: number, rotationOrder?: RotationOrder) {
if (!!a && typeof a === "object") {
this.x = a.x ?? 0;
this.y = a.y ?? 0;
this.z = a.z ?? 0;
this.rotationOrder = a.rotationOrder ?? "XYZ";
return;
}
this.x = a ?? 0;
this.y = b ?? 0;
this.z = c ?? 0;
this.rotationOrder = rotationOrder ?? "XYZ";
}
/**
* Multiplies a number to an Euler.
* @param {number} a: Number to multiply
*/
multiply(v: number) {
return new Euler(this.x * v, this.y * v, this.z * v, this.rotationOrder);
}
}
+291
View File
@@ -0,0 +1,291 @@
/**
* Returns a clamped value between min and max values
* @param {Number} val : transformed value
* @param {Number} min : minimum value
* @param {Number} max : maximum value
*/
export const clamp = (val: number, min: number, max: number) => {
return Math.max(Math.min(val, max), min);
};
/**
* Returns a remapped value between 0 and 1 using min and max values
* @param {Number} value : transformed value
* @param {Number} min : minimum value
* @param {Number} max : maximum value
*/
export const remap = (val: number, min: number, max: number) => {
//returns min to max -> 0 to 1
return (clamp(val, min, max) - min) / (max - min);
};
/** A set of default pose values in radians to serve as "rest" values */
export const RestingDefault = {
Face: {
eye: {
l: 1,
r: 1,
},
mouth: {
x: 0,
y: 0,
shape: {
A: 0,
E: 0,
I: 0,
O: 0,
U: 0,
},
},
head: {
x: 0,
y: 0,
z: 0,
width: 0.3,
height: 0.6,
position: {
x: 0.5,
y: 0.5,
z: 0,
},
},
brow: 0,
pupil: {
x: 0,
y: 0,
},
},
Pose: {
RightUpperArm: {
x: 0,
y: 0,
z: -1.25,
},
LeftUpperArm: {
x: 0,
y: 0,
z: 1.25,
}, //y: 0 > -.5 // z: -.5>.5
RightLowerArm: {
x: 0,
y: 0,
z: 0,
},
LeftLowerArm: {
x: 0,
y: 0,
z: 0,
}, //x: 0 > -4, z: 0 to -.9
LeftUpperLeg: {
x: 0,
y: 0,
z: 0,
},
RightUpperLeg: {
x: 0,
y: 0,
z: 0,
},
RightLowerLeg: {
x: 0,
y: 0,
z: 0,
},
LeftLowerLeg: {
x: 0,
y: 0,
z: 0,
},
LeftHand: {
x: 0,
y: 0,
z: 0,
},
RightHand: {
x: 0,
y: 0,
z: 0,
},
Spine: {
x: 0,
y: 0,
z: 0,
},
Hips: {
position: {
x: 0,
y: 0,
z: 0,
},
rotation: {
x: 0,
y: 0,
z: 0,
},
},
},
RightHand: {
RightWrist: {
x: -0.13,
y: -0.07,
z: -1.04,
},
RightRingProximal: {
x: 0,
y: 0,
z: -0.13,
},
RightRingIntermediate: {
x: 0,
y: 0,
z: -0.4,
},
RightRingDistal: {
x: 0,
y: 0,
z: -0.04,
},
RightIndexProximal: {
x: 0,
y: 0,
z: -0.24,
},
RightIndexIntermediate: {
x: 0,
y: 0,
z: -0.25,
},
RightIndexDistal: {
x: 0,
y: 0,
z: -0.06,
},
RightMiddleProximal: {
x: 0,
y: 0,
z: -0.09,
},
RightMiddleIntermediate: {
x: 0,
y: 0,
z: -0.44,
},
RightMiddleDistal: {
x: 0,
y: 0,
z: -0.06,
},
RightThumbProximal: {
x: -0.23,
y: -0.33,
z: -0.12,
},
RightThumbIntermediate: {
x: -0.2,
y: -0.199,
z: -0.0139,
},
RightThumbDistal: {
x: -0.2,
y: 0.002,
z: 0.15,
},
RightLittleProximal: {
x: 0,
y: 0,
z: -0.09,
},
RightLittleIntermediate: {
x: 0,
y: 0,
z: -0.225,
},
RightLittleDistal: {
x: 0,
y: 0,
z: -0.1,
},
},
LeftHand: {
LeftWrist: {
x: -0.13,
y: -0.07,
z: -1.04,
},
LeftRingProximal: {
x: 0,
y: 0,
z: 0.13,
},
LeftRingIntermediate: {
x: 0,
y: 0,
z: 0.4,
},
LeftRingDistal: {
x: 0,
y: 0,
z: 0.049,
},
LeftIndexProximal: {
x: 0,
y: 0,
z: 0.24,
},
LeftIndexIntermediate: {
x: 0,
y: 0,
z: 0.25,
},
LeftIndexDistal: {
x: 0,
y: 0,
z: 0.06,
},
LeftMiddleProximal: {
x: 0,
y: 0,
z: 0.09,
},
LeftMiddleIntermediate: {
x: 0,
y: 0,
z: 0.44,
},
LeftMiddleDistal: {
x: 0,
y: 0,
z: 0.066,
},
LeftThumbProximal: {
x: -0.23,
y: 0.33,
z: 0.12,
},
LeftThumbIntermediate: {
x: -0.2,
y: 0.25,
z: 0.05,
},
LeftThumbDistal: {
x: -0.2,
y: 0.17,
z: -0.06,
},
LeftLittleProximal: {
x: 0,
y: 0,
z: 0.17,
},
LeftLittleIntermediate: {
x: 0,
y: 0,
z: 0.4,
},
LeftLittleDistal: {
x: 0,
y: 0,
z: 0.1,
},
},
};
+474
View File
@@ -0,0 +1,474 @@
import { XYZ, AxisMap, EulerRotation } from "../Types";
import { PI, TWO_PI } from "./../constants";
/** Vector Math class. */
export default class Vector {
public x: number;
public y: number;
public z: number;
constructor(a?: number[] | XYZ | number | Vector | EulerRotation, b?: number, c?: number) {
if (Array.isArray(a)) {
this.x = a[0] ?? 0;
this.y = a[1] ?? 0;
this.z = a[2] ?? 0;
return;
}
if (!!a && typeof a === "object") {
this.x = a.x ?? 0;
this.y = a.y ?? 0;
this.z = a.z ?? 0;
return;
}
this.x = a ?? 0;
this.y = b ?? 0;
this.z = c ?? 0;
}
// Methods //
/**
* Returns the negative of this vector.
*/
negative() {
return new Vector(-this.x, -this.y, -this.z);
}
/**
* Add a vector or number to this vector.
* @param {Vector | number} a: Vector or number to add
* @returns {Vector} New vector
*/
add(v: Vector | number) {
if (v instanceof Vector) return new Vector(this.x + v.x, this.y + v.y, this.z + v.z);
else return new Vector(this.x + v, this.y + v, this.z + v);
}
/**
* Substracts a vector or number from this vector.
* @param {Vector | number} a: Vector or number to subtract
* @returns {Vector} New vector
*/
subtract(v: Vector | number) {
if (v instanceof Vector) return new Vector(this.x - v.x, this.y - v.y, this.z - v.z);
else return new Vector(this.x - v, this.y - v, this.z - v);
}
/**
* Multiplies a vector or a number to a vector.
* @param {Vector | number} a: Vector or number to multiply
* @param {Vector} b: Vector to multiply
*/
multiply(v: Vector | number) {
if (v instanceof Vector) return new Vector(this.x * v.x, this.y * v.y, this.z * v.z);
else return new Vector(this.x * v, this.y * v, this.z * v);
}
/**
* Divide this vector by a vector or a number.
* @param {Vector | number} a: Vector or number to divide
* @returns {Vector} New vector
*/
divide(v: Vector | number) {
if (v instanceof Vector) return new Vector(this.x / v.x, this.y / v.y, this.z / v.z);
else return new Vector(this.x / v, this.y / v, this.z / v);
}
/**
* Check if the given vector is equal to this vector.
* @param {Vector} v: Vector to compare
* @returns {boolean} True if equal
*/
equals(v: Vector) {
return this.x == v.x && this.y == v.y && this.z == v.z;
}
/**
* Returns the dot product of this vector and another vector.
* @param {Vector} v: Vector to dot
* @returns {number} Dot product
*/
dot(v: Vector) {
return this.x * v.x + this.y * v.y + this.z * v.z;
}
/**
* Cross product of two vectors.
* @param {Vector} a: Vector to cross
* @param {Vector} b: Vector to cross
*/
cross(v: Vector) {
return new Vector(this.y * v.z - this.z * v.y, this.z * v.x - this.x * v.z, this.x * v.y - this.y * v.x);
}
/**
* Get the length of the Vector
* @returns {number} Length
*/
length() {
return Math.sqrt(this.dot(this));
}
/**
* Find the distance between this and another vector.
* @param {Vector} v: Vector to find distance to
* @param {2 | 3} d: 2D or 3D distance
* @returns {number} Distance
*/
distance(v: Vector, d: 2 | 3 = 3) {
//2D distance
if (d === 2) return Math.sqrt(Math.pow(this.x - v.x, 2) + Math.pow(this.y - v.y, 2));
//3D distance
else return Math.sqrt(Math.pow(this.x - v.x, 2) + Math.pow(this.y - v.y, 2) + Math.pow(this.z - v.z, 2));
}
/**
* Lerp between this vector and another vector.
* @param {Vector} v: Vector to lerp to
* @param {number} fraction: Fraction to lerp
* @returns {Vector}
*/
lerp(v: Vector, fraction: number) {
return v.subtract(this).multiply(fraction).add(this);
}
/**
* Returns the unit vector of this vector.
* @returns {Vector} Unit vector
*/
unit() {
return this.divide(this.length());
}
min() {
return Math.min(Math.min(this.x, this.y), this.z);
}
max() {
return Math.max(Math.max(this.x, this.y), this.z);
}
/**
* To Angles
* @param {AxisMap} [axisMap = {x: "x", y: "y", z: "z"}]
* @returns {{ theta: number, phi: number }}
*/
toSphericalCoords(axisMap: AxisMap = { x: "x", y: "y", z: "z" }) {
return {
theta: Math.atan2(this[axisMap.y], this[axisMap.x]),
phi: Math.acos(this[axisMap.z] / this.length()),
};
}
/**
* Returns the angle between this vector and vector a in radians.
* @param {Vector} a: Vector
* @returns {number}
*/
angleTo(a: Vector) {
return Math.acos(this.dot(a) / (this.length() * a.length()));
}
/**
* Array representation of the vector.
* @param {number} n: Array length
* @returns {number[]} Array
* @example
* new Vector(1, 2, 3).toArray(); // [1, 2, 3]
*/
toArray(n: number) {
return [this.x, this.y, this.z].slice(0, n || 3);
}
/**
* Clone the vector.
* @returns {Vector} New vector
*/
clone() {
return new Vector(this.x, this.y, this.z);
}
/**
* Init this Vector with explicit values
* @param {number} x: X value
* @param {number} y: Y value
* @param {number} z: Z value
*/
init(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
// static methods //
static negative(a: Vector, b: Vector = new Vector()) {
b.x = -a.x;
b.y = -a.y;
b.z = -a.z;
return b;
}
static add(a: Vector, b: Vector | number, c: Vector = new Vector()) {
if (b instanceof Vector) {
c.x = a.x + b.x;
c.y = a.y + b.y;
c.z = a.z + b.z;
} else {
c.x = a.x + b;
c.y = a.y + b;
c.z = a.z + b;
}
return c;
}
static subtract(a: Vector, b: Vector | number, c: Vector = new Vector()) {
if (b instanceof Vector) {
c.x = a.x - b.x;
c.y = a.y - b.y;
c.z = a.z - b.z;
} else {
c.x = a.x - b;
c.y = a.y - b;
c.z = a.z - b;
}
return c;
}
static multiply(a: Vector, b: Vector | number, c: Vector = new Vector()) {
if (b instanceof Vector) {
c.x = a.x * b.x;
c.y = a.y * b.y;
c.z = a.z * b.z;
} else {
c.x = a.x * b;
c.y = a.y * b;
c.z = a.z * b;
}
return c;
}
static divide(a: Vector, b: Vector | number, c: Vector = new Vector()) {
if (b instanceof Vector) {
c.x = a.x / b.x;
c.y = a.y / b.y;
c.z = a.z / b.z;
} else {
c.x = a.x / b;
c.y = a.y / b;
c.z = a.z / b;
}
return c;
}
static cross(a: Vector, b: Vector, c: Vector = new Vector()) {
c.x = a.y * b.z - a.z * b.y;
c.y = a.z * b.x - a.x * b.z;
c.z = a.x * b.y - a.y * b.x;
return c;
}
static unit(a: Vector, b: Vector) {
const length = a.length();
b.x = a.x / length;
b.y = a.y / length;
b.z = a.z / length;
return b;
}
/**
* Create new vector from angles
* @param {number} theta: Theta angle
* @param {number} phi: Phi angle
* @returns {Vector} New vector
*/
static fromAngles(theta: number, phi: number) {
return new Vector(Math.cos(theta) * Math.cos(phi), Math.sin(phi), Math.sin(theta) * Math.cos(phi));
}
static randomDirection() {
return Vector.fromAngles(Math.random() * TWO_PI, Math.asin(Math.random() * 2 - 1));
}
static min(a: Vector, b: Vector) {
return new Vector(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.min(a.z, b.z));
}
static max(a: Vector, b: Vector) {
return new Vector(Math.max(a.x, b.x), Math.max(a.y, b.y), Math.max(a.z, b.z));
}
/**
* Lerp between two vectors
* @param {Vector} a: Vector a
* @param {Vector} b: Vector b
* @param {number} fraction: Fraction
*/
static lerp<T extends number | Vector>(a: T, b: T, fraction: number): T {
if (b instanceof Vector) {
return b.subtract(a).multiply(fraction).add(a) as unknown as T;
} else {
return (((b as number) - (a as number)) * fraction + (a as unknown as number)) as unknown as T;
}
}
/**
* Create a new vector from an Array
* @param {number[]} array: Array
* @returns {Vector} New vector
*/
static fromArray(a: Array<number> | XYZ) {
if (Array.isArray(a)) {
return new Vector(a[0], a[1], a[2]);
}
return new Vector(a.x, a.y, a.z);
}
/**
* Angle between two vectors
* @param {Vector} a: Vector a
* @param {Vector} b: Vector b
* @returns
*/
static angleBetween(a: Vector, b: Vector) {
return a.angleTo(b);
}
static distance(a: Vector, b: Vector, d: number) {
if (d === 2) return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
else return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) + Math.pow(a.z - b.z, 2));
}
static toDegrees(a: number) {
return a * (180 / PI);
}
static normalizeAngle(radians: number) {
let angle = radians % TWO_PI;
angle = angle > PI ? angle - TWO_PI : angle < -PI ? TWO_PI + angle : angle;
//returns normalized values to -1,1
return angle / PI;
}
static normalizeRadians(radians: number) {
if (radians >= PI / 2) {
radians -= TWO_PI;
}
if (radians <= -PI / 2) {
radians += TWO_PI;
radians = PI - radians;
}
//returns normalized values to -1,1
return radians / PI;
}
static find2DAngle(cx: number, cy: number, ex: number, ey: number) {
const dy = ey - cy;
const dx = ex - cx;
const theta = Math.atan2(dy, dx);
return theta;
}
/**
* Find 3D rotation between two vectors
* @param {Vector} a: First vector
* @param {Vector} b: Second vector
* @param {boolean} normalize: Normalize the result
*/
static findRotation(a: Vector | XYZ, b: Vector | XYZ, normalize = true) {
if (normalize) {
return new Vector(
Vector.normalizeRadians(Vector.find2DAngle(a.z, a.x, b.z, b.x)),
Vector.normalizeRadians(Vector.find2DAngle(a.z, a.y, b.z, b.y)),
Vector.normalizeRadians(Vector.find2DAngle(a.x, a.y, b.x, b.y))
);
} else {
return new Vector(
Vector.find2DAngle(a.z, a.x, b.z, b.x),
Vector.find2DAngle(a.z, a.y, b.z, b.y),
Vector.find2DAngle(a.x, a.y, b.x, b.y)
);
}
}
/**
* Find roll pitch yaw of plane formed by 3 points
* @param {Vector} a: Vector
* @param {Vector} b: Vector
* @param {Vector} c: Vector
*/
static rollPitchYaw(a: Vector | XYZ, b: Vector | XYZ, c?: Vector) {
if (!c) {
return new Vector(
Vector.normalizeAngle(Vector.find2DAngle(a.z, a.y, b.z, b.y)),
Vector.normalizeAngle(Vector.find2DAngle(a.z, a.x, b.z, b.x)),
Vector.normalizeAngle(Vector.find2DAngle(a.x, a.y, b.x, b.y))
);
}
const qb = (b as Vector).subtract(a as Vector);
const qc = c.subtract(a as Vector);
const n = qb.cross(qc);
const unitZ = n.unit();
const unitX = qb.unit();
const unitY = unitZ.cross(unitX);
const beta = Math.asin(unitZ.x) || 0;
const alpha = Math.atan2(-unitZ.y, unitZ.z) || 0;
const gamma = Math.atan2(-unitY.x, unitX.x) || 0;
return new Vector(Vector.normalizeAngle(alpha), Vector.normalizeAngle(beta), Vector.normalizeAngle(gamma));
}
/**
* Find angle between 3D Coordinates
* @param {Vector | number} a: Vector or Number
* @param {Vector | number} b: Vector or Number
* @param {Vector | number} c: Vector or Number
*/
static angleBetween3DCoords(a: Vector | XYZ, b: Vector | XYZ, c: Vector | XYZ) {
if (!(a instanceof Vector)) {
a = new Vector(a);
b = new Vector(b);
c = new Vector(c);
}
// Calculate vector between points 1 and 2
const v1 = (a as Vector).subtract(b as Vector);
// Calculate vector between points 2 and 3
const v2 = (c as Vector).subtract(b as Vector);
// The dot product of vectors v1 & v2 is a function of the cosine of the
// angle between them (it's scaled by the product of their magnitudes).
const v1norm = v1.unit();
const v2norm = v2.unit();
// Calculate the dot products of vectors v1 and v2
const dotProducts = v1norm.dot(v2norm);
// Extract the angle from the dot products
const angle = Math.acos(dotProducts);
// return single angle Normalized to 1
return Vector.normalizeRadians(angle);
}
/**
* Get normalized, spherical coordinates for the vector bc, relative to vector ab
* @param {Vector | number} a: Vector or Number
* @param {Vector | number} b: Vector or Number
* @param {Vector | number} c: Vector or Number
* @param {AxisMap} axisMap: Mapped axis to get the right spherical coords
*/
static getRelativeSphericalCoords(a: Vector | XYZ, b: Vector | XYZ, c: Vector | XYZ, axisMap: AxisMap) {
if (!(a instanceof Vector)) {
a = new Vector(a);
b = new Vector(b);
c = new Vector(c);
}
// Calculate vector between points 1 and 2
const v1 = (b as Vector).subtract(a as Vector);
// Calculate vector between points 2 and 3
const v2 = (c as Vector).subtract(b as Vector);
const v1norm = v1.unit();
const v2norm = v2.unit();
const { theta: theta1, phi: phi1 } = v1norm.toSphericalCoords(axisMap);
const { theta: theta2, phi: phi2 } = v2norm.toSphericalCoords(axisMap);
const theta = theta1 - theta2;
const phi = phi1 - phi2;
return {
theta: Vector.normalizeAngle(theta),
phi: Vector.normalizeAngle(phi),
};
}
/**
* Get normalized, spherical coordinates for the vector bc
* @param {Vector | number} a: Vector or Number
* @param {Vector | number} b: Vector or Number
* @param {AxisMap} axisMap: Mapped axis to get the right spherical coords
*/
static getSphericalCoords(a: Vector | XYZ, b: Vector | XYZ, axisMap: AxisMap = { x: "x", y: "y", z: "z" }) {
if (!(a instanceof Vector)) {
a = new Vector(a);
b = new Vector(b);
}
// Calculate vector between points 1 and 2
const v1 = (b as Vector).subtract(a as Vector);
const v1norm = v1.unit();
const { theta, phi } = v1norm.toSphericalCoords(axisMap);
return {
theta: Vector.normalizeAngle(-theta),
phi: Vector.normalizeAngle(PI / 2 - phi),
};
}
}
+148
View File
@@ -0,0 +1,148 @@
const cloneDeep = require("lodash.clonedeep")
import { calcLegs } from "../src/PoseSolver/calcLegs"
import poseWorldLandmarks from "./poseWorlLandmarks"
import { Results } from "../src/Types"
import { offsets } from "../src/PoseSolver/calcLegs"
let worldLandmarks: Results
const PI = Math.PI
describe("should resolve the correct upper leg rotation for", () => {
beforeEach(() => {
worldLandmarks = cloneDeep(poseWorldLandmarks)
})
test("upper left leg, 90 degree forward, up", () => {
worldLandmarks[23] = { x: -0.2, y: 0, z: 0 }
worldLandmarks[24] = { x: 0, y: 0, z: 0 }
worldLandmarks[26] = { x: 0, y: 0, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.l.x).toBeCloseTo(PI / 2)
expect(result.Unscaled.UpperLeg.l.x).toBeCloseTo(0.5)
expect(result.UpperLeg.l.z).toBeCloseTo((0 - offsets.upperLeg.z))
expect(result.Unscaled.UpperLeg.l.z).toBeCloseTo(0)
})
test("upper left leg, straight down", () => {
worldLandmarks[23] = { x: -0.2, y: 0, z: 0 }
worldLandmarks[24] = { x: 0, y: 0, z: 0 }
worldLandmarks[26] = { x: 0, y: 0.4, z: 0 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.l.x).toBeCloseTo(0)
expect(result.Unscaled.UpperLeg.l.z).toBeCloseTo(0)
expect(result.UpperLeg.l.z).toBeCloseTo((0 - offsets.upperLeg.z))
expect(result.Unscaled.UpperLeg.l.x).toBeCloseTo(-0)
})
test("upper left leg, 90 degree forward, up, 45 degree left", () => {
worldLandmarks[23] = { x: -0.2, y: 0, z: 0 }
worldLandmarks[24] = { x: 0, y: 0, z: 0 }
worldLandmarks[26] = { x: -0.4, y: 0, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.l.x).toBeCloseTo(PI / 2)
expect(result.Unscaled.UpperLeg.l.x).toBeCloseTo(0.5)
expect(result.UpperLeg.l.z).toBeCloseTo((-PI / 4 - offsets.upperLeg.z))
expect(result.Unscaled.UpperLeg.l.z).toBeCloseTo(-0.25)
})
test("upper right leg, 90 degree forward, up", () => {
worldLandmarks[23] = { x: 0, y: 0, z: 0 }
worldLandmarks[24] = { x: 0.2, y: 0, z: 0 }
worldLandmarks[25] = { x: 0, y: 0, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.r.x).toBeCloseTo(PI / 2)
expect(result.Unscaled.UpperLeg.r.x).toBeCloseTo(0.5)
expect(result.UpperLeg.r.z).toBeCloseTo(0 + offsets.upperLeg.z)
expect(result.Unscaled.UpperLeg.r.z).toBeCloseTo(0)
})
test("upper right leg, straight down", () => {
worldLandmarks[23] = { x: 0, y: 0, z: 0 }
worldLandmarks[24] = { x: 0.2, y: 0, z: 0 }
worldLandmarks[25] = { x: 0, y: 0.4, z: 0 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.r.x).toBeCloseTo(0)
expect(result.Unscaled.UpperLeg.r.z).toBeCloseTo(0)
expect(result.UpperLeg.r.z).toBeCloseTo((0 + offsets.upperLeg.z))
expect(result.Unscaled.UpperLeg.r.x).toBeCloseTo(0)
})
test("upper right leg, 90 degree forward, up, 45 degree left", () => {
worldLandmarks[23] = { x: 0, y: 0, z: 0 }
worldLandmarks[24] = { x: 0.2, y: 0, z: 0 }
worldLandmarks[25] = { x: 0.4, y: 0, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.UpperLeg.r.x).toBeCloseTo(PI / 2)
expect(result.Unscaled.UpperLeg.r.x).toBeCloseTo(0.5)
expect(result.UpperLeg.r.z).toBeCloseTo((PI / 4 + offsets.upperLeg.z))
expect(result.Unscaled.UpperLeg.r.z).toBeCloseTo(0.25)
})
})
describe("should resolve the correct lower leg rotation for", () => {
beforeEach(() => {
worldLandmarks = cloneDeep(poseWorldLandmarks)
})
test("lower left leg, 90 degree", () => {
worldLandmarks[24] = { x: 0, y: 0, z: 0 }
worldLandmarks[26] = { x: 0, y: 0, z: -0.4 }
worldLandmarks[28] = { x: 0, y: 0.4, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.LowerLeg.l.x).toBeCloseTo(-PI / 2)
expect(result.Unscaled.LowerLeg.l.x).toBeCloseTo(-0.5)
})
test("lower left leg, 45 degree", () => {
worldLandmarks[24] = { x: 0, y: 0, z: 0 }
worldLandmarks[26] = { x: 0, y: 0, z: -0.4 }
worldLandmarks[28] = { x: 0, y: 0.4, z: -0.8 }
const result = calcLegs(worldLandmarks)
expect(result.LowerLeg.l.x).toBeCloseTo(-PI / 4)
expect(result.Unscaled.LowerLeg.l.x).toBeCloseTo(-0.25)
})
test("lower right leg, 90 degree", () => {
worldLandmarks[23] = { x: -0.2, y: 0, z: 0 }
worldLandmarks[25] = { x: 0, y: 0, z: -0.4 }
worldLandmarks[27] = { x: 0, y: 0.4, z: -0.4 }
const result = calcLegs(worldLandmarks)
expect(result.LowerLeg.r.x).toBeCloseTo(-PI / 2)
expect(result.Unscaled.LowerLeg.r.x).toBeCloseTo(-0.5)
})
test("lower right leg, 45 degree", () => {
worldLandmarks[23] = { x: 0, y: 0, z: 0 }
worldLandmarks[25] = { x: 0, y: 0, z: -0.4 }
worldLandmarks[27] = { x: 0, y: 0.4, z: -0.8 }
const result = calcLegs(worldLandmarks)
expect(result.LowerLeg.r.x).toBeCloseTo(-PI / 4)
expect(result.Unscaled.LowerLeg.r.x).toBeCloseTo(-0.25)
})
})
+35
View File
@@ -0,0 +1,35 @@
export default {
0: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
1: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
2: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
3: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
4: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
5: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
6: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
7: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
8: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
9: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
10: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
11: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
12: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
13: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
14: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
15: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
16: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
17: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
18: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
19: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
20: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
21: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
22: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
23: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
24: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
25: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
26: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
27: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
28: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
29: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
30: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
31: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0},
32: {x: 0.0, y: 0.0, z: 0.0, visibility: 1.0}
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"ESNext",
"ESNext.Array",
"DOM"
],
"module": "es6",
"moduleResolution": "node",
"outDir": "dist",
"declaration": true,
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
},
"exclude": [
"node_modules",
"tests",
"dist"
],
"include": [
"src/**/*"
]
}
+33
View File
@@ -0,0 +1,33 @@
const path = require("path");
const { defineConfig } = require("vite");
import Banner from "vite-plugin-banner";
import pkg from "./package.json";
import friendlyTypeImports from "rollup-plugin-friendly-type-imports";
import { resolve } from "path";
module.exports = defineConfig({
base: "./",
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "Kalidokit",
fileName: (format) => `kalidokit.${format}.js`,
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
exports: "named",
external: [],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {},
},
},
},
plugins: [
Banner(
`/**\n * @${pkg.name} v${pkg.version}\n * ${pkg.description}\n * \n * @license\n * Copyright (c) ${pkg.year} ${pkg.author}\n * SPDX-License-Idntifier: ${pkg.license} \n * ${pkg.homepage}\n */`
),
friendlyTypeImports(),
],
});
+3657 -5
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -46,7 +46,7 @@
"detect-gpu": "^5.0.38",
"http-proxy-middleware": "^3.0.0",
"isbot": "^4.1.0",
"kalidokit": "^1.1.5",
"kalidokit": "file:./kalidokit",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
@@ -64,6 +64,7 @@
},
"devDependencies": {
"@chromatic-com/storybook": "^1.3.5",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@remix-run/dev": "^2.8.1",
"@storybook/addon-actions": "^8.1.10",
"@storybook/addon-essentials": "^8.1.10",
+3 -3
View File
@@ -9,15 +9,15 @@
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
"ESNext"
],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2022",
"target": "ESNext",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
+40
View File
@@ -2,12 +2,35 @@ import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { netlifyPlugin } from "@netlify/remix-edge-adapter/plugin";
import { viteCommonjs, esbuildCommonjs } from '@originjs/vite-plugin-commonjs'
import path from 'path'
export default defineConfig({
//build: {
//},
//resolve: {
// alias: {
// 'kalidokit': `${path.resolve(__dirname, 'src')}/`,
// },
//},
//rollupOptions: {
// // make sure to externalize deps that shouldn't be bundled
// // into your library
// exports: "named",
// external: [],
// output: {
// // Provide global variables to use in the UMD build
// // for externalized deps
// globals: {},
// },
//},
plugins: [
remix(),
netlifyPlugin(),
tsconfigPaths(),
//viteCommonjs({
// include: ["kalidokit"],
//}),
{
name: "configure-response-headers",
configureServer: (server) => {
@@ -30,4 +53,21 @@ export default defineConfig({
},
},
},
//optimizeDeps: {
// esbuildOptions: {
// plugins: [
// esbuildCommonjs(['kalidokit'])
// ]
// }
//}
optimizeDeps: {
include: [
//'vue',
//'vue-router',
//'@vueuse/core',
//'@vueuse/head',
//'consola',
//'kalidokit',
],
}
});